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,127 @@
|
|
|
1
|
+
import { use, useEffect, useState, type ReactNode } from 'react'
|
|
2
|
+
import type { ClientField, ClientKayan } from 'mithaq/admin'
|
|
3
|
+
import { ScreenContext } from 'mithaq/admin/react'
|
|
4
|
+
import type { CellProps } from './cells.tsx'
|
|
5
|
+
import { isForbidden, listed, type FindApi } from './controls.tsx'
|
|
6
|
+
import { findByIds, idsOf, nameOfRow, titleFieldOf } from './relationship-control.tsx'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What the table knows about the rows its relationship columns point at, keyed by target and id.
|
|
10
|
+
* `null` is loading, a string is the row's title, `false` is a row the read did not answer for
|
|
11
|
+
* (the access answer), an `Error` is a failed read, and an absent key was never asked for.
|
|
12
|
+
*/
|
|
13
|
+
export type RelationshipTitles = ReadonlyMap<string, string | Record<string, unknown> | null | false | Error>
|
|
14
|
+
|
|
15
|
+
export const titleKey = (target: string, id: string): string => `${target}:${id}`
|
|
16
|
+
|
|
17
|
+
/** Where the same map holds the row itself, for a cell that draws more than a title. */
|
|
18
|
+
export const rowKey = (target: string, id: string): string => `row:${target}:${id}`
|
|
19
|
+
|
|
20
|
+
/** Every id one page points at, grouped by the kayan it points into and counted once each, so a page costs one read per target. */
|
|
21
|
+
export const relationshipIdsOf = (
|
|
22
|
+
fields: readonly ClientField[],
|
|
23
|
+
docs: readonly Record<string, unknown>[],
|
|
24
|
+
): ReadonlyMap<string, readonly string[]> => {
|
|
25
|
+
const byTarget = new Map<string, Set<string>>()
|
|
26
|
+
for (const field of fields) {
|
|
27
|
+
if ((field.kind !== 'relationship' && field.kind !== 'upload') || !field.relationTo) continue
|
|
28
|
+
const held = byTarget.get(field.relationTo) ?? new Set<string>()
|
|
29
|
+
for (const row of docs) for (const id of idsOf(row[field.name])) held.add(id)
|
|
30
|
+
if (held.size) byTarget.set(field.relationTo, held)
|
|
31
|
+
}
|
|
32
|
+
return new Map([...byTarget].map(([target, ids]) => [target, [...ids]]))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One read per target per page, keyed on the grouped ids so an unchanged page asks for nothing.
|
|
37
|
+
* Cells read as loading until the browser's effect answers; with no `api` or screen, ids settle
|
|
38
|
+
* as rows the viewer cannot see. No cell renders the raw id.
|
|
39
|
+
*/
|
|
40
|
+
export const useRelationshipTitles = ({
|
|
41
|
+
fields,
|
|
42
|
+
docs,
|
|
43
|
+
context = {},
|
|
44
|
+
}: {
|
|
45
|
+
fields: readonly ClientField[]
|
|
46
|
+
docs: readonly Record<string, unknown>[]
|
|
47
|
+
context?: { api?: FindApi; client?: readonly ClientKayan[]; locale?: string }
|
|
48
|
+
}): RelationshipTitles => {
|
|
49
|
+
const { api, client, locale } = context
|
|
50
|
+
const maxLimit = use(ScreenContext)?.screen.maxLimit
|
|
51
|
+
const groups = relationshipIdsOf(fields, docs)
|
|
52
|
+
const asked = JSON.stringify([...groups])
|
|
53
|
+
const [titles, setTitles] = useState<RelationshipTitles>(() => new Map())
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
let live = true
|
|
57
|
+
const answers = new Map<string, string | Record<string, unknown> | null | false | Error>()
|
|
58
|
+
// A target with no reader or no known read cap settles as `false` now, rather than loading on a read that is never made.
|
|
59
|
+
for (const [target, ids] of groups) {
|
|
60
|
+
for (const id of ids) {
|
|
61
|
+
const asks = api?.[target] && maxLimit !== undefined ? null : false
|
|
62
|
+
answers.set(titleKey(target, id), asks)
|
|
63
|
+
answers.set(rowKey(target, id), asks)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!answers.size) return
|
|
67
|
+
setTitles(new Map(answers))
|
|
68
|
+
void Promise.all(
|
|
69
|
+
[...groups].map(async ([target, ids]) => {
|
|
70
|
+
const reads = api?.[target]
|
|
71
|
+
if (!reads || maxLimit === undefined) return
|
|
72
|
+
const kayan = client?.find((one) => one.name === target)
|
|
73
|
+
const title = titleFieldOf(kayan)
|
|
74
|
+
const kind = kayan?.fields.find((one) => one.name === title)?.kind
|
|
75
|
+
// A forbidden read is the same answer as a missing row; any other failure is held as its Error. Neither may reject: that would leave every cell loading.
|
|
76
|
+
const found = await findByIds({ reads, ids, maxLimit }).catch((thrown: unknown) => (isForbidden(thrown) ? [] : thrown instanceof Error ? thrown : new Error(String(thrown))))
|
|
77
|
+
if (found instanceof Error) {
|
|
78
|
+
for (const id of ids) {
|
|
79
|
+
answers.set(titleKey(target, id), found)
|
|
80
|
+
answers.set(rowKey(target, id), found)
|
|
81
|
+
}
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
const byId = new Map(found.map((row) => [String(row.id ?? ''), row]))
|
|
85
|
+
// An id the answer lacks is a row this actor may not read, so `false`, never left `null`.
|
|
86
|
+
for (const id of ids) {
|
|
87
|
+
const row = byId.get(id)
|
|
88
|
+
answers.set(titleKey(target, id), row ? nameOfRow({ row, title, locale, kind }) : false)
|
|
89
|
+
answers.set(rowKey(target, id), row ?? false)
|
|
90
|
+
}
|
|
91
|
+
}),
|
|
92
|
+
).then(() => {
|
|
93
|
+
if (live) setTitles(new Map(answers))
|
|
94
|
+
})
|
|
95
|
+
return () => {
|
|
96
|
+
live = false
|
|
97
|
+
}
|
|
98
|
+
// `groups` is rebuilt every render; `asked` is its content.
|
|
99
|
+
}, [api, asked, locale])
|
|
100
|
+
|
|
101
|
+
return titles
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const totalToShow = 3
|
|
105
|
+
|
|
106
|
+
export function RelationshipCell({ field, locale, t, titles, value }: CellProps): ReactNode {
|
|
107
|
+
const target = field.relationTo ?? ''
|
|
108
|
+
const ids = idsOf(value)
|
|
109
|
+
if (!ids.length) return ''
|
|
110
|
+
const names = ids.slice(0, totalToShow).map((id) => {
|
|
111
|
+
const found = titles?.get(titleKey(target, id))
|
|
112
|
+
if (found === null || found === undefined) return t('admin.loading', 'Loading…') ?? 'Loading…'
|
|
113
|
+
if (found instanceof Error) return '—'
|
|
114
|
+
// A row not answered for reads as unseeable, never as the raw id the column holds.
|
|
115
|
+
return typeof found === 'string' ? found : (t('admin.cannotSee', 'You can\'t see this') ?? 'You can\'t see this')
|
|
116
|
+
})
|
|
117
|
+
const rest = ids.length - names.length
|
|
118
|
+
return (
|
|
119
|
+
<span>
|
|
120
|
+
{listed(names, locale)}
|
|
121
|
+
{rest > 0
|
|
122
|
+
? ` ${(t('admin.andMore', 'and {count} more') ?? 'and {count} more').replace('{count}', String(rest))}`
|
|
123
|
+
: ''}
|
|
124
|
+
</span>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
// mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 9f18a8922be32e3a3671aed05e4f723d7e31d040d0c9ca341418aeaf6ab15cc4
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { createContext, use, useEffect, useRef, useState, type ReactElement, type ReactNode } from 'react'
|
|
2
|
+
import type { ClientKayan } from 'mithaq/admin'
|
|
3
|
+
import { ScreenContext } from 'mithaq/admin/react'
|
|
4
|
+
import {
|
|
5
|
+
Combobox,
|
|
6
|
+
ComboboxChip,
|
|
7
|
+
ComboboxChips,
|
|
8
|
+
ComboboxChipsInput,
|
|
9
|
+
ComboboxContent,
|
|
10
|
+
ComboboxInput,
|
|
11
|
+
ComboboxItem,
|
|
12
|
+
ComboboxList,
|
|
13
|
+
} from '@/components/ui/combobox'
|
|
14
|
+
import { Spinner } from '@/components/ui/spinner'
|
|
15
|
+
import { ariaRequired, changeOf, comboboxPhone, formatDate, isForbidden, type ControlProps, type FindApi } from './controls.tsx'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A chip's remove button is 24 pixels and a phone's finger needs 44: its hit area is widened by an
|
|
19
|
+
* inset over the chip, which the box around it keeps at 22 pixels.
|
|
20
|
+
*/
|
|
21
|
+
const chipRemovePhone = 'max-sm:[&_[data-slot=combobox-chip-remove]]:relative max-sm:[&_[data-slot=combobox-chip-remove]]:after:absolute max-sm:[&_[data-slot=combobox-chip-remove]]:after:-inset-[11px]'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What a row of the target is shown as, and what a search runs over. `??` rather than `||` because
|
|
25
|
+
* `useAsTitle` is either a field's name or absent, and `buildAsas` refuses a name that is not a field
|
|
26
|
+
* of the kayan.
|
|
27
|
+
*/
|
|
28
|
+
export const titleFieldOf = (target?: ClientKayan): string => target?.useAsTitle ?? 'id'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Every row of `ids` the target answers for, `maxLimit` ids at a time, so a set past the cap is answered in full.
|
|
32
|
+
* `maxLimit` is the build's own, as the server refuses a limit past it rather than clamping.
|
|
33
|
+
*/
|
|
34
|
+
export const findByIds = async ({ reads, ids, maxLimit }: { reads: NonNullable<FindApi[string]>; ids: readonly string[]; maxLimit: number }) => {
|
|
35
|
+
const docs: Record<string, unknown>[] = []
|
|
36
|
+
for (let at = 0; at < ids.length; at += maxLimit) {
|
|
37
|
+
const piece = ids.slice(at, at + maxLimit)
|
|
38
|
+
docs.push(...(await reads.find({ where: { id: { in: piece } }, limit: piece.length })).docs)
|
|
39
|
+
}
|
|
40
|
+
return docs
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether a term typed here can be turned into a condition at all: `like` takes text and email and
|
|
45
|
+
* nothing else, so a target whose title is its `id`, a uuid, offers a list to scroll and no search.
|
|
46
|
+
* Without this the first keystroke on such a picker is a 400.
|
|
47
|
+
*/
|
|
48
|
+
export const searchableTitle = (target: ClientKayan | undefined, title: string): boolean => {
|
|
49
|
+
const kind = target?.fields.find((field) => field.name === title)?.kind
|
|
50
|
+
return kind === 'text' || kind === 'email'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether the title is a localized field: one value per locale, which no sort can name and a `like`
|
|
55
|
+
* names by one locale, `name.ar`.
|
|
56
|
+
*/
|
|
57
|
+
export const localizedTitle = (target: ClientKayan | undefined, title: string): boolean =>
|
|
58
|
+
target?.fields.find((field) => field.name === title)?.rules.localized === true
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What one row of the target reads as: the title field's value, and the row's own id where the target
|
|
62
|
+
* declares no title or the read masked the column.
|
|
63
|
+
*/
|
|
64
|
+
export const nameOfRow = ({ row, title, locale, kind }: { row: Record<string, unknown>; title: string; locale?: string; kind?: string }): string => {
|
|
65
|
+
const held = row[title]
|
|
66
|
+
// A localized title is read in the viewer's locale and in no other: a row short of that locale shows
|
|
67
|
+
// its id, never another locale's words. A date is drawn in the format its column draws it in rather
|
|
68
|
+
// than as the wire's ISO string.
|
|
69
|
+
const shown = held !== null && typeof held === 'object' && !(held instanceof Date) ? (held as Record<string, unknown>)[locale ?? ''] : held
|
|
70
|
+
if (shown === null || shown === undefined || shown === '') return String(row.id ?? '')
|
|
71
|
+
const date = kind === 'datetime' || kind === 'date' ? formatDate({ value: shown, locale: locale ?? '', day: kind === 'date' }) : undefined
|
|
72
|
+
return date ?? String(shown)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* How a project names a picker's rows beyond their title: a hook, given the target kayan's name and the rows held,
|
|
77
|
+
* that returns a namer or `undefined` to keep the title. A hook because the name may need a read of its own.
|
|
78
|
+
*/
|
|
79
|
+
export type UseRowLabel = (target: string, rows: readonly Record<string, unknown>[]) => ((row: Record<string, unknown>) => string | undefined) | undefined
|
|
80
|
+
export const RowLabelContext = createContext<UseRowLabel>(() => undefined)
|
|
81
|
+
|
|
82
|
+
/** How many rows one page of the picker holds. */
|
|
83
|
+
const perPage = 10
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The ids one value holds: none, the one a many-to-one field carries, or the set a `hasMany` field
|
|
87
|
+
* carries, in the order the store answered with.
|
|
88
|
+
*/
|
|
89
|
+
export const idsOf = (value: unknown): readonly string[] =>
|
|
90
|
+
value === null || value === undefined ? [] : Array.isArray(value) ? value.map(String) : [String(value)]
|
|
91
|
+
|
|
92
|
+
/** One row of the target as the combobox's own item: base-ui reads a label off the shape `{ value, label }` without being told how. */
|
|
93
|
+
type Option = { value: string; label: string }
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The picker, and what each row of its list is drawn as beside its title where the caller draws more
|
|
97
|
+
* than the title, such as the upload control's thumbnail.
|
|
98
|
+
*/
|
|
99
|
+
export function RelationshipControl(props: ControlProps & { drawRow?: (row: Record<string, unknown>, label: string) => ReactNode }): ReactElement {
|
|
100
|
+
const { api, client, describedBy, drawRow, error, field, id, locale, placeholder, readOnly, t, value } = props
|
|
101
|
+
const change = changeOf(props)
|
|
102
|
+
const target = client?.find((one) => one.name === field.relationTo)
|
|
103
|
+
const title = titleFieldOf(target)
|
|
104
|
+
const searchable = searchableTitle(target, title)
|
|
105
|
+
const localized = localizedTitle(target, title)
|
|
106
|
+
const reads = target && api ? api[target.name] : undefined
|
|
107
|
+
// The most rows one read asks for: the build's `maxLimit`, off the screen. A form drawn with no
|
|
108
|
+
// screen has no such number, so it holds to the picker's own page of ten.
|
|
109
|
+
const most = use(ScreenContext)?.screen.maxLimit ?? perPage
|
|
110
|
+
/** Every id this field is holding, which is at most one unless the field said otherwise. */
|
|
111
|
+
const held = idsOf(value)
|
|
112
|
+
const many = field.hasMany === true
|
|
113
|
+
|
|
114
|
+
const [open, setOpen] = useState(false)
|
|
115
|
+
const [term, setTerm] = useState('')
|
|
116
|
+
/** The term a query has been run for, which is the typed one 300 ms later. */
|
|
117
|
+
const [searched, setSearched] = useState('')
|
|
118
|
+
const [rows, setRows] = useState<readonly Record<string, unknown>[]>([])
|
|
119
|
+
/** The page a scroll to the bottom loads next, off the answer's own `nextPage`. */
|
|
120
|
+
const [next, setNext] = useState<number | null>(null)
|
|
121
|
+
const [loading, setLoading] = useState(false)
|
|
122
|
+
/**
|
|
123
|
+
* Whether the last page read came back refused rather than empty, so the list does not say "no rows
|
|
124
|
+
* match this filter" for a target this actor may not read at all.
|
|
125
|
+
*/
|
|
126
|
+
const [refused, setRefused] = useState(false)
|
|
127
|
+
/**
|
|
128
|
+
* The chosen rows by id: one picked off a page needs no read, one the field was seeded with is read once.
|
|
129
|
+
* A map, since a `hasMany` field holds a set and a many-to-one field a set of one.
|
|
130
|
+
*/
|
|
131
|
+
const [chosen, setChosen] = useState<ReadonlyMap<string, Record<string, unknown>>>(() => new Map())
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What the popup is anchored on where the box is a chip list: the list itself and not the input
|
|
135
|
+
* inside it, which shrinks to the width the typed term needs, so a listbox that followed it would
|
|
136
|
+
* hang off the middle of the field.
|
|
137
|
+
*/
|
|
138
|
+
const chipsRef = useRef<HTMLDivElement | null>(null)
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Held ids the read settled without a row: `unseen` when refused or left out (as is every id of a target the screen
|
|
142
|
+
* does not cross), `failed` on a 500 or lost network, which says nothing about access.
|
|
143
|
+
*/
|
|
144
|
+
const [settled, setSettled] = useState<ReadonlyMap<string, 'unseen' | 'failed'>>(() => new Map())
|
|
145
|
+
|
|
146
|
+
const titleKind = target?.fields.find((one) => one.name === title)?.kind
|
|
147
|
+
const named = use(RowLabelContext)(field.relationTo ?? '', [...rows, ...chosen.values()])
|
|
148
|
+
const nameOf = (row: Record<string, unknown>) => named?.(row) ?? nameOfRow({ row, title, locale, kind: titleKind })
|
|
149
|
+
const rowOf = (held: string) => chosen.get(held) ?? rows.find((one) => String(one.id ?? '') === held)
|
|
150
|
+
const stateOf = (held: string) => (reads ? settled.get(held) : 'unseen')
|
|
151
|
+
/**
|
|
152
|
+
* What one held id reads as: the row's own title where something named it, a row this viewer cannot
|
|
153
|
+
* see where the read settled without it, a dash where the read failed, and loading until then. Never
|
|
154
|
+
* the raw id, which reads as data and is none of the viewer's.
|
|
155
|
+
*/
|
|
156
|
+
const labelOf = (held: string) => {
|
|
157
|
+
const row = rowOf(held)
|
|
158
|
+
if (row) return nameOf(row)
|
|
159
|
+
const state = stateOf(held)
|
|
160
|
+
if (state === 'failed') return '—'
|
|
161
|
+
return state === 'unseen'
|
|
162
|
+
? (t('admin.cannotSee', 'You can\'t see this') ?? 'You can\'t see this')
|
|
163
|
+
: (t('admin.loading', 'Loading…') ?? 'Loading…')
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A set whose every row the viewer cannot see says so once, in place of one chip for each. The chips
|
|
167
|
+
* are not drawn but are still the field's value, so `pick` refuses a shorter set while this holds:
|
|
168
|
+
* a row can be added, never taken off.
|
|
169
|
+
*/
|
|
170
|
+
const allRefused = many && held.length > 0 && held.every((one) => !rowOf(one) && stateOf(one) === 'unseen')
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* One page of the target, which is the whole query. The `not_in` keeps every chosen row out of the
|
|
174
|
+
* list; an empty list of ids is no special case, because the store writes `not_in []` as `true`.
|
|
175
|
+
*/
|
|
176
|
+
const queryFor = (page: number, search: string) => ({
|
|
177
|
+
limit: Math.min(perPage, most),
|
|
178
|
+
page,
|
|
179
|
+
...(!localized && { sort: title }),
|
|
180
|
+
where: {
|
|
181
|
+
id: { not_in: held },
|
|
182
|
+
...(searchable && search ? { [localized ? `${title}.${locale}` : title]: { like: search } } : {}),
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
/**
|
|
186
|
+
* The ids the query above is keyed on, as one string: `held` is rebuilt on every render, so an effect
|
|
187
|
+
* watching the array would read every page again on every keystroke.
|
|
188
|
+
*/
|
|
189
|
+
const excluded = held.join(',')
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (term === searched) return
|
|
193
|
+
const timer = setTimeout(() => setSearched(term), 300)
|
|
194
|
+
return () => clearTimeout(timer)
|
|
195
|
+
}, [searched, term])
|
|
196
|
+
|
|
197
|
+
// Page one, on open and on every term the debounce settles.
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (!open || !reads) return
|
|
200
|
+
let live = true
|
|
201
|
+
setLoading(true)
|
|
202
|
+
void reads
|
|
203
|
+
.find(queryFor(1, searched))
|
|
204
|
+
.then((found) => {
|
|
205
|
+
if (!live) return
|
|
206
|
+
setRefused(false)
|
|
207
|
+
setRows(found.docs)
|
|
208
|
+
setNext(found.hasNextPage ? found.nextPage : null)
|
|
209
|
+
})
|
|
210
|
+
// A read the server refuses leaves the list as it was rather than rejecting into nothing, and
|
|
211
|
+
// the list says it was refused rather than that nothing matched.
|
|
212
|
+
.catch(() => {
|
|
213
|
+
if (live) setRefused(true)
|
|
214
|
+
})
|
|
215
|
+
.finally(() => {
|
|
216
|
+
if (live) setLoading(false)
|
|
217
|
+
})
|
|
218
|
+
return () => {
|
|
219
|
+
live = false
|
|
220
|
+
}
|
|
221
|
+
}, [excluded, locale, open, reads, searched, title])
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The chosen rows, for every id the option list does not already hold: one read, in pieces the read
|
|
225
|
+
* cap admits (`findByIds`). A row picked off a page is never asked for, as picking it puts the row
|
|
226
|
+
* in `chosen`.
|
|
227
|
+
*/
|
|
228
|
+
const asked = useRef(new Set<string>())
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!reads) return
|
|
231
|
+
// Once per id, whatever the answer: an id the target does not answer for would otherwise be asked
|
|
232
|
+
// for again on every page the list loads, as the loaded rows are watched.
|
|
233
|
+
const missing = held.filter(
|
|
234
|
+
(one) => !chosen.has(one) && !rows.some((row) => String(row.id ?? '') === one) && !asked.current.has(one),
|
|
235
|
+
)
|
|
236
|
+
if (!missing.length) return
|
|
237
|
+
for (const one of missing) asked.current.add(one)
|
|
238
|
+
// No cleanup drops the answer: each id is asked for once, so an answer dropped because the effect
|
|
239
|
+
// ran again would never be asked for again, and the box would show the id for good.
|
|
240
|
+
void findByIds({ reads, ids: missing, maxLimit: most })
|
|
241
|
+
.then((docs) => {
|
|
242
|
+
const found = new Set(docs.map((row) => String(row.id ?? '')))
|
|
243
|
+
setSettled((already) => new Map([...already, ...missing.filter((one) => !found.has(one)).map((one) => [one, 'unseen'] as const)]))
|
|
244
|
+
if (!docs.length) return
|
|
245
|
+
setChosen((already) => new Map([...already, ...docs.map((row) => [String(row.id ?? ''), row] as const)]))
|
|
246
|
+
})
|
|
247
|
+
// A refusal is the same answer as a row not returned: the viewer cannot see it. A failure says
|
|
248
|
+
// only that the read did not answer.
|
|
249
|
+
.catch((thrown: unknown) => {
|
|
250
|
+
const state = isForbidden(thrown) ? 'unseen' : 'failed'
|
|
251
|
+
setSettled((already) => new Map([...already, ...missing.map((one) => [one, state] as const)]))
|
|
252
|
+
})
|
|
253
|
+
}, [chosen, excluded, reads, rows])
|
|
254
|
+
|
|
255
|
+
const selected = held.map((one) => ({ value: one, label: labelOf(one) }))
|
|
256
|
+
const searchLabel = t('admin.search', 'Search') ?? 'Search'
|
|
257
|
+
const options: Option[] = rows.map((row) => ({ value: String(row.id ?? ''), label: nameOf(row) }))
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* What a pick reports. Picked rows are remembered, so hydration does not re-ask for names the list showed.
|
|
261
|
+
* A `hasMany` field reports an array, never `null`: a `[]` seed against `null` would read as a change forever.
|
|
262
|
+
*/
|
|
263
|
+
const pick = (next: readonly Option[]) => {
|
|
264
|
+
// A set drawn as one note cannot be shortened: Backspace in the empty box takes the last chip off,
|
|
265
|
+
// and it would drop an id nobody sees.
|
|
266
|
+
if (allRefused && next.length < held.length) return
|
|
267
|
+
const picked = next.flatMap((one) => {
|
|
268
|
+
const row = rows.find((each) => String(each.id ?? '') === one.value)
|
|
269
|
+
return row ? [[one.value, row] as const] : []
|
|
270
|
+
})
|
|
271
|
+
if (picked.length) setChosen((already) => new Map([...already, ...picked]))
|
|
272
|
+
setTerm('')
|
|
273
|
+
const ids = next.map((one) => one.value)
|
|
274
|
+
change(many ? ids : (ids[0] ?? null))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Everything both boxes hand the root, written once. `filter={null}` because the filtering is the
|
|
279
|
+
* target kayan's `find` and not a pass over the page; `items` is the page as it arrived.
|
|
280
|
+
*/
|
|
281
|
+
const root = {
|
|
282
|
+
items: options,
|
|
283
|
+
filter: null,
|
|
284
|
+
// The first row is highlighted as the list opens, so Enter picks a row rather than nothing. The
|
|
285
|
+
// cast: base-ui's root types `autoHighlight` as a boolean while the combobox behind it reads
|
|
286
|
+
// `'always'`, and `true` alone highlights nothing until a term is typed.
|
|
287
|
+
autoHighlight: 'always' as unknown as boolean,
|
|
288
|
+
open,
|
|
289
|
+
onOpenChange: setOpen,
|
|
290
|
+
disabled: readOnly,
|
|
291
|
+
// The items are objects, so equal ids in two objects have to be told to match: the default is `Object.is`.
|
|
292
|
+
isItemEqualToValue: (one: Option, other: Option) => one.value === other.value,
|
|
293
|
+
// The typed term alone. base-ui writes the selected row's label into the input when the selection
|
|
294
|
+
// changes and when the list opens, and each of those calls this too; taking them as a search term
|
|
295
|
+
// would read the target for a name it is already showing.
|
|
296
|
+
onInputValueChange: (next: string, details: { reason: string }) =>
|
|
297
|
+
details.reason === 'input-change' && setTerm(next),
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** The listbox, which is the same page of rows whichever box is above it. */
|
|
301
|
+
const list = (
|
|
302
|
+
<ComboboxContent anchor={many ? chipsRef : undefined}>
|
|
303
|
+
<ComboboxList
|
|
304
|
+
onScroll={(event) => {
|
|
305
|
+
const list = event.currentTarget
|
|
306
|
+
if (loading || next === null || !reads) return
|
|
307
|
+
if (list.scrollTop + list.clientHeight < list.scrollHeight - 8) return
|
|
308
|
+
const page = next
|
|
309
|
+
setLoading(true)
|
|
310
|
+
void reads
|
|
311
|
+
.find(queryFor(page, searched))
|
|
312
|
+
.then((found) => {
|
|
313
|
+
setRows((already) => [...already, ...found.docs])
|
|
314
|
+
setNext(found.hasNextPage ? found.nextPage : null)
|
|
315
|
+
})
|
|
316
|
+
.catch(() => {})
|
|
317
|
+
.finally(() => setLoading(false))
|
|
318
|
+
}}
|
|
319
|
+
>
|
|
320
|
+
{options.map((option) => {
|
|
321
|
+
const row = drawRow && rows.find((one) => String(one.id ?? '') === option.value)
|
|
322
|
+
return (
|
|
323
|
+
<ComboboxItem key={option.value} value={option}>
|
|
324
|
+
{row ? drawRow(row, option.label) : option.label}
|
|
325
|
+
</ComboboxItem>
|
|
326
|
+
)
|
|
327
|
+
})}
|
|
328
|
+
</ComboboxList>
|
|
329
|
+
{loading ? (
|
|
330
|
+
<p className="flex items-center gap-2 px-3 py-1.5 text-sm text-muted-foreground">
|
|
331
|
+
<Spinner />
|
|
332
|
+
{t('admin.loading', 'Loading…') ?? 'Loading…'}
|
|
333
|
+
</p>
|
|
334
|
+
) : null}
|
|
335
|
+
{!loading && rows.length === 0 ? (
|
|
336
|
+
<p className="px-3 py-1.5 text-sm text-muted-foreground">
|
|
337
|
+
{refused
|
|
338
|
+
? (t('admin.refused', 'That list could not be read.') ?? 'That list could not be read.')
|
|
339
|
+
: (t('admin.noMatches', 'No rows match this filter.') ?? 'No rows match this filter.')}
|
|
340
|
+
</p>
|
|
341
|
+
) : null}
|
|
342
|
+
</ComboboxContent>
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
// A set: one chip per chosen row, rendered in the order `value` holds because base-ui removes a
|
|
346
|
+
// chip by its position among them.
|
|
347
|
+
if (many) {
|
|
348
|
+
return (
|
|
349
|
+
<Combobox {...root} multiple value={selected} onValueChange={pick}>
|
|
350
|
+
<ComboboxChips ref={chipsRef} aria-invalid={error ? true : undefined}>
|
|
351
|
+
{allRefused ? (
|
|
352
|
+
<span className="text-muted-foreground px-1 text-sm">{selected[0]!.label}</span>
|
|
353
|
+
) : selected.map((one) => (
|
|
354
|
+
<ComboboxChip key={one.value} className={chipRemovePhone} removeLabel={t('admin.remove', 'Remove')} showRemove={!readOnly}>
|
|
355
|
+
{one.label}
|
|
356
|
+
</ComboboxChip>
|
|
357
|
+
))}
|
|
358
|
+
<ComboboxChipsInput
|
|
359
|
+
id={id}
|
|
360
|
+
// A hint is for an empty box: beside a chip it reads as a value nobody chose, so the box keeps
|
|
361
|
+
// its name in `aria-label` and says nothing once a row is held.
|
|
362
|
+
placeholder={selected.length ? undefined : (placeholder ?? searchLabel)}
|
|
363
|
+
disabled={readOnly}
|
|
364
|
+
readOnly={!searchable}
|
|
365
|
+
aria-label={searchLabel}
|
|
366
|
+
aria-describedby={describedBy}
|
|
367
|
+
aria-invalid={error ? true : undefined}
|
|
368
|
+
aria-required={ariaRequired(field)}
|
|
369
|
+
/>
|
|
370
|
+
</ComboboxChips>
|
|
371
|
+
{list}
|
|
372
|
+
</Combobox>
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
// The keyboard model (roles, aria wiring, arrows, Enter, Escape) is base-ui's, so none is here.
|
|
378
|
+
<Combobox {...root} value={selected[0] ?? null} onValueChange={(next) => pick(next ? [next] : [])}>
|
|
379
|
+
<ComboboxInput
|
|
380
|
+
id={id}
|
|
381
|
+
className={comboboxPhone}
|
|
382
|
+
placeholder={placeholder ?? searchLabel}
|
|
383
|
+
disabled={readOnly}
|
|
384
|
+
// A target whose title cannot take `like` offers a list and no search, so the box takes no term
|
|
385
|
+
// and still takes the focus, the arrows and Enter.
|
|
386
|
+
readOnly={!searchable}
|
|
387
|
+
aria-label={searchLabel}
|
|
388
|
+
aria-describedby={describedBy}
|
|
389
|
+
aria-invalid={error ? true : undefined}
|
|
390
|
+
aria-required={ariaRequired(field)}
|
|
391
|
+
// The two icon-only buttons of the addon are named here because base-ui names neither.
|
|
392
|
+
clearLabel={t('admin.clear', 'Clear')}
|
|
393
|
+
triggerLabel={t('admin.showOptions', 'Show options')}
|
|
394
|
+
showClear={held.length > 0 && !readOnly}
|
|
395
|
+
/>
|
|
396
|
+
{list}
|
|
397
|
+
</Combobox>
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
// mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 379037afe9749f23307c507f39a782a9fa4e232e83f63894be6f9aef24f76cb5
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { defaultNodes } from 'mithaq/rich-text'
|
|
2
|
+
|
|
3
|
+
// A project's own list spreads this, in a file both the editor and the server import:
|
|
4
|
+
//
|
|
5
|
+
// export const richTextNodes = [...defaultNodes, CalloutNode]
|
|
6
|
+
// mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 12555780fdbc7bc6b283e1c56c498b12265f7c417359a0d4eff71d8207bdb3cd
|