create-mithaq 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/LICENSES/payload.txt +22 -0
- package/LICENSES/ponytail.txt +21 -0
- package/index.js +127 -0
- package/package.json +19 -4
- package/template/apps/asas-platform/components.json +25 -0
- package/template/apps/asas-platform/gitignore +3 -0
- package/template/apps/asas-platform/package.json +55 -0
- package/template/apps/asas-platform/public/favicon.svg +1 -0
- package/template/apps/asas-platform/src/api.ts +147 -0
- package/template/apps/asas-platform/src/auth-frame.tsx +150 -0
- package/template/apps/asas-platform/src/components/mithaq/cells.tsx +112 -0
- package/template/apps/asas-platform/src/components/mithaq/controls.tsx +650 -0
- package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +53 -0
- package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +108 -0
- package/template/apps/asas-platform/src/components/mithaq/form.tsx +1467 -0
- package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +108 -0
- package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +368 -0
- package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +153 -0
- package/template/apps/asas-platform/src/components/mithaq/list-query.ts +432 -0
- package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +74 -0
- package/template/apps/asas-platform/src/components/mithaq/list.tsx +865 -0
- package/template/apps/asas-platform/src/components/mithaq/nav.tsx +871 -0
- package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +1024 -0
- package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +127 -0
- package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +400 -0
- package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +6 -0
- package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +1083 -0
- package/template/apps/asas-platform/src/components/mithaq/upload.tsx +686 -0
- package/template/apps/asas-platform/src/components/ui/accordion.tsx +74 -0
- package/template/apps/asas-platform/src/components/ui/alert-dialog.tsx +185 -0
- package/template/apps/asas-platform/src/components/ui/avatar.tsx +106 -0
- package/template/apps/asas-platform/src/components/ui/badge.tsx +48 -0
- package/template/apps/asas-platform/src/components/ui/breadcrumb.tsx +124 -0
- package/template/apps/asas-platform/src/components/ui/button-group.tsx +87 -0
- package/template/apps/asas-platform/src/components/ui/button.tsx +55 -0
- package/template/apps/asas-platform/src/components/ui/calendar.tsx +223 -0
- package/template/apps/asas-platform/src/components/ui/card.tsx +102 -0
- package/template/apps/asas-platform/src/components/ui/chart.tsx +371 -0
- package/template/apps/asas-platform/src/components/ui/checkbox.tsx +28 -0
- package/template/apps/asas-platform/src/components/ui/collapsible.tsx +19 -0
- package/template/apps/asas-platform/src/components/ui/combobox.tsx +308 -0
- package/template/apps/asas-platform/src/components/ui/dialog.tsx +158 -0
- package/template/apps/asas-platform/src/components/ui/direction.tsx +6 -0
- package/template/apps/asas-platform/src/components/ui/directional-icon.tsx +20 -0
- package/template/apps/asas-platform/src/components/ui/drawer.tsx +216 -0
- package/template/apps/asas-platform/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/apps/asas-platform/src/components/ui/field.tsx +236 -0
- package/template/apps/asas-platform/src/components/ui/input-group.tsx +155 -0
- package/template/apps/asas-platform/src/components/ui/input-otp.tsx +84 -0
- package/template/apps/asas-platform/src/components/ui/input.tsx +19 -0
- package/template/apps/asas-platform/src/components/ui/label.tsx +19 -0
- package/template/apps/asas-platform/src/components/ui/popover.tsx +90 -0
- package/template/apps/asas-platform/src/components/ui/radio-group.tsx +37 -0
- package/template/apps/asas-platform/src/components/ui/resizable.tsx +47 -0
- package/template/apps/asas-platform/src/components/ui/scroll-area.tsx +54 -0
- package/template/apps/asas-platform/src/components/ui/select.tsx +204 -0
- package/template/apps/asas-platform/src/components/ui/separator.tsx +24 -0
- package/template/apps/asas-platform/src/components/ui/sheet.tsx +139 -0
- package/template/apps/asas-platform/src/components/ui/sidebar.tsx +714 -0
- package/template/apps/asas-platform/src/components/ui/skeleton.tsx +13 -0
- package/template/apps/asas-platform/src/components/ui/slider.tsx +55 -0
- package/template/apps/asas-platform/src/components/ui/spinner.tsx +10 -0
- package/template/apps/asas-platform/src/components/ui/switch.tsx +29 -0
- package/template/apps/asas-platform/src/components/ui/table.tsx +115 -0
- package/template/apps/asas-platform/src/components/ui/tabs.tsx +79 -0
- package/template/apps/asas-platform/src/components/ui/textarea.tsx +17 -0
- package/template/apps/asas-platform/src/components/ui/toggle-group.tsx +89 -0
- package/template/apps/asas-platform/src/components/ui/toggle.tsx +44 -0
- package/template/apps/asas-platform/src/components/ui/tooltip.tsx +63 -0
- package/template/apps/asas-platform/src/copy.ts +1 -0
- package/template/apps/asas-platform/src/hooks/use-mobile.ts +19 -0
- package/template/apps/asas-platform/src/lib/utils.ts +1 -0
- package/template/apps/asas-platform/src/links.ts +38 -0
- package/template/apps/asas-platform/src/locale-switch.tsx +26 -0
- package/template/apps/asas-platform/src/redirect.ts +1 -0
- package/template/apps/asas-platform/src/request-headers.ts +11 -0
- package/template/apps/asas-platform/src/routeTree.gen.ts +181 -0
- package/template/apps/asas-platform/src/router.tsx +7 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +4 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform/screen.json +3609 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform/types.ts +145 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform.$.tsx +17 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform.index.tsx +10 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform.tsx +13 -0
- package/template/apps/asas-platform/src/routes/__root.tsx +95 -0
- package/template/apps/asas-platform/src/routes/api/$.ts +30 -0
- package/template/apps/asas-platform/src/routes/auth.sign-in.tsx +72 -0
- package/template/apps/asas-platform/src/routes/unauthorized.tsx +122 -0
- package/template/apps/asas-platform/src/screen-form.tsx +34 -0
- package/template/apps/asas-platform/src/screen-list.tsx +139 -0
- package/template/apps/asas-platform/src/screen-names.ts +10 -0
- package/template/apps/asas-platform/src/screens.tsx +868 -0
- package/template/apps/asas-platform/src/server.ts +20 -0
- package/template/apps/asas-platform/src/slide.ts +22 -0
- package/template/apps/asas-platform/src/start.ts +42 -0
- package/template/apps/asas-platform/src/styles.css +200 -0
- package/template/apps/asas-platform/src/theme.ts +2 -0
- package/template/apps/asas-platform/tsconfig.json +19 -0
- package/template/apps/asas-platform/vite.config.ts +26 -0
- package/template/apps/asas-tanstack/components.json +25 -0
- package/template/apps/asas-tanstack/gitignore +3 -0
- package/template/apps/asas-tanstack/package.json +56 -0
- package/template/apps/asas-tanstack/public/favicon.svg +1 -0
- package/template/apps/asas-tanstack/src/api.ts +147 -0
- package/template/apps/asas-tanstack/src/auth-frame.tsx +150 -0
- package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +112 -0
- package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +650 -0
- package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +53 -0
- package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +108 -0
- package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +1467 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +108 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +368 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +153 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +432 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +74 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +865 -0
- package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +871 -0
- package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +1024 -0
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +127 -0
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +400 -0
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +6 -0
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +1083 -0
- package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +686 -0
- package/template/apps/asas-tanstack/src/components/ui/accordion.tsx +10 -0
- package/template/apps/asas-tanstack/src/components/ui/alert-dialog.tsx +173 -0
- package/template/apps/asas-tanstack/src/components/ui/avatar.tsx +104 -0
- package/template/apps/asas-tanstack/src/components/ui/badge.tsx +48 -0
- package/template/apps/asas-tanstack/src/components/ui/breadcrumb.tsx +125 -0
- package/template/apps/asas-tanstack/src/components/ui/button-group.tsx +87 -0
- package/template/apps/asas-tanstack/src/components/ui/button.tsx +56 -0
- package/template/apps/asas-tanstack/src/components/ui/calendar.tsx +223 -0
- package/template/apps/asas-tanstack/src/components/ui/card.tsx +94 -0
- package/template/apps/asas-tanstack/src/components/ui/chart.tsx +369 -0
- package/template/apps/asas-tanstack/src/components/ui/checkbox.tsx +27 -0
- package/template/apps/asas-tanstack/src/components/ui/collapsible.tsx +19 -0
- package/template/apps/asas-tanstack/src/components/ui/combobox.tsx +300 -0
- package/template/apps/asas-tanstack/src/components/ui/dialog.tsx +149 -0
- package/template/apps/asas-tanstack/src/components/ui/direction.tsx +5 -0
- package/template/apps/asas-tanstack/src/components/ui/directional-icon.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +215 -0
- package/template/apps/asas-tanstack/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/apps/asas-tanstack/src/components/ui/field.tsx +225 -0
- package/template/apps/asas-tanstack/src/components/ui/input-group.tsx +147 -0
- package/template/apps/asas-tanstack/src/components/ui/input-otp.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/input.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/label.tsx +18 -0
- package/template/apps/asas-tanstack/src/components/ui/popover.tsx +50 -0
- package/template/apps/asas-tanstack/src/components/ui/radio-group.tsx +10 -0
- package/template/apps/asas-tanstack/src/components/ui/resizable.tsx +47 -0
- package/template/apps/asas-tanstack/src/components/ui/scroll-area.tsx +52 -0
- package/template/apps/asas-tanstack/src/components/ui/select.tsx +190 -0
- package/template/apps/asas-tanstack/src/components/ui/separator.tsx +23 -0
- package/template/apps/asas-tanstack/src/components/ui/sheet.tsx +127 -0
- package/template/apps/asas-tanstack/src/components/ui/sidebar.tsx +716 -0
- package/template/apps/asas-tanstack/src/components/ui/skeleton.tsx +13 -0
- package/template/apps/asas-tanstack/src/components/ui/slider.tsx +13 -0
- package/template/apps/asas-tanstack/src/components/ui/spinner.tsx +11 -0
- package/template/apps/asas-tanstack/src/components/ui/switch.tsx +8 -0
- package/template/apps/asas-tanstack/src/components/ui/table.tsx +119 -0
- package/template/apps/asas-tanstack/src/components/ui/tabs.tsx +80 -0
- package/template/apps/asas-tanstack/src/components/ui/textarea.tsx +18 -0
- package/template/apps/asas-tanstack/src/components/ui/toggle-group.tsx +8 -0
- package/template/apps/asas-tanstack/src/components/ui/toggle.tsx +44 -0
- package/template/apps/asas-tanstack/src/components/ui/tooltip.tsx +68 -0
- package/template/apps/asas-tanstack/src/copy.ts +870 -0
- package/template/apps/asas-tanstack/src/hooks/use-mobile.ts +19 -0
- package/template/apps/asas-tanstack/src/lib/utils.ts +6 -0
- package/template/apps/asas-tanstack/src/links.ts +38 -0
- package/template/apps/asas-tanstack/src/locale-switch.tsx +26 -0
- package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
- package/template/apps/asas-tanstack/src/redirect.ts +20 -0
- package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
- package/template/apps/asas-tanstack/src/routeTree.gen.ts +382 -0
- package/template/apps/asas-tanstack/src/router.tsx +7 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +4 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +3883 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +158 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.$.tsx +17 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.index.tsx +10 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.tsx +13 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +4 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1800 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +92 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.$.tsx +17 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.index.tsx +10 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.tsx +13 -0
- package/template/apps/asas-tanstack/src/routes/__root.tsx +99 -0
- package/template/apps/asas-tanstack/src/routes/api/$.ts +11 -0
- package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +101 -0
- package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +94 -0
- package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +39 -0
- package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +109 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +86 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +122 -0
- package/template/apps/asas-tanstack/src/routes/index.tsx +142 -0
- package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +122 -0
- package/template/apps/asas-tanstack/src/screen-form.tsx +34 -0
- package/template/apps/asas-tanstack/src/screen-list.tsx +139 -0
- package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
- package/template/apps/asas-tanstack/src/screens.tsx +868 -0
- package/template/apps/asas-tanstack/src/server.ts +16 -0
- package/template/apps/asas-tanstack/src/slide.ts +22 -0
- package/template/apps/asas-tanstack/src/start.ts +57 -0
- package/template/apps/asas-tanstack/src/styles.css +159 -0
- package/template/apps/asas-tanstack/src/theme.ts +5 -0
- package/template/apps/asas-tanstack/tsconfig.json +19 -0
- package/template/apps/asas-tanstack/vite.config.ts +28 -0
- package/template/package.json +16 -0
- package/template/templates/asas/.env.example +16 -0
- package/template/templates/asas/components.json +22 -0
- package/template/templates/asas/mithaq.hints.json +25 -0
- package/template/templates/asas/package.json +38 -0
- package/template/templates/asas/src/asas.ts +93 -0
- package/template/templates/asas/src/audit-log/audit-log.ts +187 -0
- package/template/templates/asas/src/audit-log/sink.ts +89 -0
- package/template/templates/asas/src/audit-log/ui/activity.tsx +243 -0
- package/template/templates/asas/src/courses/courses.ts +612 -0
- package/template/templates/asas/src/courses/ui/archive-button.tsx +25 -0
- package/template/templates/asas/src/courses/ui/fee-cell.tsx +19 -0
- package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +11 -0
- package/template/templates/asas/src/courses/ui/no-courses.tsx +13 -0
- package/template/templates/asas/src/courses/ui/on-offer.ts +6 -0
- package/template/templates/asas/src/courses/ui/status-cell.tsx +31 -0
- package/template/templates/asas/src/enrollments/enrollments.ts +137 -0
- package/template/templates/asas/src/enrollments/ui/session-cell.tsx +65 -0
- package/template/templates/asas/src/kayan/auth/user/account.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/account.ts +31 -0
- package/template/templates/asas/src/kayan/auth/user/index.ts +18 -0
- package/template/templates/asas/src/kayan/auth/user/invitation.access.ts +4 -0
- package/template/templates/asas/src/kayan/auth/user/invitation.ts +27 -0
- package/template/templates/asas/src/kayan/auth/user/member.access.ts +10 -0
- package/template/templates/asas/src/kayan/auth/user/member.screens.ts +37 -0
- package/template/templates/asas/src/kayan/auth/user/member.ts +25 -0
- package/template/templates/asas/src/kayan/auth/user/organization.access.ts +20 -0
- package/template/templates/asas/src/kayan/auth/user/organization.screens.ts +37 -0
- package/template/templates/asas/src/kayan/auth/user/organization.ts +24 -0
- package/template/templates/asas/src/kayan/auth/user/organizationRole.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/organizationRole.ts +25 -0
- package/template/templates/asas/src/kayan/auth/user/session.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/session.ts +28 -0
- package/template/templates/asas/src/kayan/auth/user/user.access.ts +29 -0
- package/template/templates/asas/src/kayan/auth/user/user.screens.ts +45 -0
- package/template/templates/asas/src/kayan/auth/user/user.ts +31 -0
- package/template/templates/asas/src/kayan/auth/user/verification.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/verification.ts +23 -0
- package/template/templates/asas/src/local-users/local-users.ts +143 -0
- package/template/templates/asas/src/media/media.ts +137 -0
- package/template/templates/asas/src/media/ui/preview.tsx +143 -0
- package/template/templates/asas/src/member-roles/member-roles.ts +207 -0
- package/template/templates/asas/src/messages/account.json +106 -0
- package/template/templates/asas/src/messages/auditLog.json +186 -0
- package/template/templates/asas/src/messages/courses.json +270 -0
- package/template/templates/asas/src/messages/enrollments.json +42 -0
- package/template/templates/asas/src/messages/invitation.json +66 -0
- package/template/templates/asas/src/messages/localUsers.json +42 -0
- package/template/templates/asas/src/messages/media.json +66 -0
- package/template/templates/asas/src/messages/member.json +42 -0
- package/template/templates/asas/src/messages/memberRoles.json +42 -0
- package/template/templates/asas/src/messages/mithaq.json +170 -0
- package/template/templates/asas/src/messages/organization.json +50 -0
- package/template/templates/asas/src/messages/organizationRole.json +50 -0
- package/template/templates/asas/src/messages/roles.json +58 -0
- package/template/templates/asas/src/messages/session.json +82 -0
- package/template/templates/asas/src/messages/sessions.json +106 -0
- package/template/templates/asas/src/messages/settings.json +66 -0
- package/template/templates/asas/src/messages/tags.json +38 -0
- package/template/templates/asas/src/messages/user.json +90 -0
- package/template/templates/asas/src/messages/verification.json +50 -0
- package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/migration.sql +177 -0
- package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/snapshot.json +2169 -0
- package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/snapshot.json +2182 -0
- package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/migration.sql +20 -0
- package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/snapshot.json +2409 -0
- package/template/templates/asas/src/migrations/20260922082945_military_ironclad/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20260922082945_military_ironclad/snapshot.json +2627 -0
- package/template/templates/asas/src/migrations/20260922112630_bitter_chat/migration.sql +5 -0
- package/template/templates/asas/src/migrations/20260922112630_bitter_chat/snapshot.json +2691 -0
- package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/migration.sql +8 -0
- package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/snapshot.json +2795 -0
- package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/migration.sql +3 -0
- package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/snapshot.json +2834 -0
- package/template/templates/asas/src/migrations/20260927212525_bent_prowler/migration.sql +9 -0
- package/template/templates/asas/src/migrations/20260927212525_bent_prowler/snapshot.json +2870 -0
- package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/snapshot.json +2881 -0
- package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/snapshot.json +3073 -0
- package/template/templates/asas/src/migrations/20260929024407_grey_morlun/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20260929024407_grey_morlun/snapshot.json +3098 -0
- package/template/templates/asas/src/migrations/20260929130530_bored_jackal/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20260929130530_bored_jackal/snapshot.json +3124 -0
- package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/migration.sql +32 -0
- package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/snapshot.json +3324 -0
- package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/migration.sql +2 -0
- package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/snapshot.json +3350 -0
- package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/migration.sql +8 -0
- package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/migration.sql +18 -0
- package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/migration.sql +18 -0
- package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/migration.sql +11 -0
- package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/snapshot.json +3667 -0
- package/template/templates/asas/src/migrations/20261012000752_organization_ownership/migration.sql +48 -0
- package/template/templates/asas/src/migrations/20261012000752_organization_ownership/snapshot.json +3891 -0
- package/template/templates/asas/src/migrations/drizzle.config.ts +11 -0
- package/template/templates/asas/src/migrations/schema.ts +29 -0
- package/template/templates/asas/src/mithaq.ts +406 -0
- package/template/templates/asas/src/pages.ts +74 -0
- package/template/templates/asas/src/platform-http.ts +96 -0
- package/template/templates/asas/src/rich-text-nodes.ts +11 -0
- package/template/templates/asas/src/roles/grants.ts +47 -0
- package/template/templates/asas/src/roles/roles.ts +184 -0
- package/template/templates/asas/src/roles/ui/role-form.tsx +226 -0
- package/template/templates/asas/src/roles/ui/roles.tsx +166 -0
- package/template/templates/asas/src/roles.ts +16 -0
- package/template/templates/asas/src/seed.ts +325 -0
- package/template/templates/asas/src/sessions/sessions.ts +155 -0
- package/template/templates/asas/src/settings/settings.ts +69 -0
- package/template/templates/asas/src/statements.ts +11 -0
- package/template/templates/asas/src/steps/expected-updated-at.ts +41 -0
- package/template/templates/asas/src/storage/defaults.ts +156 -0
- package/template/templates/asas/src/storage/index.ts +95 -0
- package/template/templates/asas/src/storage/local.ts +143 -0
- package/template/templates/asas/src/storage/r2.ts +5 -0
- package/template/templates/asas/src/storage/s3.ts +91 -0
- package/template/templates/asas/src/storage/uploadthing.ts +59 -0
- package/template/templates/asas/src/tags/tags.ts +80 -0
- package/template/templates/asas/src/tenant-access.ts +221 -0
- package/template/templates/asas/src/tenant-roles.ts +262 -0
- package/template/templates/asas/src/ui/app-shell.tsx +260 -0
- package/template/templates/asas/src/ui/kpi-card.tsx +25 -0
- package/template/templates/asas/src/ui/record-page.tsx +50 -0
- package/template/templates/asas/src/ui/rich-text-nodes.tsx +15 -0
- package/template/templates/asas/src/ui/screen-nav.tsx +28 -0
- package/template/templates/asas/src/ui/session-labels.tsx +36 -0
- package/template/templates/asas/src/ui/table-page.tsx +9 -0
- package/template/templates/asas/src/ui/top-shell.tsx +60 -0
- package/template/templates/asas/src/ui-dashboard/dashboard.tsx +294 -0
- package/template/templates/asas/src/ui-dashboard/members.tsx +630 -0
- package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
- package/template/templates/asas/src/ui-dashboard/versions.tsx +432 -0
- package/template/templates/asas/src/ui-platform/home.tsx +63 -0
- package/template/templates/asas/src/ui-platform/organization-actions.tsx +114 -0
- package/template/templates/asas/src/ui-platform/record-page.tsx +30 -0
- package/template/templates/asas/src/ui-platform/role-cells.tsx +52 -0
- package/template/templates/asas/src/ui-platform/user-actions.tsx +251 -0
- package/template/templates/asas/src/ui-portal/home.tsx +77 -0
- package/template/templates/asas/src/ui-portal/page.tsx +11 -0
- package/template/templates/auth/package.json +14 -0
- package/template/templates/auth/src/index.ts +690 -0
- package/template/templates/auth/src/platform-roles.ts +87 -0
- package/README.md +0 -3
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { useState, useSyncExternalStore, type ChangeEvent, type ReactElement } from 'react'
|
|
2
|
+
import { Calendar as CalendarIcon } from 'lucide-react'
|
|
3
|
+
import type { AccessReport, ClientField, ClientKayan } from 'mithaq/admin'
|
|
4
|
+
import { Checkbox } from '@/components/ui/checkbox'
|
|
5
|
+
import { Switch } from '@/components/ui/switch'
|
|
6
|
+
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
|
7
|
+
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
|
8
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
9
|
+
import { Slider } from '@/components/ui/slider'
|
|
10
|
+
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'
|
|
11
|
+
import { REGEXP_ONLY_DIGITS } from 'input-otp'
|
|
12
|
+
import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem, ComboboxList } from '@/components/ui/combobox'
|
|
13
|
+
import { Input } from '@/components/ui/input'
|
|
14
|
+
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from '@/components/ui/input-group'
|
|
15
|
+
import { Textarea } from '@/components/ui/textarea'
|
|
16
|
+
import { Toggle } from '@/components/ui/toggle'
|
|
17
|
+
import { Button } from '@/components/ui/button'
|
|
18
|
+
import { ButtonGroup } from '@/components/ui/button-group'
|
|
19
|
+
import { Calendar } from '@/components/ui/calendar'
|
|
20
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
|
21
|
+
import { useDirection } from '@/components/ui/direction'
|
|
22
|
+
import { ar, enUS } from 'react-day-picker/locale'
|
|
23
|
+
import { RelationshipControl } from './relationship-control.tsx'
|
|
24
|
+
import { RichTextControl } from './rich-text.tsx'
|
|
25
|
+
import { UploadControl } from './upload.tsx'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* How a string of copy is resolved. A project passes one function over its own committed copy files, whose keys are the flat `<scope>.<key>` space `mithaq generate messages` writes. A missing key degrades to `fallback`.
|
|
29
|
+
*/
|
|
30
|
+
export type Translate = (key: string, fallback?: string) => string | undefined
|
|
31
|
+
|
|
32
|
+
/** What every screen is handed to resolve copy with. */
|
|
33
|
+
export type Copy = {
|
|
34
|
+
t: Translate
|
|
35
|
+
locale: string
|
|
36
|
+
/** Every locale the host reads in, which a localized field draws one box for. */
|
|
37
|
+
locales?: readonly string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A field's label: the record's string for the locale, else the project's copy under `<kayan>.<field>`, else the field name. A label written as a function crosses the projection as nothing, so the key is rebuilt from the two names.
|
|
42
|
+
*/
|
|
43
|
+
export const labelFor = ({ kayan, field, copy: { t, locale } }: { kayan: ClientKayan; field: ClientField; copy: Copy }): string =>
|
|
44
|
+
(typeof field.label === 'object' ? field.label[locale] : undefined)
|
|
45
|
+
?? t(`${kayan.name}.${field.name}`, field.name)
|
|
46
|
+
?? field.name
|
|
47
|
+
|
|
48
|
+
/** The same chain for a field's description, which is optional everywhere. */
|
|
49
|
+
export const descriptionFor = ({ kayan, field, copy: { t, locale } }: { kayan: ClientKayan; field: ClientField; copy: Copy }): string | undefined =>
|
|
50
|
+
(typeof field.description === 'object' ? field.description[locale] : undefined)
|
|
51
|
+
?? t(`${kayan.name}.${field.name}.description`)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The same chain for a field's placeholder, the hint a text-like control shows while empty. A kind whose control has nowhere to draw a hint ignores it.
|
|
55
|
+
*/
|
|
56
|
+
export const placeholderFor = ({ kayan, field, copy: { t, locale } }: { kayan: ClientKayan; field: ClientField; copy: Copy }): string | undefined =>
|
|
57
|
+
(typeof field.placeholder === 'object' ? field.placeholder[locale] : undefined)
|
|
58
|
+
?? t(`${kayan.name}.${field.name}.placeholder`)
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What a kayan is for, in one sentence, resolved like a field's description. `undefined` when the kayan declares none, so a heading is never repeated as its own subtitle.
|
|
62
|
+
*/
|
|
63
|
+
export const descriptionOf = (kayan: ClientKayan, { t, locale }: Copy): string | undefined =>
|
|
64
|
+
(typeof kayan.description === 'object' ? kayan.description[locale] : undefined)
|
|
65
|
+
?? t(`${kayan.name}.description`)
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The words on a page's button for a kayan's operation: the page's label for the locale, else the project's copy under `<kayan>.operations.<name>`, else the operation's name.
|
|
69
|
+
*/
|
|
70
|
+
export const operationLabelFor = ({
|
|
71
|
+
kayan,
|
|
72
|
+
operation,
|
|
73
|
+
copy: { t, locale },
|
|
74
|
+
}: {
|
|
75
|
+
kayan: ClientKayan
|
|
76
|
+
operation: { name: string; label?: Readonly<Record<string, string>> }
|
|
77
|
+
copy: Copy
|
|
78
|
+
}): string =>
|
|
79
|
+
(typeof operation.label === 'object' ? operation.label[locale] : undefined)
|
|
80
|
+
?? t(`${kayan.name}.operations.${operation.name}`, operation.name)
|
|
81
|
+
?? operation.name
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The words on one box of an operation's dialog, under `<kayan>.operations.<name>.<field>`, so an operation's field and a kayan's field can share a name and never a key.
|
|
85
|
+
*/
|
|
86
|
+
export const operationFieldLabelFor = ({
|
|
87
|
+
kayan,
|
|
88
|
+
operation,
|
|
89
|
+
field,
|
|
90
|
+
copy: { t, locale },
|
|
91
|
+
}: {
|
|
92
|
+
kayan: ClientKayan
|
|
93
|
+
operation: { name: string }
|
|
94
|
+
field: ClientField
|
|
95
|
+
copy: Copy
|
|
96
|
+
}): string =>
|
|
97
|
+
(typeof field.label === 'object' ? field.label[locale] : undefined)
|
|
98
|
+
?? t(`${kayan.name}.operations.${operation.name}.${field.name}`, field.name)
|
|
99
|
+
?? field.name
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* What a kayan is called: the `singular` or `plural` key of its copy scope, else its name. Takes the name alone, so a shell that hands its browser only a kayan's name can still name it.
|
|
103
|
+
*/
|
|
104
|
+
export const titleOf = ({ kayan, copy: { t }, many = false }: { kayan: Pick<ClientKayan, 'name'>; copy: Copy; many?: boolean }): string =>
|
|
105
|
+
t(`${kayan.name}.${many ? 'plural' : 'singular'}`, kayan.name) ?? kayan.name
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* What a create page is titled: the kayan's `new` key, else "Create {kind}" with its singular.
|
|
109
|
+
*/
|
|
110
|
+
export const newTitleOf = ({ kayan, copy }: { kayan: Pick<ClientKayan, 'name'>; copy: Copy }): string =>
|
|
111
|
+
copy.t(`${kayan.name}.new`) ?? (copy.t('admin.createTitle', 'Create {kind}') ?? 'Create {kind}').replace('{kind}', () => titleOf({ kayan, copy }))
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Several values as one phrase in the viewer's own language, by `Intl.ListFormat`. The type is `unit`, so English joins with commas alone and Arabic with its own conjunction.
|
|
115
|
+
*/
|
|
116
|
+
export const listed = (values: readonly string[], locale: string) => new Intl.ListFormat(locale, { type: 'unit' }).format(values)
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* What a cell draws for a value nobody set: a dash hidden from screen readers, since a cell read as nothing says the same.
|
|
120
|
+
*/
|
|
121
|
+
export const noValue = <span aria-hidden="true">—</span>
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A sentence that counts, in the plural form the locale takes for that count: `key` plus `_one`, `_two`, `_few`, `_many` or `_other` as `Intl.PluralRules` names it, falling back to `_other`. `{count}` is the number in the locale's digits.
|
|
125
|
+
*/
|
|
126
|
+
export const counted = ({ t, locale, key, count, fallback }: { t: Translate; locale: string; key: string; count: number; fallback: string }): string =>
|
|
127
|
+
(t(`${key}_${new Intl.PluralRules(locale || undefined).select(count)}`) ?? t(`${key}_other`, fallback) ?? fallback).replace('{count}', () => count.toLocaleString(locale || undefined))
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A moment, or a calendar day where `day` is set, in the one format every cell draws it in for this locale; `undefined` for anything that is not a date. A day is drawn in UTC, so a `yyyy-mm-dd` string never reads as the day before west of Greenwich. The time of day is on the 24-hour clock.
|
|
131
|
+
*/
|
|
132
|
+
export const formatDate = ({ value, locale, day = false }: { value: unknown; locale: string; day?: boolean }): string | undefined => {
|
|
133
|
+
const date = value instanceof Date ? value : typeof value === 'string' ? new Date(value) : undefined
|
|
134
|
+
if (!date || Number.isNaN(date.getTime())) return undefined
|
|
135
|
+
return new Intl.DateTimeFormat(locale || undefined, day ? { dateStyle: 'long', timeZone: 'UTC' } : { dateStyle: 'long', timeStyle: 'short', hourCycle: 'h23' }).format(date)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The key a refusal carries, as `data: { key, params }`, which a client resolves against its own copy. Anything that is not a refusal carries no key.
|
|
140
|
+
*/
|
|
141
|
+
export const refusalKeyOf = (error: unknown): string | undefined => {
|
|
142
|
+
const key = (error as { data?: { key?: unknown } } | null | undefined)?.data?.key
|
|
143
|
+
return typeof key === 'string' ? key : undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Which locales of each localized field a refusal names, off the issue paths: `title.ar` names the Arabic box of `title`. A path of one segment names no locale.
|
|
148
|
+
*/
|
|
149
|
+
export const localesOf = (error: unknown): Readonly<Record<string, readonly string[]>> => {
|
|
150
|
+
const issues = (error as { data?: { params?: { issues?: unknown } } } | null | undefined)?.data?.params?.issues
|
|
151
|
+
const byField: Record<string, string[]> = Object.create(null)
|
|
152
|
+
if (!Array.isArray(issues)) return byField
|
|
153
|
+
for (const issue of issues as readonly { path?: unknown }[]) {
|
|
154
|
+
const [name, locale] = typeof issue?.path === 'string' ? issue.path.split('.') : []
|
|
155
|
+
if (name && locale) (byField[name] ??= []).push(locale)
|
|
156
|
+
}
|
|
157
|
+
return byField
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* One page, as much of the managed reader's answer as a control reads. Every list `Page` is assignable to it.
|
|
162
|
+
*/
|
|
163
|
+
export type FoundPage = {
|
|
164
|
+
docs: readonly Record<string, unknown>[]
|
|
165
|
+
hasNextPage: boolean
|
|
166
|
+
nextPage: number | null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The typed client, as much of it as a control reads: one kayan's `find`, looked up by the kayan's name. An app hands an adapter over its own client, because a control builds its argument as a record and no typing survives that boundary.
|
|
171
|
+
*/
|
|
172
|
+
export type FindApi = Readonly<
|
|
173
|
+
Record<
|
|
174
|
+
string,
|
|
175
|
+
| {
|
|
176
|
+
find: (input: Record<string, unknown>) => Promise<FoundPage>
|
|
177
|
+
}
|
|
178
|
+
| undefined
|
|
179
|
+
>
|
|
180
|
+
>
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* What every control is handed. A control is controlled over one value and holds no form state; the shell draws its label, description and refusal. It gets no `path`, `validate` or `locale`: the server validates, a field's name is its path, and `t` closes over the locale.
|
|
184
|
+
*/
|
|
185
|
+
export type ControlProps = {
|
|
186
|
+
value: unknown
|
|
187
|
+
/** Current values of the form that owns this control. */
|
|
188
|
+
values: Readonly<Record<string, unknown>>
|
|
189
|
+
/** Write another field through the same form, subject to its write access. */
|
|
190
|
+
setValue: (name: string, value: unknown) => void
|
|
191
|
+
/**
|
|
192
|
+
* What the control reports a change through. A value cleared back to nothing is `null`, not left out, which lets a form empty an optional field.
|
|
193
|
+
*/
|
|
194
|
+
onChange: (value: unknown) => void
|
|
195
|
+
field: ClientField
|
|
196
|
+
kayan: ClientKayan
|
|
197
|
+
/** Every kayan of the build, so a relationship control can find its target by `field.relationTo`. */
|
|
198
|
+
client?: readonly ClientKayan[]
|
|
199
|
+
readOnly: boolean
|
|
200
|
+
/** A refusal for this field, already resolved through the project's copy. */
|
|
201
|
+
error?: string
|
|
202
|
+
/**
|
|
203
|
+
* The hint an empty control shows, already resolved through the project's copy. A control drawn for something other than a row of this kayan, such as the filter panel's value box, gets none.
|
|
204
|
+
*/
|
|
205
|
+
placeholder?: string
|
|
206
|
+
t: Translate
|
|
207
|
+
/** The `id` the label points at, unique within the form. */
|
|
208
|
+
id: string
|
|
209
|
+
/**
|
|
210
|
+
* The id of the `<form>` this control saves through, named as a native box's `form` attribute: a leaf outside the form element saves on Enter only through it. A control drawn for no row gets none.
|
|
211
|
+
*/
|
|
212
|
+
form?: string
|
|
213
|
+
/** The typed client, for a control that reads a second kayan. */
|
|
214
|
+
api?: FindApi
|
|
215
|
+
/**
|
|
216
|
+
* The ids of the field's description and refusal, for the control's own `aria-describedby`, which belongs on the focusable element the control renders.
|
|
217
|
+
*/
|
|
218
|
+
describedBy?: string
|
|
219
|
+
/**
|
|
220
|
+
* The viewer's locale, for the words a control reads off the projection rather than through `t`: an option's label and a relationship's title.
|
|
221
|
+
*/
|
|
222
|
+
locale?: string
|
|
223
|
+
/**
|
|
224
|
+
* Every locale the host reads in, for a control that asks a localized field of its own, one box per locale.
|
|
225
|
+
*/
|
|
226
|
+
locales?: readonly string[]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* What a control reports a change through: a read-only control reports none, which still holds where a control is not a native input.
|
|
233
|
+
*/
|
|
234
|
+
export const changeOf = ({ onChange, readOnly }: ControlProps) => (value: unknown) => {
|
|
235
|
+
if (!readOnly) onChange(value)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const sliderKeys = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown'])
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* A value as an input's text; a missing value is empty. A `Date` is its ISO string, because the fetch plug does not read `String(date)` back as a date. An invalid `Date`, which has no ISO string, is drawn as `String` does.
|
|
242
|
+
*/
|
|
243
|
+
export const asText = (value: unknown) =>
|
|
244
|
+
value === null || value === undefined ? '' : value instanceof Date && !Number.isNaN(value.getTime()) ? value.toISOString() : String(value)
|
|
245
|
+
|
|
246
|
+
const cleared = (text: string) => (text === '' ? null : text)
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* A moment as the `yyyy-mm-ddThh:mm` of the reader's own wall clock. It is shifted by the reader's offset before it is sliced, because `toISOString` alone would draw a London evening as the next morning in Riyadh.
|
|
250
|
+
*/
|
|
251
|
+
export const asMoment = (value: unknown) => {
|
|
252
|
+
const date = value instanceof Date ? value : typeof value === 'string' || typeof value === 'number' ? new Date(value) : undefined
|
|
253
|
+
if (!date || Number.isNaN(date.getTime())) return ''
|
|
254
|
+
return new Date(date.getTime() - date.getTimezoneOffset() * 60_000).toISOString().slice(0, 16)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const typed = (
|
|
258
|
+
type: string,
|
|
259
|
+
format: (value: unknown) => string = asText,
|
|
260
|
+
parse: (text: string) => unknown = cleared,
|
|
261
|
+
) =>
|
|
262
|
+
function TypedControl(props: ControlProps): ReactElement {
|
|
263
|
+
const { describedBy, error, field, form, id, locale, placeholder, readOnly, value } = props
|
|
264
|
+
const change = changeOf(props)
|
|
265
|
+
const input = {
|
|
266
|
+
id,
|
|
267
|
+
form,
|
|
268
|
+
name: field.name,
|
|
269
|
+
type,
|
|
270
|
+
className: 'max-sm:h-11',
|
|
271
|
+
dir: type === 'text' && field.kind !== 'decimal' ? 'auto' : 'ltr',
|
|
272
|
+
step: field.kind === 'integer' ? '1' : undefined,
|
|
273
|
+
inputMode: field.kind === 'decimal' ? ('decimal' as const) : undefined,
|
|
274
|
+
value: format(value),
|
|
275
|
+
onChange: (event: ChangeEvent<HTMLInputElement>) => change(parse(event.target.value)),
|
|
276
|
+
disabled: readOnly,
|
|
277
|
+
readOnly,
|
|
278
|
+
placeholder,
|
|
279
|
+
'aria-describedby': describedBy,
|
|
280
|
+
'aria-invalid': error ? true : undefined,
|
|
281
|
+
'aria-required': ariaRequired(field),
|
|
282
|
+
}
|
|
283
|
+
const prefix = locale ? field.prefix?.[locale] : undefined
|
|
284
|
+
const suffix = locale ? field.suffix?.[locale] : undefined
|
|
285
|
+
if (prefix === undefined && suffix === undefined) return <Input {...input} />
|
|
286
|
+
return (
|
|
287
|
+
<InputGroup className="max-sm:h-11">
|
|
288
|
+
{prefix !== undefined ? <InputGroupAddon data-affix="prefix" className={`${affix} rounded-s-[inherit] border-e`}><InputGroupText className="text-foreground">{prefix}</InputGroupText></InputGroupAddon> : null}
|
|
289
|
+
<InputGroupInput {...input} />
|
|
290
|
+
{suffix !== undefined ? <InputGroupAddon data-affix="suffix" align="inline-end" className={`${affix} rounded-e-[inherit] border-s`}><InputGroupText className="text-foreground">{suffix}</InputGroupText></InputGroupAddon> : null}
|
|
291
|
+
</InputGroup>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The input group classes that make a combobox's box and its addon buttons 44 pixels on a phone, because the shadcn primitive sizes them at 36 and 24.
|
|
297
|
+
*/
|
|
298
|
+
export const comboboxPhone = 'max-sm:h-11 max-sm:[&_[data-slot=input-group-addon]]:py-0 max-sm:[&_[data-slot=input-group-button]]:size-11 max-sm:[&_[data-slot=combobox-clear]]:size-11 max-sm:[&_[data-slot=input-group-control]]:h-11'
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The one size every checkbox of a screen is drawn at, so a box in a list, a form and the column picker match.
|
|
302
|
+
*/
|
|
303
|
+
export const checkboxSize = 'size-4'
|
|
304
|
+
|
|
305
|
+
const affix = 'bg-muted border-input self-stretch px-3'
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* A `uuid` is always the store's to make, so this reports no change whatever `readOnly` says.
|
|
309
|
+
*/
|
|
310
|
+
function ReadOnlyControl({ describedBy, field, id, value }: ControlProps): ReactElement {
|
|
311
|
+
return (
|
|
312
|
+
<Input
|
|
313
|
+
id={id}
|
|
314
|
+
name={field.name}
|
|
315
|
+
type="text"
|
|
316
|
+
value={asText(value)}
|
|
317
|
+
readOnly
|
|
318
|
+
disabled
|
|
319
|
+
aria-describedby={describedBy}
|
|
320
|
+
/>
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const dayPickerLocales: Record<string, typeof enUS> = { ar, en: enUS }
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* `aria-required` where the field's rules hold `required`, for whatever element of a control takes focus. The browser's `required` is not used, because its bubble is replaced by the form's own check.
|
|
328
|
+
*/
|
|
329
|
+
export const ariaRequired = (field: Pick<ControlProps['field'], 'rules'>): true | undefined => (field.rules.required === true ? true : undefined)
|
|
330
|
+
|
|
331
|
+
function SelectControl(props: ControlProps): ReactElement {
|
|
332
|
+
const options = props.field.options ?? []
|
|
333
|
+
return <Select value={typeof props.value === 'string' ? props.value : null} onValueChange={changeOf(props)} disabled={props.readOnly}>
|
|
334
|
+
<SelectTrigger id={props.id} className="w-full max-sm:data-[size=default]:h-11" aria-describedby={props.describedBy} aria-required={ariaRequired(props.field)}><SelectValue>{options.find((one) => one.value === props.value)?.label[props.locale ?? ''] ?? props.placeholder ?? ''}</SelectValue></SelectTrigger>
|
|
335
|
+
<SelectContent>{options.map((one) => <SelectItem key={one.value} value={one.value}>{one.label[props.locale ?? ''] ?? one.value}</SelectItem>)}</SelectContent>
|
|
336
|
+
</Select>
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const dayOf = (value: unknown): Date | undefined => {
|
|
340
|
+
const [year, month, day] = typeof value === 'string' ? value.split('-').map(Number) : []
|
|
341
|
+
return year && month && day ? new Date(year, month - 1, day) : undefined
|
|
342
|
+
}
|
|
343
|
+
const isoDay = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
344
|
+
|
|
345
|
+
function DatePicker(props: ControlProps): ReactElement {
|
|
346
|
+
const { describedBy, error, field, form, id, locale = '', placeholder, readOnly, t, value } = props
|
|
347
|
+
const change = changeOf(props)
|
|
348
|
+
const dir = useDirection()
|
|
349
|
+
const [open, setOpen] = useState(false)
|
|
350
|
+
const moment = field.kind === 'datetime'
|
|
351
|
+
const clock = asMoment(value)
|
|
352
|
+
const time = clock.slice(11)
|
|
353
|
+
// A moment is held only when both its day and its time are set: a day picked before any time is typed stays here and writes nothing.
|
|
354
|
+
const [picked, setPicked] = useState<Date | undefined>()
|
|
355
|
+
// False only while hydrating, so the server and the first client render agree on a moment's day (UTC); the viewer's own day follows once mounted.
|
|
356
|
+
const mounted = useSyncExternalStore(() => () => {}, () => true, () => false)
|
|
357
|
+
const held = moment ? (clock ? new Date(clock) : picked) : dayOf(value)
|
|
358
|
+
const at = (day: Date, text: string) => {
|
|
359
|
+
const [hours = 0, minutes = 0] = text.split(':').map(Number)
|
|
360
|
+
return new Date(day.getFullYear(), day.getMonth(), day.getDate(), hours, minutes)
|
|
361
|
+
}
|
|
362
|
+
const label = labelFor({ kayan: props.kayan, field, copy: { t, locale } })
|
|
363
|
+
const shown = held
|
|
364
|
+
? (moment && clock && !mounted ? formatDate({ value, locale, day: true }) : undefined) ?? new Intl.DateTimeFormat(locale || undefined, { dateStyle: 'long' }).format(held)
|
|
365
|
+
: placeholder ?? t('admin.pickDate', 'Pick a date')
|
|
366
|
+
return (
|
|
367
|
+
// Wraps, so a phone puts the time under the day.
|
|
368
|
+
<div className="flex flex-wrap gap-2">
|
|
369
|
+
<span id={`${id}-name`} className="sr-only">{label}</span>
|
|
370
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
371
|
+
<PopoverTrigger
|
|
372
|
+
id={id}
|
|
373
|
+
disabled={readOnly}
|
|
374
|
+
// The label's `htmlFor` leaves out the day the button shows, so the name is built from both texts on the page.
|
|
375
|
+
aria-labelledby={`${id}-name ${id}-day`}
|
|
376
|
+
aria-describedby={describedBy}
|
|
377
|
+
aria-invalid={error ? true : undefined}
|
|
378
|
+
aria-required={ariaRequired(field)}
|
|
379
|
+
render={<Button type="button" variant="outline" className="min-w-32 flex-1 justify-start font-normal max-sm:h-11" />}
|
|
380
|
+
>
|
|
381
|
+
<CalendarIcon aria-hidden="true" />
|
|
382
|
+
<span id={`${id}-day`} className={held ? 'truncate' : 'text-muted-foreground truncate'}>{shown}</span>
|
|
383
|
+
</PopoverTrigger>
|
|
384
|
+
<PopoverContent align="start" className="w-auto p-0">
|
|
385
|
+
<Calendar
|
|
386
|
+
mode="single"
|
|
387
|
+
autoFocus
|
|
388
|
+
dir={dir}
|
|
389
|
+
locale={dayPickerLocales[locale]}
|
|
390
|
+
selected={held}
|
|
391
|
+
defaultMonth={held}
|
|
392
|
+
onSelect={(day: Date | undefined) => {
|
|
393
|
+
setPicked(time ? undefined : day)
|
|
394
|
+
if (!moment) change(day ? isoDay(day) : null)
|
|
395
|
+
else if (!day) clock && change(null)
|
|
396
|
+
else if (time) change(at(day, time))
|
|
397
|
+
setOpen(false)
|
|
398
|
+
}}
|
|
399
|
+
/>
|
|
400
|
+
</PopoverContent>
|
|
401
|
+
</Popover>
|
|
402
|
+
{moment ? (
|
|
403
|
+
<Input
|
|
404
|
+
type="time"
|
|
405
|
+
form={form}
|
|
406
|
+
dir="ltr"
|
|
407
|
+
className="w-28 shrink-0 max-sm:h-11"
|
|
408
|
+
value={time}
|
|
409
|
+
disabled={readOnly || !held}
|
|
410
|
+
readOnly={readOnly}
|
|
411
|
+
aria-label={`${label} ${t('admin.time', 'Time')}`}
|
|
412
|
+
aria-describedby={describedBy}
|
|
413
|
+
aria-invalid={error ? true : undefined}
|
|
414
|
+
aria-required={ariaRequired(field)}
|
|
415
|
+
onChange={(event) => {
|
|
416
|
+
const text = event.target.value
|
|
417
|
+
// A time no clock reads is reported as nothing to write, not as a value that would clear the column.
|
|
418
|
+
if (text !== '' && !/^\d{2}:\d{2}/.test(text)) return change(undefined)
|
|
419
|
+
// Clearing the time clears the value and never writes midnight; the day stays on the button.
|
|
420
|
+
if (text === '' || !held) return (setPicked(held), change(null))
|
|
421
|
+
setPicked(undefined)
|
|
422
|
+
change(at(held, text))
|
|
423
|
+
}}
|
|
424
|
+
/>
|
|
425
|
+
) : null}
|
|
426
|
+
</div>
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** One control per shipped kind, keyed by the kind's own name. */
|
|
431
|
+
export const controls: Record<string, (props: ControlProps) => ReactElement> = {
|
|
432
|
+
combobox: function ComboboxControl(props: ControlProps): ReactElement {
|
|
433
|
+
const options = props.field.options ?? []
|
|
434
|
+
const label = (value: string) => options.find((one) => one.value === value)?.label[props.locale ?? ''] ?? value
|
|
435
|
+
return <Combobox items={options.map((one) => one.value)} value={typeof props.value === 'string' ? props.value : null} onValueChange={changeOf(props)} itemToStringLabel={label} disabled={props.readOnly}>
|
|
436
|
+
<ComboboxInput id={props.id} className={comboboxPhone} placeholder={props.placeholder} clearLabel={props.t('admin.clear', 'Clear')} triggerLabel={props.t('admin.open', 'Open')} aria-describedby={props.describedBy} aria-required={ariaRequired(props.field)} />
|
|
437
|
+
<ComboboxContent><ComboboxList>{options.map((one) => <ComboboxItem key={one.value} value={one.value}>{label(one.value)}</ComboboxItem>)}</ComboboxList></ComboboxContent>
|
|
438
|
+
</Combobox>
|
|
439
|
+
},
|
|
440
|
+
otp: function OtpControl(props: ControlProps): ReactElement {
|
|
441
|
+
const length = typeof props.field.rules.maxLength === 'number' ? props.field.rules.maxLength : 6
|
|
442
|
+
return <InputOTP id={props.id} value={asText(props.value)} onChange={changeOf(props)} maxLength={length} pattern={REGEXP_ONLY_DIGITS} disabled={props.readOnly} aria-describedby={props.describedBy}>
|
|
443
|
+
<InputOTPGroup>{Array.from({ length }, (_, index) => <InputOTPSlot key={index} index={index} />)}</InputOTPGroup>
|
|
444
|
+
</InputOTP>
|
|
445
|
+
},
|
|
446
|
+
slider: function SliderControl(props: ControlProps): ReactElement {
|
|
447
|
+
const min = typeof props.field.rules.min === 'number' ? props.field.rules.min : 0
|
|
448
|
+
const max = typeof props.field.rules.max === 'number' ? props.field.rules.max : 100
|
|
449
|
+
// Unset is drawn with no thumb or fill, not as `min`. The slider reports no change for a value it already holds,
|
|
450
|
+
// so the first press or key reports `min` itself, before the slider moves on.
|
|
451
|
+
const held = typeof props.value === 'number'
|
|
452
|
+
const change = changeOf(props)
|
|
453
|
+
const first = held ? undefined : () => change(min)
|
|
454
|
+
return <Slider
|
|
455
|
+
value={held ? props.value as number : min}
|
|
456
|
+
data-empty={held ? undefined : ''}
|
|
457
|
+
className={held ? undefined : '**:data-[slot=slider-range]:opacity-0 **:data-[slot=slider-thumb]:not-has-focus-visible:opacity-0'}
|
|
458
|
+
getAriaValueText={held ? undefined : () => props.t('admin.unset', 'unset') ?? 'unset'}
|
|
459
|
+
onPointerDownCapture={first}
|
|
460
|
+
onKeyDownCapture={first && ((event) => { if (sliderKeys.has(event.key)) first() })}
|
|
461
|
+
min={min}
|
|
462
|
+
max={max}
|
|
463
|
+
onValueChange={change}
|
|
464
|
+
disabled={props.readOnly}
|
|
465
|
+
aria-label={labelFor({ kayan: props.kayan, field: props.field, copy: { t: props.t, locale: props.locale ?? '' } })}
|
|
466
|
+
aria-describedby={props.describedBy}
|
|
467
|
+
/>
|
|
468
|
+
},
|
|
469
|
+
// `nativeSelect` draws the same shadcn select, so two boxes over one kind of choice never differ in their border.
|
|
470
|
+
nativeSelect: SelectControl,
|
|
471
|
+
select: SelectControl,
|
|
472
|
+
toggleGroup: function ToggleGroupControl(props: ControlProps): ReactElement {
|
|
473
|
+
return <ToggleGroup value={typeof props.value === 'string' ? [props.value] : []} onValueChange={(next) => changeOf(props)(next[0] ?? null)} disabled={props.readOnly} aria-describedby={props.describedBy} className="flex gap-1">
|
|
474
|
+
{props.field.options?.map((option) => <ToggleGroupItem key={option.value} value={option.value}>{option.label[props.locale ?? ''] ?? option.value}</ToggleGroupItem>)}
|
|
475
|
+
</ToggleGroup>
|
|
476
|
+
},
|
|
477
|
+
radio: function RadioControl(props: ControlProps): ReactElement {
|
|
478
|
+
return <RadioGroup name={props.field.name} value={typeof props.value === 'string' ? props.value : ''} onValueChange={changeOf(props)} disabled={props.readOnly} aria-describedby={props.describedBy} aria-required={ariaRequired(props.field)} className="grid gap-2">
|
|
479
|
+
{props.field.options?.map((option) => <label key={option.value} className="flex items-center gap-2 text-sm">
|
|
480
|
+
<RadioGroupItem value={option.value} />{option.label[props.locale ?? ''] ?? option.value}
|
|
481
|
+
</label>)}
|
|
482
|
+
</RadioGroup>
|
|
483
|
+
},
|
|
484
|
+
switch: function SwitchControl(props: ControlProps): ReactElement {
|
|
485
|
+
return <Switch id={props.id} name={props.field.name} checked={props.value === true} onCheckedChange={changeOf(props)} disabled={props.readOnly} aria-describedby={props.describedBy} aria-required={ariaRequired(props.field)} />
|
|
486
|
+
},
|
|
487
|
+
text: typed('text'),
|
|
488
|
+
email: typed('email'),
|
|
489
|
+
// The write shape is `z.number()` with no coercion, so a reported string is a 400. `NaN` is reported as `undefined`,
|
|
490
|
+
// which `valuesOf` leaves out of the write, rather than as a value that would clear the column.
|
|
491
|
+
number: typed('number', asText, (text) => {
|
|
492
|
+
if (text === '') return null
|
|
493
|
+
const parsed = Number(text)
|
|
494
|
+
return Number.isNaN(parsed) ? undefined : parsed
|
|
495
|
+
}),
|
|
496
|
+
integer: (props) => controls.number!(props),
|
|
497
|
+
// A text box keeps every digit; a number box reads through a JS double and cannot edit exact money.
|
|
498
|
+
decimal: typed('text'),
|
|
499
|
+
datetime: DatePicker,
|
|
500
|
+
// A calendar day reports the `yyyy-mm-dd` the column holds and never goes through a moment, which would move the day.
|
|
501
|
+
date: DatePicker,
|
|
502
|
+
checkbox: function CheckboxControl(props: ControlProps): ReactElement {
|
|
503
|
+
const { describedBy, field, id, readOnly, value } = props
|
|
504
|
+
const change = changeOf(props)
|
|
505
|
+
// `=== true` because the checked state is `boolean | 'indeterminate'` and only a boolean is a value this column takes.
|
|
506
|
+
return (
|
|
507
|
+
<Checkbox
|
|
508
|
+
id={id}
|
|
509
|
+
name={field.name}
|
|
510
|
+
checked={value === true}
|
|
511
|
+
onCheckedChange={(next) => change(next === true)}
|
|
512
|
+
className={checkboxSize}
|
|
513
|
+
disabled={readOnly}
|
|
514
|
+
aria-describedby={describedBy}
|
|
515
|
+
aria-required={ariaRequired(field)}
|
|
516
|
+
/>
|
|
517
|
+
)
|
|
518
|
+
},
|
|
519
|
+
textArray: function TextArrayControl(props: ControlProps): ReactElement {
|
|
520
|
+
const { describedBy, error, field, id, placeholder, readOnly, value } = props
|
|
521
|
+
const change = changeOf(props)
|
|
522
|
+
return (
|
|
523
|
+
<Textarea
|
|
524
|
+
id={id}
|
|
525
|
+
name={field.name}
|
|
526
|
+
rows={3}
|
|
527
|
+
placeholder={placeholder}
|
|
528
|
+
dir="auto"
|
|
529
|
+
value={Array.isArray(value) ? value.map(asText).join('\n') : asText(value)}
|
|
530
|
+
onChange={(event) => {
|
|
531
|
+
const lines = event.target.value.split('\n').map((line) => line.trim()).filter(Boolean)
|
|
532
|
+
change(lines.length ? lines : null)
|
|
533
|
+
}}
|
|
534
|
+
disabled={readOnly}
|
|
535
|
+
readOnly={readOnly}
|
|
536
|
+
aria-describedby={describedBy}
|
|
537
|
+
aria-invalid={error ? true : undefined}
|
|
538
|
+
aria-required={ariaRequired(field)}
|
|
539
|
+
/>
|
|
540
|
+
)
|
|
541
|
+
},
|
|
542
|
+
uuid: ReadOnlyControl,
|
|
543
|
+
relationship: RelationshipControl,
|
|
544
|
+
upload: UploadControl,
|
|
545
|
+
richText: RichTextControl,
|
|
546
|
+
textarea: function TextareaControl(props: ControlProps): ReactElement {
|
|
547
|
+
const { describedBy, error, field, id, placeholder, readOnly, value } = props
|
|
548
|
+
const change = changeOf(props)
|
|
549
|
+
return <Textarea id={id} name={field.name} rows={4} dir="auto" value={asText(value)} onChange={(event) => change(cleared(event.target.value))} disabled={readOnly} readOnly={readOnly} placeholder={placeholder} aria-describedby={describedBy} aria-invalid={error ? true : undefined} aria-required={ariaRequired(field)} />
|
|
550
|
+
},
|
|
551
|
+
toggle: function ToggleControl(props: ControlProps): ReactElement {
|
|
552
|
+
return <Toggle id={props.id} variant="outline" pressed={props.value === true} onPressedChange={(pressed) => changeOf(props)(pressed)} disabled={props.readOnly} aria-describedby={props.describedBy}>
|
|
553
|
+
{labelFor({ kayan: props.kayan, field: props.field, copy: { t: props.t, locale: props.locale ?? '' } })}
|
|
554
|
+
</Toggle>
|
|
555
|
+
},
|
|
556
|
+
buttonGroup: function ButtonGroupControl(props: ControlProps): ReactElement {
|
|
557
|
+
return <ButtonGroup aria-describedby={props.describedBy}>
|
|
558
|
+
{props.field.options?.map((option) => <Button key={option.value} type="button" variant={props.value === option.value ? 'default' : 'outline'} aria-pressed={props.value === option.value} disabled={props.readOnly} onClick={() => changeOf(props)(option.value)}>
|
|
559
|
+
{option.label[props.locale ?? ''] ?? option.value}
|
|
560
|
+
</Button>)}
|
|
561
|
+
</ButtonGroup>
|
|
562
|
+
},
|
|
563
|
+
checkboxGroup: function CheckboxGroupControl(props: ControlProps): ReactElement {
|
|
564
|
+
const options = props.field.options ?? []
|
|
565
|
+
const held = new Set(Array.isArray(props.value) ? props.value.map(String) : [])
|
|
566
|
+
const change = changeOf(props)
|
|
567
|
+
return <div role="group" id={props.id} aria-describedby={props.describedBy} className="grid gap-2">
|
|
568
|
+
{options.map((option) => <label key={option.value} className="flex items-center gap-2 text-sm">
|
|
569
|
+
<Checkbox className={checkboxSize} checked={held.has(option.value)} disabled={props.readOnly} onCheckedChange={(on) => {
|
|
570
|
+
const next = options.map((one) => one.value).filter((value) => (value === option.value ? on === true : held.has(value)))
|
|
571
|
+
change(next.length ? next : null)
|
|
572
|
+
}} />
|
|
573
|
+
{option.label[props.locale ?? ''] ?? option.value}
|
|
574
|
+
</label>)}
|
|
575
|
+
</div>
|
|
576
|
+
},
|
|
577
|
+
calendar: function CalendarControl(props: ControlProps): ReactElement {
|
|
578
|
+
const held = dayOf(props.value)
|
|
579
|
+
return <Calendar mode="single" locale={dayPickerLocales[props.locale ?? '']} selected={held} defaultMonth={held} disabled={props.readOnly} onSelect={(date: Date | undefined) => changeOf(props)(date ? isoDay(date) : null)} aria-describedby={props.describedBy} />
|
|
580
|
+
},
|
|
581
|
+
}
|
|
582
|
+
// No prototype: a field's `component` is a string off the projection, and a plain object would answer `constructor` or
|
|
583
|
+
// `toString` with a function that is not a control.
|
|
584
|
+
Object.setPrototypeOf(controls, null)
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Whether a control's label sits beside it rather than above: a checkbox or a switch reads as attached to its words.
|
|
588
|
+
*/
|
|
589
|
+
export const besideLabel = (Control: unknown) => Control === controls.checkbox || Control === controls.switch
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* The control for a field: the one its screen decided, else the kind's own, else a read-only fallback. A name the registry does not carry falls through to the next step.
|
|
593
|
+
*/
|
|
594
|
+
export const controlFor = (field: ClientField): ((props: ControlProps) => ReactElement) =>
|
|
595
|
+
controls[field.component] ?? controls[field.kind] ?? ReadOnlyControl
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Whether a field is the server's to fill on this operation: a `uuid`, or a field that names a provider other than the client for it. Restates the server's `providerOf` rather than importing it, because this file is copied into a browser bundle that imports nothing from `mithaq` at runtime.
|
|
599
|
+
*/
|
|
600
|
+
export const readOnlyField = (field: ClientField, on: 'create' | 'update'): boolean =>
|
|
601
|
+
field.kind === 'uuid'
|
|
602
|
+
|| ((field.rules?.providedBy as { create?: string; update?: string } | undefined)?.[on] ?? 'client') !== 'client'
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Whether the report lets this actor create a row of a kayan with something to write: the create, and at least one client-provided field they may set. A role that may set only server-filled fields meets no New.
|
|
606
|
+
*/
|
|
607
|
+
export const createsIn = ({ kayan, report }: { kayan: ClientKayan; report: NonNullable<AccessReport['collections']>[string] }): boolean =>
|
|
608
|
+
report?.create === true && kayan.fields.some((field) => !readOnlyField(field, 'create') && report.fields?.[field.name]?.create === true)
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Whether a read was refused by the viewer's access (a 403) rather than failed, so a cell says the viewer cannot see a row for the first and never for a 500 or a network failure.
|
|
612
|
+
*/
|
|
613
|
+
export const isForbidden = (thrown: unknown): boolean =>
|
|
614
|
+
(thrown as { status?: unknown } | null | undefined)?.status === 403 || refusalKeyOf(thrown)?.startsWith('error.forbidden') === true
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* What a form starts holding: the row's values, one key per field, so a control is controlled from its first render. A field the row does not carry starts `undefined` and stays out of the write until something sets it.
|
|
618
|
+
*/
|
|
619
|
+
export const seedOf = (fields: readonly ClientField[], value?: Record<string, unknown> | null): Record<string, unknown> =>
|
|
620
|
+
Object.fromEntries(
|
|
621
|
+
fields.map((field) => [
|
|
622
|
+
field.name,
|
|
623
|
+
// On a create a checkbox starts `false` and a required text list `[]`, as drawn; an update leaves a missing key
|
|
624
|
+
// unseeded, since it is a column this actor may not read and seeding it would overwrite the row.
|
|
625
|
+
field.kind === 'datetime' && typeof value?.[field.name] === 'string' && !Number.isNaN(new Date(value[field.name] as string).getTime())
|
|
626
|
+
? new Date(value[field.name] as string)
|
|
627
|
+
: value?.[field.name] ?? (value ? undefined : field.kind === 'checkbox' ? false : field.kind === 'textArray' && field.rules.required === true ? [] : undefined),
|
|
628
|
+
]),
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Every field of `fields` the caller may write, as the object an operation takes. `owned` is the form's own answer for who provides a field, handed in so the fields drawn disabled and the fields left out of the write cannot drift apart.
|
|
633
|
+
*/
|
|
634
|
+
export const valuesOf = ({
|
|
635
|
+
fields,
|
|
636
|
+
values,
|
|
637
|
+
owned,
|
|
638
|
+
}: {
|
|
639
|
+
fields: readonly ClientField[]
|
|
640
|
+
values: Record<string, unknown>
|
|
641
|
+
owned: (field: ClientField) => boolean
|
|
642
|
+
}): Record<string, unknown> => {
|
|
643
|
+
const written: Record<string, unknown> = {}
|
|
644
|
+
for (const field of fields) {
|
|
645
|
+
if (owned(field)) continue
|
|
646
|
+
if (values[field.name] !== undefined) written[field.name] = values[field.name]
|
|
647
|
+
}
|
|
648
|
+
return written
|
|
649
|
+
}
|
|
650
|
+
// mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 c8c8412c13f92b549fa31642c5ca78a301fd0809919eb37f35a7a2bd7c0bf474
|