@@ -3,14 +3,18 @@ import {
33 AppSchema ,
44 AppBrandingSchema ,
55 NavigationItemSchema ,
6+ NavigationAreaSchema ,
67 ObjectNavItemSchema ,
78 DashboardNavItemSchema ,
89 PageNavItemSchema ,
910 UrlNavItemSchema ,
11+ ReportNavItemSchema ,
12+ ActionNavItemSchema ,
1013 GroupNavItemSchema ,
1114 defineApp ,
1215 type App ,
1316 type NavigationItem ,
17+ type NavigationArea ,
1418} from './app.zod' ;
1519
1620describe ( 'AppBrandingSchema' , ( ) => {
@@ -664,3 +668,317 @@ describe('AppSchema sharing and embed fields', () => {
664668 expect ( app . navigation ) . toHaveLength ( 1 ) ;
665669 } ) ;
666670} ) ;
671+
672+ describe ( 'BaseNavItem new fields (order, badge, requiredPermissions)' , ( ) => {
673+ it ( 'should accept nav item with order' , ( ) => {
674+ const item = NavigationItemSchema . parse ( {
675+ id : 'nav_home' ,
676+ label : 'Home' ,
677+ type : 'dashboard' ,
678+ dashboardName : 'home' ,
679+ order : 1 ,
680+ } ) ;
681+ expect ( item . order ) . toBe ( 1 ) ;
682+ } ) ;
683+
684+ it ( 'should accept nav item with string badge' , ( ) => {
685+ const item = NavigationItemSchema . parse ( {
686+ id : 'nav_inbox' ,
687+ label : 'Inbox' ,
688+ type : 'object' ,
689+ objectName : 'message' ,
690+ badge : 'New' ,
691+ } ) ;
692+ expect ( item . badge ) . toBe ( 'New' ) ;
693+ } ) ;
694+
695+ it ( 'should accept nav item with numeric badge' , ( ) => {
696+ const item = NavigationItemSchema . parse ( {
697+ id : 'nav_tasks' ,
698+ label : 'Tasks' ,
699+ type : 'object' ,
700+ objectName : 'task' ,
701+ badge : 5 ,
702+ } ) ;
703+ expect ( item . badge ) . toBe ( 5 ) ;
704+ } ) ;
705+
706+ it ( 'should accept nav item with requiredPermissions' , ( ) => {
707+ const item = NavigationItemSchema . parse ( {
708+ id : 'nav_admin' ,
709+ label : 'Admin Panel' ,
710+ type : 'page' ,
711+ pageName : 'admin_panel' ,
712+ requiredPermissions : [ 'admin.access' , 'system.manage' ] ,
713+ } ) ;
714+ expect ( item . requiredPermissions ) . toEqual ( [ 'admin.access' , 'system.manage' ] ) ;
715+ } ) ;
716+
717+ it ( 'should accept nav item with all new fields combined' , ( ) => {
718+ const item = NavigationItemSchema . parse ( {
719+ id : 'nav_reports' ,
720+ label : 'Reports' ,
721+ type : 'page' ,
722+ pageName : 'reports_page' ,
723+ order : 10 ,
724+ badge : 3 ,
725+ visible : 'user.is_admin' ,
726+ requiredPermissions : [ 'reports.view' ] ,
727+ } ) ;
728+ expect ( item . order ) . toBe ( 10 ) ;
729+ expect ( item . badge ) . toBe ( 3 ) ;
730+ expect ( item . requiredPermissions ) . toEqual ( [ 'reports.view' ] ) ;
731+ } ) ;
732+
733+ it ( 'should still accept nav items without new fields (backward compatibility)' , ( ) => {
734+ const item = NavigationItemSchema . parse ( {
735+ id : 'nav_accounts' ,
736+ label : 'Accounts' ,
737+ type : 'object' ,
738+ objectName : 'account' ,
739+ } ) ;
740+ expect ( item . order ) . toBeUndefined ( ) ;
741+ expect ( item . badge ) . toBeUndefined ( ) ;
742+ expect ( item . requiredPermissions ) . toBeUndefined ( ) ;
743+ } ) ;
744+ } ) ;
745+
746+ describe ( 'ReportNavItemSchema' , ( ) => {
747+ it ( 'should accept report nav item' , ( ) => {
748+ const navItem = {
749+ id : 'nav_sales_report' ,
750+ label : 'Sales Report' ,
751+ icon : 'file-text' ,
752+ type : 'report' as const ,
753+ reportName : 'quarterly_sales' ,
754+ } ;
755+
756+ const result = ReportNavItemSchema . parse ( navItem ) ;
757+ expect ( result . type ) . toBe ( 'report' ) ;
758+ expect ( result . reportName ) . toBe ( 'quarterly_sales' ) ;
759+ } ) ;
760+
761+ it ( 'should accept report nav item via NavigationItemSchema' , ( ) => {
762+ const item = NavigationItemSchema . parse ( {
763+ id : 'nav_pipeline_report' ,
764+ label : 'Pipeline Report' ,
765+ type : 'report' ,
766+ reportName : 'pipeline_overview' ,
767+ order : 5 ,
768+ } ) ;
769+ expect ( item . type ) . toBe ( 'report' ) ;
770+ expect ( item . reportName ) . toBe ( 'pipeline_overview' ) ;
771+ } ) ;
772+
773+ it ( 'should reject report nav item without reportName' , ( ) => {
774+ expect ( ( ) => ReportNavItemSchema . parse ( {
775+ id : 'nav_bad_report' ,
776+ label : 'Bad Report' ,
777+ type : 'report' ,
778+ } ) ) . toThrow ( ) ;
779+ } ) ;
780+ } ) ;
781+
782+ describe ( 'ActionNavItemSchema' , ( ) => {
783+ it ( 'should accept action nav item' , ( ) => {
784+ const navItem = {
785+ id : 'nav_new_lead' ,
786+ label : 'New Lead' ,
787+ icon : 'plus' ,
788+ type : 'action' as const ,
789+ actionDef : {
790+ actionName : 'create_lead' ,
791+ } ,
792+ } ;
793+
794+ const result = ActionNavItemSchema . parse ( navItem ) ;
795+ expect ( result . type ) . toBe ( 'action' ) ;
796+ expect ( result . actionDef . actionName ) . toBe ( 'create_lead' ) ;
797+ } ) ;
798+
799+ it ( 'should accept action nav item with params' , ( ) => {
800+ const result = ActionNavItemSchema . parse ( {
801+ id : 'nav_run_flow' ,
802+ label : 'Run Flow' ,
803+ type : 'action' ,
804+ actionDef : {
805+ actionName : 'launch_approval_flow' ,
806+ params : { recordType : 'contract' , priority : 'high' } ,
807+ } ,
808+ } ) ;
809+ expect ( result . actionDef . params ) . toEqual ( { recordType : 'contract' , priority : 'high' } ) ;
810+ } ) ;
811+
812+ it ( 'should accept action nav item via NavigationItemSchema' , ( ) => {
813+ const item = NavigationItemSchema . parse ( {
814+ id : 'nav_export' ,
815+ label : 'Export Data' ,
816+ type : 'action' ,
817+ actionDef : { actionName : 'export_csv' } ,
818+ badge : 'New' ,
819+ } ) ;
820+ expect ( item . type ) . toBe ( 'action' ) ;
821+ expect ( item . actionDef . actionName ) . toBe ( 'export_csv' ) ;
822+ } ) ;
823+
824+ it ( 'should reject action nav item without actionDef' , ( ) => {
825+ expect ( ( ) => ActionNavItemSchema . parse ( {
826+ id : 'nav_bad_action' ,
827+ label : 'Bad Action' ,
828+ type : 'action' ,
829+ } ) ) . toThrow ( ) ;
830+ } ) ;
831+ } ) ;
832+
833+ describe ( 'NavigationAreaSchema' , ( ) => {
834+ it ( 'should accept minimal area' , ( ) => {
835+ const area = NavigationAreaSchema . parse ( {
836+ id : 'area_sales' ,
837+ label : 'Sales' ,
838+ navigation : [ ] ,
839+ } ) ;
840+ expect ( area . id ) . toBe ( 'area_sales' ) ;
841+ expect ( area . navigation ) . toEqual ( [ ] ) ;
842+ } ) ;
843+
844+ it ( 'should accept area with full properties' , ( ) => {
845+ const area = NavigationAreaSchema . parse ( {
846+ id : 'area_service' ,
847+ label : 'Service' ,
848+ icon : 'headset' ,
849+ order : 2 ,
850+ description : 'Customer service management' ,
851+ visible : 'user.has_permission("service.access")' ,
852+ requiredPermissions : [ 'service.access' ] ,
853+ navigation : [
854+ { id : 'nav_cases' , type : 'object' , label : 'Cases' , objectName : 'case' } ,
855+ { id : 'nav_knowledge' , type : 'page' , label : 'Knowledge Base' , pageName : 'knowledge_base' } ,
856+ ] ,
857+ } ) ;
858+ expect ( area . id ) . toBe ( 'area_service' ) ;
859+ expect ( area . icon ) . toBe ( 'headset' ) ;
860+ expect ( area . order ) . toBe ( 2 ) ;
861+ expect ( area . requiredPermissions ) . toEqual ( [ 'service.access' ] ) ;
862+ expect ( area . navigation ) . toHaveLength ( 2 ) ;
863+ } ) ;
864+
865+ it ( 'should enforce snake_case for area id' , ( ) => {
866+ expect ( ( ) => NavigationAreaSchema . parse ( {
867+ id : 'AreaSales' ,
868+ label : 'Sales' ,
869+ navigation : [ ] ,
870+ } ) ) . toThrow ( ) ;
871+ } ) ;
872+ } ) ;
873+
874+ describe ( 'AppSchema with areas' , ( ) => {
875+ it ( 'should accept app with areas' , ( ) => {
876+ const app = AppSchema . parse ( {
877+ name : 'enterprise_crm' ,
878+ label : 'Enterprise CRM' ,
879+ areas : [
880+ {
881+ id : 'area_sales' ,
882+ label : 'Sales' ,
883+ icon : 'briefcase' ,
884+ order : 1 ,
885+ navigation : [
886+ { id : 'nav_leads' , type : 'object' , label : 'Leads' , objectName : 'lead' } ,
887+ { id : 'nav_opportunities' , type : 'object' , label : 'Opportunities' , objectName : 'opportunity' } ,
888+ ] ,
889+ } ,
890+ {
891+ id : 'area_service' ,
892+ label : 'Service' ,
893+ icon : 'headset' ,
894+ order : 2 ,
895+ navigation : [
896+ { id : 'nav_cases' , type : 'object' , label : 'Cases' , objectName : 'case' } ,
897+ ] ,
898+ } ,
899+ {
900+ id : 'area_settings' ,
901+ label : 'Settings' ,
902+ icon : 'settings' ,
903+ order : 99 ,
904+ requiredPermissions : [ 'admin.access' ] ,
905+ navigation : [
906+ { id : 'nav_users' , type : 'object' , label : 'Users' , objectName : 'user' } ,
907+ ] ,
908+ } ,
909+ ] ,
910+ } ) ;
911+
912+ expect ( app . areas ) . toHaveLength ( 3 ) ;
913+ expect ( app . areas ! [ 0 ] . id ) . toBe ( 'area_sales' ) ;
914+ expect ( app . areas ! [ 0 ] . navigation ) . toHaveLength ( 2 ) ;
915+ expect ( app . areas ! [ 2 ] . requiredPermissions ) . toEqual ( [ 'admin.access' ] ) ;
916+ } ) ;
917+
918+ it ( 'should accept app with both navigation and areas (backward compatibility)' , ( ) => {
919+ const app = AppSchema . parse ( {
920+ name : 'hybrid_app' ,
921+ label : 'Hybrid App' ,
922+ navigation : [
923+ { id : 'nav_home' , type : 'dashboard' , label : 'Home' , dashboardName : 'home' } ,
924+ ] ,
925+ areas : [
926+ {
927+ id : 'area_main' ,
928+ label : 'Main' ,
929+ navigation : [
930+ { id : 'nav_accounts' , type : 'object' , label : 'Accounts' , objectName : 'account' } ,
931+ ] ,
932+ } ,
933+ ] ,
934+ } ) ;
935+ expect ( app . navigation ) . toHaveLength ( 1 ) ;
936+ expect ( app . areas ) . toHaveLength ( 1 ) ;
937+ } ) ;
938+
939+ it ( 'should accept app without areas (backward compatibility)' , ( ) => {
940+ const app = AppSchema . parse ( {
941+ name : 'simple_app' ,
942+ label : 'Simple App' ,
943+ navigation : [
944+ { id : 'nav_home' , type : 'object' , label : 'Home' , objectName : 'account' } ,
945+ ] ,
946+ } ) ;
947+ expect ( app . areas ) . toBeUndefined ( ) ;
948+ expect ( app . navigation ) . toHaveLength ( 1 ) ;
949+ } ) ;
950+
951+ it ( 'should accept enterprise app with areas containing all nav item types' , ( ) => {
952+ const app = AppSchema . parse ( {
953+ name : 'full_enterprise' ,
954+ label : 'Full Enterprise' ,
955+ areas : [
956+ {
957+ id : 'area_main' ,
958+ label : 'Main' ,
959+ order : 1 ,
960+ navigation : [
961+ { id : 'nav_home' , type : 'dashboard' , label : 'Home' , dashboardName : 'home' } ,
962+ { id : 'nav_accounts' , type : 'object' , label : 'Accounts' , objectName : 'account' , order : 1 } ,
963+ { id : 'nav_custom' , type : 'page' , label : 'Custom Page' , pageName : 'custom' } ,
964+ { id : 'nav_report' , type : 'report' , label : 'Sales Report' , reportName : 'quarterly_sales' } ,
965+ { id : 'nav_action' , type : 'action' , label : 'New Lead' , actionDef : { actionName : 'create_lead' } } ,
966+ { id : 'nav_docs' , type : 'url' , label : 'Docs' , url : 'https://docs.example.com' , target : '_blank' } ,
967+ {
968+ id : 'grp_nested' ,
969+ type : 'group' ,
970+ label : 'Nested' ,
971+ expanded : true ,
972+ children : [
973+ { id : 'nav_nested_item' , type : 'object' , label : 'Nested Item' , objectName : 'nested' , badge : 3 } ,
974+ ] ,
975+ } ,
976+ ] ,
977+ } ,
978+ ] ,
979+ } ) ;
980+
981+ expect ( app . areas ) . toHaveLength ( 1 ) ;
982+ expect ( app . areas ! [ 0 ] . navigation ) . toHaveLength ( 7 ) ;
983+ } ) ;
984+ } ) ;
0 commit comments