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,108 @@
|
|
|
1
|
+
import { useState, type ReactElement } from 'react'
|
|
2
|
+
import { ChevronDown, Columns as LayoutColumns } from 'lucide-react'
|
|
3
|
+
import type { ClientField, ClientKayan } from 'mithaq/admin'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { Checkbox } from '@/components/ui/checkbox'
|
|
6
|
+
import { Label } from '@/components/ui/label'
|
|
7
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
|
8
|
+
import { checkboxSize, labelFor, type Translate } from './controls.tsx'
|
|
9
|
+
import type { ColumnChoice } from './list-query.ts'
|
|
10
|
+
|
|
11
|
+
export type ColumnsProps = {
|
|
12
|
+
kayan: ClientKayan
|
|
13
|
+
/** Every column, in order, with whether it is shown. */
|
|
14
|
+
columns: readonly ColumnChoice[]
|
|
15
|
+
onChange: (columns: readonly ColumnChoice[]) => void
|
|
16
|
+
t: Translate
|
|
17
|
+
locale: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Which fields the popup may offer: the ones this actor may read. Chrome only: the read masks a denied column whatever the popup shows. */
|
|
21
|
+
export const columnCandidates = (
|
|
22
|
+
kayan: ClientKayan,
|
|
23
|
+
reads?: (kayan: string, field: string) => boolean,
|
|
24
|
+
): readonly ClientField[] =>
|
|
25
|
+
kayan.fields.filter(
|
|
26
|
+
(field) => !reads || reads(kayan.name, field.name),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
/** The next column in the same section, or -1 at its end. The popup draws one list twice, so a neighbour never crosses into the other section. */
|
|
30
|
+
export const neighbour = ({ columns, at, by }: { columns: readonly ColumnChoice[]; at: number; by: -1 | 1 }): number => {
|
|
31
|
+
for (let index = at + by; index >= 0 && index < columns.length; index += by) {
|
|
32
|
+
if (columns[index]!.active === columns[at]!.active) return index
|
|
33
|
+
}
|
|
34
|
+
return -1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One column swapped with its neighbour in the same section; at a section's end the same list comes back. */
|
|
38
|
+
export const move = ({ columns, at, by }: { columns: readonly ColumnChoice[]; at: number; by: -1 | 1 }): readonly ColumnChoice[] => {
|
|
39
|
+
const to = neighbour({ columns, at, by })
|
|
40
|
+
if (to < 0) return columns
|
|
41
|
+
const next = [...columns]
|
|
42
|
+
next[at] = columns[to]!
|
|
43
|
+
next[to] = columns[at]!
|
|
44
|
+
return next
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Columns({ columns, kayan, locale, onChange, t }: ColumnsProps): ReactElement {
|
|
48
|
+
const [open, setOpen] = useState(false)
|
|
49
|
+
const byName = new Map(kayan.fields.map((field) => [field.name, field]))
|
|
50
|
+
const nameOf = (name: string) => {
|
|
51
|
+
const field = byName.get(name)
|
|
52
|
+
return field ? labelFor({ kayan, field, copy: { t, locale } }) : name
|
|
53
|
+
}
|
|
54
|
+
const toggle = (name: string) =>
|
|
55
|
+
onChange(columns.map((one) => (one.name === name ? { ...one, active: !one.active } : one)))
|
|
56
|
+
// The box is a `role="checkbox"` button, so the label points at it by id; the kayan name keeps ids unique when two lists share a screen.
|
|
57
|
+
const boxId = (name: string) => `mithaq-column-${kayan.name}-${name}`
|
|
58
|
+
const section = (active: boolean) => (
|
|
59
|
+
<ul className="flex flex-col gap-1">
|
|
60
|
+
{columns.map((column, at) =>
|
|
61
|
+
column.active !== active ? null : (
|
|
62
|
+
<li key={column.name} className="flex items-center justify-between gap-2">
|
|
63
|
+
<span className="flex items-center gap-2">
|
|
64
|
+
<Checkbox className={checkboxSize} id={boxId(column.name)} checked={column.active} onCheckedChange={() => toggle(column.name)} />
|
|
65
|
+
<Label htmlFor={boxId(column.name)} className="font-normal">
|
|
66
|
+
{nameOf(column.name)}
|
|
67
|
+
</Label>
|
|
68
|
+
</span>
|
|
69
|
+
<span className="flex gap-1">
|
|
70
|
+
<Button type="button" variant="outline" size="icon-xs" className="max-sm:size-11" disabled={neighbour({ columns, at, by: -1 }) < 0} onClick={() => onChange(move({ columns, at, by: -1 }))} aria-label={`${t('columns.up', 'Move up') ?? 'Move up'}: ${nameOf(column.name)}`}>
|
|
71
|
+
↑
|
|
72
|
+
</Button>
|
|
73
|
+
<Button type="button" variant="outline" size="icon-xs" className="max-sm:size-11" disabled={neighbour({ columns, at, by: 1 }) < 0} onClick={() => onChange(move({ columns, at, by: 1 }))} aria-label={`${t('columns.down', 'Move down') ?? 'Move down'}: ${nameOf(column.name)}`}>
|
|
74
|
+
↓
|
|
75
|
+
</Button>
|
|
76
|
+
</span>
|
|
77
|
+
</li>
|
|
78
|
+
),
|
|
79
|
+
)}
|
|
80
|
+
</ul>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
85
|
+
<PopoverTrigger
|
|
86
|
+
render={
|
|
87
|
+
<Button type="button" variant="outline" size="sm" className="max-sm:h-11">
|
|
88
|
+
<LayoutColumns />
|
|
89
|
+
<span className="hidden lg:inline">{t('columns.customize', 'Customize Columns') ?? 'Customize Columns'}</span>
|
|
90
|
+
<span className="lg:hidden">{t('columns.title', 'Columns') ?? 'Columns'}</span>
|
|
91
|
+
<ChevronDown />
|
|
92
|
+
</Button>
|
|
93
|
+
}
|
|
94
|
+
/>
|
|
95
|
+
<PopoverContent align="end" className="flex max-h-[min(var(--available-height),28rem)] w-56 flex-col gap-3 overflow-y-auto p-3">
|
|
96
|
+
<div className="flex flex-col gap-1">
|
|
97
|
+
<p className="text-xs font-medium uppercase text-muted-foreground">{t('columns.shown', 'Shown') ?? 'Shown'}</p>
|
|
98
|
+
{section(true)}
|
|
99
|
+
</div>
|
|
100
|
+
<div className="flex flex-col gap-1">
|
|
101
|
+
<p className="text-xs font-medium uppercase text-muted-foreground">{t('columns.hidden', 'Not shown') ?? 'Not shown'}</p>
|
|
102
|
+
{section(false)}
|
|
103
|
+
</div>
|
|
104
|
+
</PopoverContent>
|
|
105
|
+
</Popover>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
// mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 94cd106d0ec41f77e03f3e843d130f7cf848c1c56619ab392d98a5d0d95bd0f1
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ReactElement } from 'react'
|
|
2
|
+
import type { ClientKayan } from 'mithaq/admin'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { Input } from '@/components/ui/input'
|
|
5
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
6
|
+
import { controls, labelFor, type FindApi, type Translate } from './controls.tsx'
|
|
7
|
+
import { RelationshipControl } from './relationship-control.tsx'
|
|
8
|
+
import { operatorsByKind, valueShapeOf, type FilterPath } from './filter-operators.ts'
|
|
9
|
+
import { instantOf, type Condition, type FilterWhere } from './list-query.ts'
|
|
10
|
+
|
|
11
|
+
export type FiltersProps = {
|
|
12
|
+
/** Every path this kayan offers, already narrowed by the access report. */
|
|
13
|
+
paths: readonly FilterPath[]
|
|
14
|
+
value?: FilterWhere
|
|
15
|
+
/** Where a committed panel goes. `undefined` is "no filter at all". */
|
|
16
|
+
onChange: (where: FilterWhere | undefined) => void
|
|
17
|
+
t: Translate
|
|
18
|
+
locale: string
|
|
19
|
+
/** Every kayan of the build, so a relationship row's picker can find its target. */
|
|
20
|
+
client?: readonly ClientKayan[]
|
|
21
|
+
/** The typed client's reads, for that picker's own paging. */
|
|
22
|
+
api?: FindApi
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type Row = { path: string; operator: string; value: unknown }
|
|
26
|
+
|
|
27
|
+
const rowOf = (condition: Condition): Row => {
|
|
28
|
+
const [path = '', operators = {}] = Object.entries(condition)[0] ?? []
|
|
29
|
+
const [operator = 'equals', value] = Object.entries(operators)[0] ?? []
|
|
30
|
+
return { path, operator, value }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const conditionOf = (row: Row): Condition => ({ [row.path]: { [row.operator]: row.value } })
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The rows the panel holds: the `or` of `and`s a `FilterWhere` carries. Held as state, not derived
|
|
37
|
+
* from `value`, because a row with no value yet does not survive the round trip through the URL.
|
|
38
|
+
*/
|
|
39
|
+
export type Conditions = readonly { and: Condition[] }[]
|
|
40
|
+
|
|
41
|
+
/** What a set of rows commits: the filled rows, and no group that keeps none. */
|
|
42
|
+
export const committedOf = (conditions: Conditions): FilterWhere | undefined => {
|
|
43
|
+
const kept = conditions
|
|
44
|
+
.map((group) => ({ and: group.and.filter((one) => !isEmptyConditionValue(rowOf(one).value)) }))
|
|
45
|
+
.filter((group) => group.and.length > 0)
|
|
46
|
+
return kept.length ? { or: kept } : undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One more row, in a group of its own; `and` is reached by adding and then switching the join. */
|
|
50
|
+
export const addCondition = (conditions: Conditions, condition: Condition): Conditions => [
|
|
51
|
+
...conditions,
|
|
52
|
+
{ and: [condition] },
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
/** One row gone, and the group it emptied gone with it. */
|
|
56
|
+
export const removeCondition = ({ conditions, orIndex, andIndex }: { conditions: Conditions; orIndex: number; andIndex: number }): Conditions =>
|
|
57
|
+
conditions
|
|
58
|
+
.map((group, o) => (o === orIndex ? { and: group.and.filter((_, a) => a !== andIndex) } : group))
|
|
59
|
+
.filter((group) => group.and.length > 0)
|
|
60
|
+
|
|
61
|
+
/** One row rewritten in place. */
|
|
62
|
+
export const updateCondition = ({
|
|
63
|
+
conditions,
|
|
64
|
+
orIndex,
|
|
65
|
+
andIndex,
|
|
66
|
+
condition,
|
|
67
|
+
}: {
|
|
68
|
+
conditions: Conditions
|
|
69
|
+
orIndex: number
|
|
70
|
+
andIndex: number
|
|
71
|
+
condition: Condition
|
|
72
|
+
}): Conditions =>
|
|
73
|
+
conditions.map((group, o) => (o === orIndex ? { and: group.and.map((one, a) => (a === andIndex ? condition : one)) } : group))
|
|
74
|
+
|
|
75
|
+
/** Moves one row between `and` and `or` by flattening the groups to one list and rebuilding them from each row's join. */
|
|
76
|
+
export const updateJoin = ({
|
|
77
|
+
conditions,
|
|
78
|
+
orIndex,
|
|
79
|
+
andIndex,
|
|
80
|
+
join,
|
|
81
|
+
}: {
|
|
82
|
+
conditions: Conditions
|
|
83
|
+
orIndex: number
|
|
84
|
+
andIndex: number
|
|
85
|
+
join: 'and' | 'or'
|
|
86
|
+
}): Conditions => {
|
|
87
|
+
const flattened: { condition: Condition; join: 'and' | 'or' | null }[] = []
|
|
88
|
+
conditions.forEach((group, o) => {
|
|
89
|
+
group.and.forEach((condition, a) => {
|
|
90
|
+
const isTarget = o === orIndex && a === andIndex
|
|
91
|
+
const current = flattened.length === 0 ? null : a === 0 ? 'or' : 'and'
|
|
92
|
+
flattened.push({ condition, join: isTarget ? join : current })
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
const rebuilt: { and: Condition[] }[] = []
|
|
96
|
+
flattened.forEach(({ condition, join: itemJoin }, index) => {
|
|
97
|
+
if (index === 0 || itemJoin === 'or') rebuilt.push({ and: [condition] })
|
|
98
|
+
else rebuilt[rebuilt.length - 1]!.and.push(condition)
|
|
99
|
+
})
|
|
100
|
+
return rebuilt
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const isEmptyConditionValue = (value: unknown): boolean => value === undefined || value === null || value === ''
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* An empty row reports `undefined` as soon as it mounts, and committing that stores
|
|
107
|
+
* `{ [operator]: undefined }`, a row gone on the next parse. Only a value edit can be a no-op.
|
|
108
|
+
*/
|
|
109
|
+
const isNoOpConditionValueUpdate = (stored: unknown, incoming: unknown): boolean =>
|
|
110
|
+
isEmptyConditionValue(stored) && isEmptyConditionValue(incoming)
|
|
111
|
+
|
|
112
|
+
// A select item's value may not be '' (the primitive reserves it for nothing chosen), so an unanswered row is carried under this name and mapped back to ''.
|
|
113
|
+
const unanswered = 'unanswered'
|
|
114
|
+
|
|
115
|
+
// `like` needs telling apart from `contains`: the drizzle store runs one `ilike` per whitespace-separated word. Labels go through `t` under `operator.<name>`.
|
|
116
|
+
const operatorFallbacks: Readonly<Record<string, string>> = {
|
|
117
|
+
contains: 'contains',
|
|
118
|
+
equals: 'is',
|
|
119
|
+
exists: 'exists',
|
|
120
|
+
greater_than: 'is after',
|
|
121
|
+
greater_than_equal: 'is at least',
|
|
122
|
+
in: 'is one of',
|
|
123
|
+
less_than: 'is before',
|
|
124
|
+
less_than_equal: 'is at most',
|
|
125
|
+
like: 'matches every word',
|
|
126
|
+
not_equals: 'is not',
|
|
127
|
+
not_in: 'is none of',
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const asText = (value: unknown) =>
|
|
131
|
+
value === null || value === undefined ? '' : Array.isArray(value) ? value.join(', ') : String(value)
|
|
132
|
+
|
|
133
|
+
// A moment typed with no zone becomes the instant it names on the viewer's clock; a moment with its own zone, and text no clock reads, are sent as typed for the server to judge.
|
|
134
|
+
const valueFrom = (kind: string, operator: string, text: string): unknown => {
|
|
135
|
+
const shape = valueShapeOf(operator)
|
|
136
|
+
if (shape === 'boolean') return text === '' ? undefined : text === 'true'
|
|
137
|
+
if (shape === 'list') {
|
|
138
|
+
const tokens = text.split(',').map((one) => one.trim()).filter(Boolean)
|
|
139
|
+
return tokens.length ? (kind === 'number' || kind === 'integer' ? tokens.map(Number) : kind === 'datetime' ? tokens.map(instantOf) : tokens) : undefined
|
|
140
|
+
}
|
|
141
|
+
if (text === '') return undefined
|
|
142
|
+
if (kind === 'checkbox') return text === 'true'
|
|
143
|
+
return kind === 'number' || kind === 'integer' ? Number(text) : kind === 'datetime' ? instantOf(text) : text
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function ValueInput({ id, locale, operator, onCommit, path, t, value }: { id: string; locale: string; operator: string; path: FilterPath; t: Translate; value: unknown; onCommit: (value: unknown) => void }): ReactElement {
|
|
147
|
+
const { kind } = path
|
|
148
|
+
const [text, setText] = useState(asText(value))
|
|
149
|
+
// `sent` is the last commit read back as text; it differs from the typed text where the commit converts it.
|
|
150
|
+
const reported = useRef(asText(value))
|
|
151
|
+
const sent = useRef(asText(value))
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
// Re-seeding from this input's own commit coming back would drop keystrokes typed since, or swap the moment typed for its instant.
|
|
154
|
+
const incoming = asText(value)
|
|
155
|
+
if (incoming !== sent.current) {
|
|
156
|
+
reported.current = sent.current = incoming
|
|
157
|
+
setText(incoming)
|
|
158
|
+
}
|
|
159
|
+
}, [value])
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (text === reported.current) return
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
const next = valueFrom(kind, operator, text)
|
|
164
|
+
reported.current = text
|
|
165
|
+
sent.current = asText(next)
|
|
166
|
+
onCommit(next)
|
|
167
|
+
}, 300)
|
|
168
|
+
return () => clearTimeout(timer)
|
|
169
|
+
}, [kind, onCommit, operator, text])
|
|
170
|
+
|
|
171
|
+
const shape = valueShapeOf(operator)
|
|
172
|
+
if (shape === 'boolean' || kind === 'checkbox') {
|
|
173
|
+
return (
|
|
174
|
+
<Select value={text === '' ? unanswered : text} onValueChange={(next) => setText(next === null || next === unanswered ? '' : next)}>
|
|
175
|
+
{/* The word is the trigger's own child: the item list mounts only while open, so a server render would show an empty trigger. */}
|
|
176
|
+
<SelectTrigger aria-label="value" size="sm">
|
|
177
|
+
<SelectValue>{text === '' ? '—' : text}</SelectValue>
|
|
178
|
+
</SelectTrigger>
|
|
179
|
+
<SelectContent>
|
|
180
|
+
<SelectItem value={unanswered}>—</SelectItem>
|
|
181
|
+
<SelectItem value="true">true</SelectItem>
|
|
182
|
+
<SelectItem value="false">false</SelectItem>
|
|
183
|
+
</SelectContent>
|
|
184
|
+
</Select>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
// A moment is sent as its ISO instant, so 09:30 picked at UTC+3 asks for 06:30Z and the picker draws it back on the reader's clock. `in` and `not_in` keep the token list.
|
|
188
|
+
if ((kind === 'date' || kind === 'datetime') && shape !== 'list') {
|
|
189
|
+
const Picker = controls[kind]!
|
|
190
|
+
return (
|
|
191
|
+
<Picker
|
|
192
|
+
field={path.field}
|
|
193
|
+
kayan={path.kayan}
|
|
194
|
+
values={{}}
|
|
195
|
+
setValue={() => {}}
|
|
196
|
+
t={t}
|
|
197
|
+
locale={locale}
|
|
198
|
+
id={id}
|
|
199
|
+
value={text}
|
|
200
|
+
readOnly={false}
|
|
201
|
+
onChange={(next) => setText(next instanceof Date ? next.toISOString() : asText(next))}
|
|
202
|
+
/>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
// A relationship reaches this arm only for `in` and `not_in`, a list of ids; its scalar operators get the picker.
|
|
206
|
+
const type = shape === 'list' ? 'text' : kind === 'number' || kind === 'integer' ? 'number' : kind === 'email' ? 'email' : 'text'
|
|
207
|
+
return (
|
|
208
|
+
<Input
|
|
209
|
+
aria-label="value"
|
|
210
|
+
className="w-auto"
|
|
211
|
+
type={type}
|
|
212
|
+
step={kind === 'integer' && shape !== 'list' ? '1' : undefined}
|
|
213
|
+
inputMode={kind === 'decimal' ? 'decimal' : undefined}
|
|
214
|
+
dir="auto"
|
|
215
|
+
value={text}
|
|
216
|
+
placeholder={shape === 'list' ? 'a, b, c' : undefined}
|
|
217
|
+
onChange={(event) => setText(event.target.value)}
|
|
218
|
+
/>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function Filters({ api, client, locale, onChange, paths, t, value }: FiltersProps): ReactElement | null {
|
|
223
|
+
const [conditions, setConditions] = useState<Conditions>(() => value?.or ?? [])
|
|
224
|
+
// What this panel commits comes back through the URL with the unfilled rows gone; re-seeding from that would drop the row being written. A `value` it did not report is an outside change and does re-seed.
|
|
225
|
+
const reported = useRef(JSON.stringify(value ?? null))
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
const incoming = JSON.stringify(value ?? null)
|
|
228
|
+
if (incoming === reported.current) return
|
|
229
|
+
reported.current = incoming
|
|
230
|
+
setConditions(value?.or ?? [])
|
|
231
|
+
}, [value])
|
|
232
|
+
|
|
233
|
+
// No panel for a kayan with no filterable field; after the hooks, which must run on every render.
|
|
234
|
+
if (!paths.length) return null
|
|
235
|
+
const first = paths[0]!
|
|
236
|
+
const flat = conditions.flatMap((group, orIndex) => group.and.map((condition, andIndex) => ({ andIndex, condition, orIndex })))
|
|
237
|
+
const blank = (): Condition => ({ [first.path]: { [(operatorsByKind[first.kind] ?? ['equals'])[0]!]: undefined } })
|
|
238
|
+
|
|
239
|
+
const commit = (next: Conditions) => {
|
|
240
|
+
setConditions(next)
|
|
241
|
+
const where = committedOf(next)
|
|
242
|
+
const encoded = JSON.stringify(where ?? null)
|
|
243
|
+
reported.current = encoded
|
|
244
|
+
// Nothing goes up when the commit equals the filter the host holds: a host resets to page one on any `onChange`, so Add filter on page three would lose the page.
|
|
245
|
+
if (encoded !== JSON.stringify(value ?? null)) onChange(where)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const replace = (orIndex: number, andIndex: number, row: Row) =>
|
|
249
|
+
commit(updateCondition({ conditions, orIndex, andIndex, condition: conditionOf(row) }))
|
|
250
|
+
|
|
251
|
+
// With nothing committed the panel shows the virtual row below, so the click makes it real and appends beside it.
|
|
252
|
+
const add = () => commit(flat.length ? addCondition(conditions, blank()) : [{ and: [blank()] }, { and: [blank()] }])
|
|
253
|
+
|
|
254
|
+
const labelOf = (path: FilterPath) => {
|
|
255
|
+
const own = labelFor({ kayan: path.kayan, field: path.field, copy: { t, locale } })
|
|
256
|
+
return path.through && path.through !== path.field.name ? `${path.through} > ${own}` : own
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The virtual first row commits on the first real edit rather than on mount, so an untouched panel writes no URL.
|
|
260
|
+
const rows = flat.length ? flat : [{ andIndex: 0, condition: blank(), orIndex: 0 }]
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div className="flex flex-col gap-2 rounded-lg border p-3" data-mithaq-filters={rows.length}>
|
|
264
|
+
{rows.map(({ andIndex, condition, orIndex }, index) => {
|
|
265
|
+
const row = rowOf(condition)
|
|
266
|
+
const path = paths.find((one) => one.path === row.path) ?? first
|
|
267
|
+
const operators = operatorsByKind[path.kind] ?? []
|
|
268
|
+
const operator = operators.includes(row.operator as never) ? row.operator : (operators[0] ?? 'equals')
|
|
269
|
+
const virtual = flat.length === 0
|
|
270
|
+
const write = (next: Row) => (virtual ? commit([{ and: [conditionOf(next)] }]) : replace(orIndex, andIndex, next))
|
|
271
|
+
return (
|
|
272
|
+
<div key={`${orIndex}.${andIndex}`} className="flex flex-wrap items-center gap-2">
|
|
273
|
+
{index === 0 ? (
|
|
274
|
+
<span className="text-sm text-muted-foreground">{t('filter.where', 'Where') ?? 'Where'}</span>
|
|
275
|
+
) : (
|
|
276
|
+
<Select
|
|
277
|
+
value={andIndex === 0 ? 'or' : 'and'}
|
|
278
|
+
onValueChange={(next) => commit(updateJoin({ conditions, orIndex, andIndex, join: next === 'or' ? 'or' : 'and' }))}
|
|
279
|
+
>
|
|
280
|
+
<SelectTrigger aria-label={t('filter.join', 'Join') ?? 'Join'} size="sm">
|
|
281
|
+
<SelectValue>
|
|
282
|
+
{andIndex === 0 ? (t('filter.or', 'or') ?? 'or') : (t('filter.and', 'and') ?? 'and')}
|
|
283
|
+
</SelectValue>
|
|
284
|
+
</SelectTrigger>
|
|
285
|
+
<SelectContent>
|
|
286
|
+
<SelectItem value="and">{t('filter.and', 'and') ?? 'and'}</SelectItem>
|
|
287
|
+
<SelectItem value="or">{t('filter.or', 'or') ?? 'or'}</SelectItem>
|
|
288
|
+
</SelectContent>
|
|
289
|
+
</Select>
|
|
290
|
+
)}
|
|
291
|
+
<Select
|
|
292
|
+
value={path.path}
|
|
293
|
+
onValueChange={(chosen) => {
|
|
294
|
+
const next = paths.find((one) => one.path === chosen) ?? first
|
|
295
|
+
const kept = (operatorsByKind[next.kind] ?? []).includes(operator as never) ? operator : (operatorsByKind[next.kind] ?? ['equals'])[0]!
|
|
296
|
+
write({ path: next.path, operator: kept, value: undefined })
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
<SelectTrigger aria-label={t('filter.field', 'Field') ?? 'Field'} size="sm">
|
|
300
|
+
<SelectValue>{labelOf(path)}</SelectValue>
|
|
301
|
+
</SelectTrigger>
|
|
302
|
+
<SelectContent>
|
|
303
|
+
{paths.map((one) => (
|
|
304
|
+
<SelectItem key={one.path} value={one.path}>
|
|
305
|
+
{labelOf(one)}
|
|
306
|
+
</SelectItem>
|
|
307
|
+
))}
|
|
308
|
+
</SelectContent>
|
|
309
|
+
</Select>
|
|
310
|
+
<Select value={operator} onValueChange={(chosen) => chosen && write({ path: path.path, operator: chosen, value: undefined })}>
|
|
311
|
+
<SelectTrigger aria-label={t('filter.operator', 'Operator') ?? 'Operator'} size="sm">
|
|
312
|
+
<SelectValue>{t(`operator.${operator}`, operatorFallbacks[operator]) ?? operator}</SelectValue>
|
|
313
|
+
</SelectTrigger>
|
|
314
|
+
<SelectContent>
|
|
315
|
+
{operators.map((one) => (
|
|
316
|
+
<SelectItem key={one} value={one}>
|
|
317
|
+
{t(`operator.${one}`, operatorFallbacks[one]) ?? one}
|
|
318
|
+
</SelectItem>
|
|
319
|
+
))}
|
|
320
|
+
</SelectContent>
|
|
321
|
+
</Select>
|
|
322
|
+
{path.kind === 'relationship' && valueShapeOf(operator) === 'scalar' ? (
|
|
323
|
+
// The form's picker rather than a box for typing a uuid; `in` and `not_in` keep the token list because the picker settles on one id.
|
|
324
|
+
<RelationshipControl
|
|
325
|
+
field={path.field}
|
|
326
|
+
values={{}}
|
|
327
|
+
setValue={() => {}}
|
|
328
|
+
kayan={path.kayan}
|
|
329
|
+
client={client}
|
|
330
|
+
api={api}
|
|
331
|
+
t={t}
|
|
332
|
+
id={`filter-${orIndex}-${andIndex}`}
|
|
333
|
+
value={row.value}
|
|
334
|
+
readOnly={false}
|
|
335
|
+
onChange={(next) => {
|
|
336
|
+
if (isNoOpConditionValueUpdate(row.value, next)) return
|
|
337
|
+
write({ path: path.path, operator, value: next })
|
|
338
|
+
}}
|
|
339
|
+
/>
|
|
340
|
+
) : (
|
|
341
|
+
<ValueInput
|
|
342
|
+
id={`filter-${orIndex}-${andIndex}`}
|
|
343
|
+
locale={locale}
|
|
344
|
+
path={path}
|
|
345
|
+
t={t}
|
|
346
|
+
operator={operator}
|
|
347
|
+
value={row.value}
|
|
348
|
+
onCommit={(next) => {
|
|
349
|
+
if (isNoOpConditionValueUpdate(row.value, next)) return
|
|
350
|
+
write({ path: path.path, operator, value: next })
|
|
351
|
+
}}
|
|
352
|
+
/>
|
|
353
|
+
)}
|
|
354
|
+
<Button type="button" variant="outline" size="sm" onClick={() => commit(virtual ? [] : removeCondition({ conditions, orIndex, andIndex }))}>
|
|
355
|
+
{t('filter.remove', 'Remove') ?? 'Remove'}
|
|
356
|
+
</Button>
|
|
357
|
+
</div>
|
|
358
|
+
)
|
|
359
|
+
})}
|
|
360
|
+
<div>
|
|
361
|
+
<Button type="button" variant="outline" size="sm" onClick={add}>
|
|
362
|
+
{t('filter.add', 'Add filter') ?? 'Add filter'}
|
|
363
|
+
</Button>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
// mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 8606b5fd4339471a01d4f8ff1c783d4c79ae3c0f92bd195586d04260a77a9ea0
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactElement } from 'react'
|
|
2
|
+
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { DirectionalIcon } from '@/components/ui/directional-icon'
|
|
5
|
+
import { Input } from '@/components/ui/input'
|
|
6
|
+
import { Label } from '@/components/ui/label'
|
|
7
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
8
|
+
import type { Translate } from './controls.tsx'
|
|
9
|
+
import { useScreen } from 'mithaq/admin/react'
|
|
10
|
+
import { limitsUpTo } from './list-query.ts'
|
|
11
|
+
|
|
12
|
+
/** As much of one page as the pager reads. */
|
|
13
|
+
export type PageInfo = {
|
|
14
|
+
hasNextPage: boolean
|
|
15
|
+
hasPrevPage: boolean
|
|
16
|
+
limit: number
|
|
17
|
+
nextPage: number | null
|
|
18
|
+
page: number
|
|
19
|
+
prevPage: number | null
|
|
20
|
+
totalDocs: number
|
|
21
|
+
totalPages: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type PaginationProps = {
|
|
25
|
+
page: PageInfo
|
|
26
|
+
onPage: (page: number) => void
|
|
27
|
+
/** A new page size. The host resets the page to one, as it does for a filter. */
|
|
28
|
+
onLimit: (limit: number) => void
|
|
29
|
+
t: Translate
|
|
30
|
+
locale: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The trigger is a button, not a native select, so the label points at it by id; one pager renders per screen.
|
|
34
|
+
const perPageId = 'mithaq-per-page'
|
|
35
|
+
|
|
36
|
+
export function Pagination({ locale, onLimit, onPage, page, t }: PaginationProps): ReactElement {
|
|
37
|
+
const [typed, setTyped] = useState(String(page.page))
|
|
38
|
+
useEffect(() => setTyped(String(page.page)), [page.page])
|
|
39
|
+
const { maxLimit } = useScreen()
|
|
40
|
+
|
|
41
|
+
const clamp = () => {
|
|
42
|
+
const parsed = Number.parseInt(typed, 10)
|
|
43
|
+
const within = Number.isInteger(parsed) ? Math.min(Math.max(parsed, 1), Math.max(page.totalPages, 1)) : page.page
|
|
44
|
+
setTyped(String(within))
|
|
45
|
+
if (within !== page.page) onPage(within)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const from = page.totalDocs === 0 ? 0 : (page.page - 1) * page.limit + 1
|
|
49
|
+
const to = Math.min(page.page * page.limit, page.totalDocs)
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<footer className="flex flex-wrap items-center justify-between gap-x-8 gap-y-3 px-2">
|
|
53
|
+
<span className="text-muted-foreground flex flex-1 text-sm whitespace-nowrap" data-mithaq-range={`${from}-${to}/${page.totalDocs}`}>
|
|
54
|
+
{from.toLocaleString(locale)}–{to.toLocaleString(locale)} {t('admin.of', 'of') ?? 'of'} {page.totalDocs.toLocaleString(locale)}
|
|
55
|
+
</span>
|
|
56
|
+
{/* `contents` on a phone, so the size box joins the range's line and the rest wraps. */}
|
|
57
|
+
<div className="ms-auto flex items-center gap-8 max-sm:contents">
|
|
58
|
+
<div className="flex items-center gap-2">
|
|
59
|
+
<Label className="text-sm font-medium" htmlFor={perPageId}>
|
|
60
|
+
{t('admin.perPage', 'Per page') ?? 'Per page'}
|
|
61
|
+
</Label>
|
|
62
|
+
{/* The size is the trigger's own child: the item list mounts only while open, so a server render would show an empty trigger. */}
|
|
63
|
+
<Select value={String(page.limit)} onValueChange={(next: string | null) => next && onLimit(Number(next))}>
|
|
64
|
+
<SelectTrigger id={perPageId} size="sm" className="h-8 w-[70px] max-sm:data-[size=sm]:h-11">
|
|
65
|
+
<SelectValue>{page.limit}</SelectValue>
|
|
66
|
+
</SelectTrigger>
|
|
67
|
+
<SelectContent side="top">
|
|
68
|
+
{limitsUpTo(maxLimit).map((one) => (
|
|
69
|
+
<SelectItem key={one} value={String(one)}>
|
|
70
|
+
{one}
|
|
71
|
+
</SelectItem>
|
|
72
|
+
))}
|
|
73
|
+
</SelectContent>
|
|
74
|
+
</Select>
|
|
75
|
+
</div>
|
|
76
|
+
<div className="flex w-fit items-center justify-center gap-2 text-sm font-medium">
|
|
77
|
+
<Input
|
|
78
|
+
className="h-8 w-16 text-center max-sm:h-11"
|
|
79
|
+
type="number"
|
|
80
|
+
min={1}
|
|
81
|
+
max={Math.max(page.totalPages, 1)}
|
|
82
|
+
value={typed}
|
|
83
|
+
aria-label={t('admin.page', 'Page') ?? 'Page'}
|
|
84
|
+
onChange={(event) => setTyped(event.target.value)}
|
|
85
|
+
onBlur={clamp}
|
|
86
|
+
onKeyDown={(event) => {
|
|
87
|
+
if (event.key === 'Enter') clamp()
|
|
88
|
+
if (event.key === 'Escape') setTyped(String(page.page))
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
<span className="text-muted-foreground">
|
|
92
|
+
{t('admin.of', 'of') ?? 'of'} {Math.max(page.totalPages, 1).toLocaleString(locale)}
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="ms-auto flex items-center gap-2 sm:ms-0">
|
|
96
|
+
<Button
|
|
97
|
+
type="button"
|
|
98
|
+
variant="outline"
|
|
99
|
+
size="icon"
|
|
100
|
+
className="hidden size-8 sm:flex"
|
|
101
|
+
disabled={!page.hasPrevPage}
|
|
102
|
+
onClick={() => onPage(1)}
|
|
103
|
+
>
|
|
104
|
+
<span className="sr-only">{t('admin.first', 'First') ?? 'First'}</span>
|
|
105
|
+
<DirectionalIcon>
|
|
106
|
+
<ChevronsLeft />
|
|
107
|
+
</DirectionalIcon>
|
|
108
|
+
</Button>
|
|
109
|
+
<Button
|
|
110
|
+
type="button"
|
|
111
|
+
variant="outline"
|
|
112
|
+
size="icon"
|
|
113
|
+
className="size-8 max-sm:size-11"
|
|
114
|
+
disabled={!page.hasPrevPage}
|
|
115
|
+
onClick={() => onPage(page.prevPage ?? 1)}
|
|
116
|
+
>
|
|
117
|
+
<span className="sr-only">{t('admin.previous', 'Previous') ?? 'Previous'}</span>
|
|
118
|
+
<DirectionalIcon>
|
|
119
|
+
<ChevronLeft />
|
|
120
|
+
</DirectionalIcon>
|
|
121
|
+
</Button>
|
|
122
|
+
<Button
|
|
123
|
+
type="button"
|
|
124
|
+
variant="outline"
|
|
125
|
+
size="icon"
|
|
126
|
+
className="size-8 max-sm:size-11"
|
|
127
|
+
disabled={!page.hasNextPage}
|
|
128
|
+
onClick={() => onPage(page.nextPage ?? page.page)}
|
|
129
|
+
>
|
|
130
|
+
<span className="sr-only">{t('admin.next', 'Next') ?? 'Next'}</span>
|
|
131
|
+
<DirectionalIcon>
|
|
132
|
+
<ChevronRight />
|
|
133
|
+
</DirectionalIcon>
|
|
134
|
+
</Button>
|
|
135
|
+
<Button
|
|
136
|
+
type="button"
|
|
137
|
+
variant="outline"
|
|
138
|
+
size="icon"
|
|
139
|
+
className="hidden size-8 sm:flex"
|
|
140
|
+
disabled={!page.hasNextPage}
|
|
141
|
+
onClick={() => onPage(Math.max(page.totalPages, 1))}
|
|
142
|
+
>
|
|
143
|
+
<span className="sr-only">{t('admin.last', 'Last') ?? 'Last'}</span>
|
|
144
|
+
<DirectionalIcon>
|
|
145
|
+
<ChevronsRight />
|
|
146
|
+
</DirectionalIcon>
|
|
147
|
+
</Button>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</footer>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
// mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 78d68b28893401072ea1b499a0b3c22588ded882bd7964122ccbb0048698d373
|