create-mithaq 0.1.1 → 0.2.0

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.
Files changed (348) hide show
  1. package/LICENSES/openstatus-time-picker.txt +21 -0
  2. package/index.js +457 -63
  3. package/package.json +1 -1
  4. package/template/apps/{asas-platform → asas-backoffice}/package.json +5 -3
  5. package/template/apps/{asas-platform → asas-backoffice}/src/api.ts +1 -1
  6. package/template/apps/{asas-platform → asas-backoffice}/src/auth-frame.tsx +12 -12
  7. package/template/apps/asas-backoffice/src/components.ts +35 -0
  8. package/template/apps/asas-backoffice/src/first-user.ts +14 -0
  9. package/template/apps/{asas-platform → asas-backoffice}/src/links.ts +1 -1
  10. package/template/apps/{asas-platform → asas-backoffice}/src/locale-switch.tsx +7 -5
  11. package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
  12. package/template/apps/asas-backoffice/src/redirect.ts +1 -0
  13. package/template/apps/{asas-platform → asas-backoffice}/src/request-headers.ts +1 -1
  14. package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
  15. package/template/apps/asas-backoffice/src/router.tsx +18 -0
  16. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
  17. package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/screen.json +426 -64
  18. package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/types.ts +18 -0
  19. package/template/apps/{asas-platform/src/routes/(screens)/platform.$.tsx → asas-backoffice/src/routes/(screens)/backoffice.$.tsx} +4 -4
  20. package/template/apps/{asas-platform/src/routes/(screens)/platform.index.tsx → asas-backoffice/src/routes/(screens)/backoffice.index.tsx} +3 -3
  21. package/template/apps/{asas-platform/src/routes/(screens)/platform.tsx → asas-backoffice/src/routes/(screens)/backoffice.tsx} +3 -3
  22. package/template/apps/{asas-platform → asas-backoffice}/src/routes/__root.tsx +2 -2
  23. package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
  24. package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
  25. package/template/apps/{asas-platform → asas-backoffice}/src/routes/auth.sign-in.tsx +9 -4
  26. package/template/apps/{asas-platform → asas-backoffice}/src/routes/unauthorized.tsx +24 -12
  27. package/template/apps/{asas-platform → asas-backoffice}/src/screen-form.tsx +5 -3
  28. package/template/apps/{asas-platform → asas-backoffice}/src/screen-list.tsx +6 -5
  29. package/template/apps/{asas-platform → asas-backoffice}/src/screen-names.ts +1 -1
  30. package/template/apps/{asas-platform → asas-backoffice}/src/screens.tsx +46 -26
  31. package/template/apps/asas-backoffice/src/server.ts +30 -0
  32. package/template/apps/{asas-platform → asas-backoffice}/src/slide.ts +1 -1
  33. package/template/apps/{asas-platform → asas-backoffice}/src/start.ts +1 -1
  34. package/template/apps/{asas-platform → asas-backoffice}/src/styles.css +9 -7
  35. package/template/apps/{asas-platform → asas-backoffice}/tsconfig.json +2 -2
  36. package/template/apps/{asas-platform → asas-backoffice}/vite.config.ts +7 -1
  37. package/template/apps/asas-tanstack/package.json +4 -2
  38. package/template/apps/asas-tanstack/src/api.ts +1 -1
  39. package/template/apps/asas-tanstack/src/auth-frame.tsx +12 -12
  40. package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
  41. package/template/apps/asas-tanstack/src/components.ts +35 -0
  42. package/template/apps/asas-tanstack/src/copy.ts +416 -79
  43. package/template/apps/asas-tanstack/src/first-run.ts +16 -0
  44. package/template/apps/asas-tanstack/src/links.ts +1 -1
  45. package/template/apps/asas-tanstack/src/locale-switch.tsx +7 -5
  46. package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
  47. package/template/apps/asas-tanstack/src/redirect.ts +20 -1
  48. package/template/apps/asas-tanstack/src/request-headers.ts +1 -1
  49. package/template/apps/asas-tanstack/src/routeTree.gen.ts +42 -0
  50. package/template/apps/asas-tanstack/src/router.tsx +12 -1
  51. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +9 -2
  52. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +475 -116
  53. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +4 -2
  54. package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +7 -2
  55. package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +83 -319
  56. package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +3 -1
  57. package/template/apps/asas-tanstack/src/routes/__root.tsx +1 -1
  58. package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +14 -7
  59. package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
  60. package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +2 -2
  61. package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +5 -5
  62. package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +7 -100
  63. package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
  64. package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +27 -15
  65. package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +59 -38
  66. package/template/apps/asas-tanstack/src/routes/index.tsx +18 -14
  67. package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +24 -12
  68. package/template/apps/asas-tanstack/src/screen-form.tsx +5 -3
  69. package/template/apps/asas-tanstack/src/screen-list.tsx +6 -5
  70. package/template/apps/asas-tanstack/src/screen-names.ts +1 -1
  71. package/template/apps/asas-tanstack/src/screens.tsx +46 -26
  72. package/template/apps/asas-tanstack/src/server.ts +10 -0
  73. package/template/apps/asas-tanstack/src/slide.ts +1 -1
  74. package/template/apps/asas-tanstack/src/styles.css +7 -3
  75. package/template/apps/asas-tanstack/tsconfig.json +2 -2
  76. package/template/apps/asas-tanstack/vite.config.ts +7 -1
  77. package/template/package.json +3 -3
  78. package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
  79. package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
  80. package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
  81. package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
  82. package/template/templates/asas/.env.example +22 -5
  83. package/template/templates/asas/AGENTS.md +101 -0
  84. package/template/templates/asas/CLAUDE.md +1 -0
  85. package/template/templates/asas/README.md +1056 -0
  86. package/template/templates/asas/package.json +19 -4
  87. package/template/templates/asas/src/asas.ts +81 -56
  88. package/template/templates/asas/src/{platform-http.ts → backoffice-http.ts} +48 -27
  89. package/template/templates/asas/src/dev-setup.ts +112 -0
  90. package/template/templates/asas/src/local-database.ts +22 -0
  91. package/template/templates/asas/src/messages/account.json +129 -103
  92. package/template/templates/asas/src/messages/auditLog.json +223 -105
  93. package/template/templates/asas/src/messages/backofficeLog.json +132 -0
  94. package/template/templates/asas/src/messages/courses.json +256 -214
  95. package/template/templates/asas/src/messages/enrollments.json +45 -27
  96. package/template/templates/asas/src/messages/invitation.json +72 -56
  97. package/template/templates/asas/src/messages/localUsers.json +34 -28
  98. package/template/templates/asas/src/messages/media.json +31 -21
  99. package/template/templates/asas/src/messages/member.json +59 -39
  100. package/template/templates/asas/src/messages/memberRoles.json +37 -31
  101. package/template/templates/asas/src/messages/mithaq.json +206 -162
  102. package/template/templates/asas/src/messages/organization.json +63 -41
  103. package/template/templates/asas/src/messages/organizationRole.json +59 -47
  104. package/template/templates/asas/src/messages/roles.json +59 -45
  105. package/template/templates/asas/src/messages/session.json +99 -79
  106. package/template/templates/asas/src/messages/sessions.json +110 -80
  107. package/template/templates/asas/src/messages/settings.json +51 -53
  108. package/template/templates/asas/src/messages/tags.json +31 -21
  109. package/template/templates/asas/src/messages/user.json +109 -87
  110. package/template/templates/asas/src/messages/verification.json +59 -47
  111. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
  112. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
  113. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
  114. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
  115. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
  116. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
  117. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
  118. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
  119. package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
  120. package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
  121. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
  122. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
  123. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
  124. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
  125. package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
  126. package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
  127. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
  128. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
  129. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
  130. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
  131. package/template/templates/asas/src/migrations/schema.ts +1 -0
  132. package/template/templates/asas/src/mithaq.ts +4 -406
  133. package/template/templates/asas/src/organizations.ts +3 -0
  134. package/template/templates/asas/src/seed.ts +147 -45
  135. package/template/templates/asas/src/{ui-platform → ui-backoffice}/home.tsx +9 -9
  136. package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
  137. package/template/templates/asas/src/{ui-platform → ui-backoffice}/record-page.tsx +4 -4
  138. package/template/templates/asas/src/{ui-platform → ui-backoffice}/role-cells.tsx +13 -6
  139. package/template/templates/asas/src/{ui-platform → ui-backoffice}/user-actions.tsx +46 -48
  140. package/template/templates/asas/src/ui-dashboard/dashboard.tsx +10 -4
  141. package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
  142. package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
  143. package/template/templates/asas/src/ui-dashboard/members.tsx +637 -438
  144. package/template/templates/asas/src/ui-dashboard/versions.tsx +22 -8
  145. package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
  146. package/template/templates/asas/src/ui-portal/home.tsx +9 -13
  147. package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
  148. package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
  149. package/template/templates/features/audit-log/index.ts +4 -0
  150. package/template/templates/features/audit-log/package.json +13 -0
  151. package/template/templates/features/auth/index.ts +11 -0
  152. package/template/templates/{asas/src → features/auth}/local-users/local-users.ts +7 -7
  153. package/template/templates/{auth → features/auth}/package.json +2 -3
  154. package/template/templates/{asas/src/kayan → features}/auth/user/account.access.ts +2 -2
  155. package/template/templates/{asas/src/kayan → features}/auth/user/account.ts +2 -2
  156. package/template/templates/{asas/src/kayan → features}/auth/user/invitation.access.ts +1 -1
  157. package/template/templates/{asas/src/kayan → features}/auth/user/invitation.ts +3 -3
  158. package/template/templates/{asas/src/kayan → features}/auth/user/member.access.ts +4 -4
  159. package/template/templates/{asas/src/kayan → features}/auth/user/member.screens.ts +5 -5
  160. package/template/templates/{asas/src/kayan → features}/auth/user/member.ts +4 -3
  161. package/template/templates/{asas/src/kayan → features}/auth/user/organization.access.ts +6 -6
  162. package/template/templates/{asas/src/kayan → features}/auth/user/organization.screens.ts +6 -6
  163. package/template/templates/{asas/src/kayan → features}/auth/user/organization.ts +2 -1
  164. package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.access.ts +2 -2
  165. package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.ts +3 -3
  166. package/template/templates/{asas/src/kayan → features}/auth/user/session.access.ts +2 -2
  167. package/template/templates/{asas/src/kayan → features}/auth/user/session.ts +2 -2
  168. package/template/templates/{asas/src/kayan → features}/auth/user/user.access.ts +8 -7
  169. package/template/templates/{asas/src/kayan → features}/auth/user/user.screens.ts +9 -9
  170. package/template/templates/{asas/src/kayan → features}/auth/user/user.ts +2 -2
  171. package/template/templates/{asas/src/kayan → features}/auth/user/verification.access.ts +2 -2
  172. package/template/templates/{asas/src/kayan → features}/auth/user/verification.ts +2 -2
  173. package/template/templates/{asas/src → features/courses}/courses/courses.ts +122 -133
  174. package/template/templates/features/courses/courses/cover-reads.ts +33 -0
  175. package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
  176. package/template/templates/{asas/src → features/courses}/courses/ui/fee-cell.tsx +1 -1
  177. package/template/templates/{asas/src → features/courses}/courses/ui/status-cell.tsx +5 -3
  178. package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
  179. package/template/templates/features/courses/domain/archive.ts +39 -0
  180. package/template/templates/features/courses/domain/on-offer.ts +6 -0
  181. package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
  182. package/template/templates/{asas/src → features/courses}/enrollments/ui/session-cell.tsx +17 -7
  183. package/template/templates/{asas/src → features/courses/enrollments}/ui/session-labels.tsx +3 -3
  184. package/template/templates/features/courses/index.ts +8 -0
  185. package/template/templates/features/courses/package.json +24 -0
  186. package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
  187. package/template/templates/features/courses/portal/ui/course.tsx +176 -0
  188. package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
  189. package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
  190. package/template/templates/features/courses/portal/ui/reads.ts +76 -0
  191. package/template/templates/features/courses/portal/ui/rules.ts +87 -0
  192. package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
  193. package/template/templates/features/courses/sessions/seats.ts +42 -0
  194. package/template/templates/{asas/src → features/courses}/sessions/sessions.ts +45 -14
  195. package/template/templates/features/media/index.ts +10 -0
  196. package/template/templates/{asas/src → features/media}/media/media.ts +34 -40
  197. package/template/templates/{asas/src → features/media}/media/ui/preview.tsx +2 -2
  198. package/template/templates/features/media/package.json +19 -0
  199. package/template/templates/features/roles/index.ts +4 -0
  200. package/template/templates/{asas/src → features/roles}/member-roles/member-roles.ts +15 -15
  201. package/template/templates/features/roles/package.json +21 -0
  202. package/template/templates/{asas/src → features/roles}/roles/roles.ts +23 -23
  203. package/template/templates/{asas/src → features/roles}/roles/ui/role-form.tsx +65 -24
  204. package/template/templates/{asas/src → features/roles}/roles/ui/roles.tsx +49 -25
  205. package/template/templates/features/settings/index.ts +3 -0
  206. package/template/templates/features/settings/package.json +13 -0
  207. package/template/templates/{asas/src → features/settings}/settings/settings.ts +12 -17
  208. package/template/templates/features/tags/index.ts +3 -0
  209. package/template/templates/features/tags/package.json +14 -0
  210. package/template/templates/{asas/src → features/tags}/tags/tags.ts +15 -15
  211. package/template/templates/mithaq/package.json +33 -0
  212. package/template/templates/mithaq/src/mail/index.ts +32 -0
  213. package/template/templates/mithaq/src/mail/mailers.ts +36 -0
  214. package/template/templates/{asas/src/audit-log/sink.ts → mithaq/src/mithaq/audit-sink.ts} +31 -9
  215. package/template/templates/{auth/src/platform-roles.ts → mithaq/src/mithaq/auth/backoffice-roles.ts} +44 -23
  216. package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
  217. package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
  218. package/template/templates/{asas/src → mithaq/src/mithaq}/pages.ts +16 -7
  219. package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
  220. package/template/templates/mithaq/src/mithaq.ts +593 -0
  221. package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
  222. package/template/templates/{asas → mithaq}/src/roles/grants.ts +1 -1
  223. package/template/templates/mithaq/src/roles/member-access.ts +11 -0
  224. package/template/templates/{asas/src → mithaq/src/roles}/statements.ts +6 -4
  225. package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
  226. package/template/templates/{asas/src → mithaq/src/roles}/tenant-access.ts +96 -57
  227. package/template/templates/{asas/src → mithaq/src/roles}/tenant-roles.ts +84 -25
  228. package/template/templates/{asas → mithaq}/src/steps/expected-updated-at.ts +2 -2
  229. package/template/templates/{asas → mithaq}/src/storage/index.ts +3 -1
  230. package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
  231. package/template/templates/mithaq/src/ui/activity.tsx +527 -0
  232. package/template/templates/{asas → mithaq}/src/ui/app-shell.tsx +27 -19
  233. package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
  234. package/template/templates/{asas → mithaq}/src/ui/screen-nav.tsx +1 -1
  235. package/template/templates/{asas → mithaq}/src/ui/top-shell.tsx +8 -8
  236. package/template/apps/asas-platform/src/components/mithaq/cells.tsx +0 -112
  237. package/template/apps/asas-platform/src/components/mithaq/controls.tsx +0 -650
  238. package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +0 -53
  239. package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +0 -108
  240. package/template/apps/asas-platform/src/components/mithaq/form.tsx +0 -1467
  241. package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +0 -108
  242. package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +0 -368
  243. package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +0 -153
  244. package/template/apps/asas-platform/src/components/mithaq/list-query.ts +0 -432
  245. package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +0 -74
  246. package/template/apps/asas-platform/src/components/mithaq/list.tsx +0 -865
  247. package/template/apps/asas-platform/src/components/mithaq/nav.tsx +0 -871
  248. package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +0 -1024
  249. package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +0 -127
  250. package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +0 -400
  251. package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +0 -6
  252. package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +0 -1083
  253. package/template/apps/asas-platform/src/components/mithaq/upload.tsx +0 -686
  254. package/template/apps/asas-platform/src/components/ui/drawer.tsx +0 -216
  255. package/template/apps/asas-platform/src/redirect.ts +0 -1
  256. package/template/apps/asas-platform/src/routeTree.gen.ts +0 -181
  257. package/template/apps/asas-platform/src/router.tsx +0 -7
  258. package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +0 -4
  259. package/template/apps/asas-platform/src/routes/api/$.ts +0 -30
  260. package/template/apps/asas-platform/src/server.ts +0 -20
  261. package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +0 -112
  262. package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +0 -650
  263. package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +0 -53
  264. package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +0 -108
  265. package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +0 -1467
  266. package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +0 -108
  267. package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +0 -368
  268. package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +0 -153
  269. package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +0 -432
  270. package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +0 -74
  271. package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +0 -865
  272. package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +0 -871
  273. package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +0 -1024
  274. package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +0 -127
  275. package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +0 -400
  276. package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +0 -6
  277. package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +0 -1083
  278. package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +0 -686
  279. package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +0 -215
  280. package/template/templates/asas/src/audit-log/audit-log.ts +0 -187
  281. package/template/templates/asas/src/audit-log/ui/activity.tsx +0 -243
  282. package/template/templates/asas/src/courses/ui/archive-button.tsx +0 -25
  283. package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +0 -11
  284. package/template/templates/asas/src/courses/ui/on-offer.ts +0 -6
  285. package/template/templates/asas/src/enrollments/enrollments.ts +0 -137
  286. package/template/templates/asas/src/rich-text-nodes.ts +0 -11
  287. package/template/templates/asas/src/roles.ts +0 -16
  288. package/template/templates/asas/src/ui/rich-text-nodes.tsx +0 -15
  289. package/template/templates/asas/src/ui-platform/organization-actions.tsx +0 -114
  290. package/template/templates/auth/src/index.ts +0 -690
  291. /package/template/apps/{asas-platform → asas-backoffice}/components.json +0 -0
  292. /package/template/apps/{asas-platform → asas-backoffice}/gitignore +0 -0
  293. /package/template/apps/{asas-platform → asas-backoffice}/public/favicon.svg +0 -0
  294. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/accordion.tsx +0 -0
  295. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/alert-dialog.tsx +0 -0
  296. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/avatar.tsx +0 -0
  297. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/badge.tsx +0 -0
  298. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/breadcrumb.tsx +0 -0
  299. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button-group.tsx +0 -0
  300. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button.tsx +0 -0
  301. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/calendar.tsx +0 -0
  302. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/card.tsx +0 -0
  303. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/chart.tsx +0 -0
  304. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/checkbox.tsx +0 -0
  305. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/collapsible.tsx +0 -0
  306. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/combobox.tsx +0 -0
  307. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dialog.tsx +0 -0
  308. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/direction.tsx +0 -0
  309. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/directional-icon.tsx +0 -0
  310. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dropdown-menu.tsx +0 -0
  311. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/field.tsx +0 -0
  312. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-group.tsx +0 -0
  313. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-otp.tsx +0 -0
  314. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input.tsx +0 -0
  315. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/label.tsx +0 -0
  316. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/popover.tsx +0 -0
  317. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/radio-group.tsx +0 -0
  318. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/resizable.tsx +0 -0
  319. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/scroll-area.tsx +0 -0
  320. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/select.tsx +0 -0
  321. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/separator.tsx +0 -0
  322. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sheet.tsx +0 -0
  323. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sidebar.tsx +0 -0
  324. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/skeleton.tsx +0 -0
  325. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/slider.tsx +0 -0
  326. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/spinner.tsx +0 -0
  327. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/switch.tsx +0 -0
  328. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/table.tsx +0 -0
  329. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tabs.tsx +0 -0
  330. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/textarea.tsx +0 -0
  331. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle-group.tsx +0 -0
  332. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle.tsx +0 -0
  333. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tooltip.tsx +0 -0
  334. /package/template/apps/{asas-platform → asas-backoffice}/src/copy.ts +0 -0
  335. /package/template/apps/{asas-platform → asas-backoffice}/src/hooks/use-mobile.ts +0 -0
  336. /package/template/apps/{asas-platform → asas-backoffice}/src/lib/utils.ts +0 -0
  337. /package/template/apps/{asas-platform → asas-backoffice}/src/theme.ts +0 -0
  338. /package/template/templates/{asas/src/kayan → features}/auth/user/index.ts +0 -0
  339. /package/template/templates/{asas/src → features/courses}/courses/ui/no-courses.tsx +0 -0
  340. /package/template/templates/{asas → mithaq}/components.json +0 -0
  341. /package/template/templates/{asas → mithaq}/src/storage/defaults.ts +0 -0
  342. /package/template/templates/{asas → mithaq}/src/storage/local.ts +0 -0
  343. /package/template/templates/{asas → mithaq}/src/storage/r2.ts +0 -0
  344. /package/template/templates/{asas → mithaq}/src/storage/s3.ts +0 -0
  345. /package/template/templates/{asas → mithaq}/src/storage/uploadthing.ts +0 -0
  346. /package/template/templates/{asas → mithaq}/src/ui/kpi-card.tsx +0 -0
  347. /package/template/templates/{asas → mithaq}/src/ui/record-page.tsx +0 -0
  348. /package/template/templates/{asas → mithaq}/src/ui/table-page.tsx +0 -0
@@ -1,432 +0,0 @@
1
- import { isDecimal, type ClientField, type ClientKayan } from 'mithaq/admin'
2
- import { filterPathsOf, offers, valueShapeOf, type FilterContext, type FilterPath } from './filter-operators.ts'
3
-
4
- // The reader is a trust boundary: any bad key, field, operator or value refuses the whole URL (`QueryRefused`),
5
- // and a link naming a field the viewer may not read is refused, never read as a smaller filter.
6
-
7
- /** One row of the panel: a path, an operator and its value. */
8
- export type Condition = Record<string, Record<string, unknown>>
9
-
10
- /** The panel's state, two levels deep and never deeper: a disjunction of conjunctions, the shape of a `where` the server takes. */
11
- export type FilterWhere = { or: { and: Condition[] }[] }
12
-
13
- /** What the screen drives `find` with, and what the URL carries. */
14
- export type ListQuery = {
15
- where?: FilterWhere
16
- search?: string
17
- sort?: string
18
- page?: number
19
- limit?: number
20
- /** The column accessor, prefixed `-` when the column is hidden. */
21
- cols?: readonly string[]
22
- }
23
-
24
- /** One column as the popup holds it, which is what the `-` prefix encodes. */
25
- export type ColumnChoice = { name: string; active: boolean }
26
-
27
- /** The URL's column list read back into choices; a `-` prefix marks a hidden column. */
28
- export const columnsFrom = (cols: readonly string[] | undefined): ColumnChoice[] =>
29
- (cols ?? []).map((one) => (one.startsWith('-') ? { name: one.slice(1), active: false } : { name: one, active: true }))
30
-
31
- /** The choices written back as the URL's column list. */
32
- export const columnsTo = (columns: readonly ColumnChoice[]): string[] =>
33
- columns.map((one) => (one.active ? one.name : `-${one.name}`))
34
-
35
- /** The page sizes the screen offers. The first is the one a list opens with. */
36
- export const limits = [10, 25, 50, 100] as const
37
-
38
- /**
39
- * The page sizes a build whose served read answers at most `maxLimit` rows
40
- * offers: the ones up to it, since a larger one is a 400, or the maximum alone
41
- * where none of them fits.
42
- */
43
- export const limitsUpTo = (maxLimit: number): readonly number[] => {
44
- const fit = limits.filter((one) => one <= maxLimit)
45
- return fit.length ? fit : [maxLimit]
46
- }
47
-
48
- const whole = (value: unknown) => {
49
- const parsed = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10)
50
- return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
51
- }
52
-
53
- /** Why a URL was refused, which the host shows in place of the rows it never asked for. */
54
- export class QueryRefused extends Error {
55
- override name = 'QueryRefused'
56
- }
57
- function refuse(sentence: string): never {
58
- throw new QueryRefused(`This link ${sentence}`)
59
- }
60
-
61
- /** Whether a field can order a page: the same questions the read answers before it takes a sort. */
62
- export const canSort = (field: ClientField) =>
63
- field.rules.localized !== true && field.hasMany !== true && field.kind !== 'join' && field.sortable !== false
64
-
65
- /** The fields a list may be sorted by. */
66
- export const sortableOf = (kayan: { fields: readonly ClientField[] }) => kayan.fields.filter(canSort).map((field) => field.name)
67
-
68
- /**
69
- * What a URL is read against: the paths the list filters by, the fields it
70
- * sorts by, and the list node's own page size, which is one the URL may name
71
- * whether or not the pager offers it.
72
- */
73
- export type ListRules = { paths: readonly FilterPath[]; sortable: readonly string[]; limit?: number; maxLimit: number }
74
-
75
- /** What a list node says about its query: what it filters and searches by, and the sort and limit it opens with. */
76
- export type ListDefaults = { filters?: readonly string[]; search?: readonly string[]; sort?: string; limit?: number }
77
-
78
- /** The rules one list node reads its URL by. */
79
- export const rulesOf = ({ kayan, node, maxLimit, context = {} }: { kayan: ClientKayan; node: ListDefaults | undefined; maxLimit: number; context?: FilterContext }): ListRules => ({
80
- paths: filterPathsOf(kayan, { ...context, ...(node?.filters && { filters: node.filters }) }),
81
- sortable: sortableOf(kayan),
82
- maxLimit,
83
- ...(node?.limit !== undefined && { limit: node.limit }),
84
- })
85
-
86
- /** A `where` in `qs`'s bracket form: `where[or][0][and][0][title][like]`. */
87
- const bracketed = /^where((?:\[[^[\]]*\])+)$/
88
- /** `where[or][0][and][0][path][operator][index]`: seven segments and never more. */
89
- const depth = 7
90
- /** A `where` is at most this many values, which is far past what the panel writes. */
91
- const most = 64
92
- /** The three keys that reach an object's prototype, which a `where` never names. */
93
- const unsafe = ['__proto__', 'constructor', 'prototype']
94
-
95
- /** The `where` pairs of a URL built into the object they spell; anything `qs` would silently repair is refused. */
96
- const treeOf = (pairs: readonly [string, string][]): unknown => {
97
- if (pairs.length > most) refuse(`carries more than ${most} where values.`)
98
- const tree: Record<string, unknown> = {}
99
- for (const [key, value] of pairs) {
100
- const segments = [...(bracketed.exec(key)?.[1] ?? refuse('carries a where that is not written in brackets.')).matchAll(/\[([^[\]]*)\]/g)].map((one) => one[1]!)
101
- if (segments.length > depth) refuse('is nested deeper than a where goes.')
102
- let at: Record<string, unknown> | unknown[] = tree
103
- segments.forEach((segment, index) => {
104
- if (unsafe.includes(segment)) refuse(`names "${segment}", which no where may name.`)
105
- const numbered = /^\d{1,2}$/.test(segment)
106
- if (numbered !== Array.isArray(at) || segment === '') refuse(`mixes a list and an object at "${segment}".`)
107
- const slot = at as Record<string, unknown>
108
- if (index === segments.length - 1) {
109
- if (slot[segment] !== undefined) refuse(`says "${key}" twice.`)
110
- slot[segment] = value
111
- return
112
- }
113
- slot[segment] ??= /^\d{1,2}$/.test(segments[index + 1]!) ? [] : {}
114
- if (typeof slot[segment] !== 'object') refuse(`says "${key}" twice.`)
115
- at = slot[segment] as Record<string, unknown> | unknown[]
116
- })
117
- }
118
- const gapless = (value: unknown): boolean =>
119
- !value || typeof value !== 'object' || ((!Array.isArray(value) || [...value].every((one) => one !== undefined)) && Object.values(value).every(gapless))
120
- if (!gapless(tree)) refuse('holds a gap in its list.')
121
- return tree
122
- }
123
-
124
- /**
125
- * A `where` hoisted into the panel's two levels: an `or` whose entries hold no
126
- * `and` gets one, a bare `and` is one group, and top-level siblings are one
127
- * group of their own.
128
- */
129
- const hoisted = (where: Record<string, unknown>) => {
130
- const or = where.or as Record<string, unknown>[] | undefined
131
- const and = where.and as unknown[] | undefined
132
- // `where[or][title]` builds an object where a list belongs, which the read would refuse.
133
- if (or !== undefined && !Array.isArray(or)) refuse('holds an or that is no list.')
134
- if (and !== undefined && !Array.isArray(and)) refuse('holds an and that is no list.')
135
- if (or && and) refuse('holds or and and at the root of one where.')
136
- if (or) return or.map((group) => (Array.isArray(group.and) ? group.and : [group]))
137
- if (and) return [and]
138
- return [[where]]
139
- }
140
-
141
- /**
142
- * A moment with no zone, `2026-10-07T09:30`, as the ISO instant on the reader's own clock.
143
- * Anything else (a moment with its zone, text no clock reads) is handed back for the server to judge.
144
- */
145
- export const instantOf = (text: string): string => {
146
- if (!/^\d{4}-\d\d-\d\d[T ]\d\d:\d\d(?::\d\d(?:\.\d{1,3})?)?$/.test(text)) return text
147
- const local = new Date(text.replace(' ', 'T'))
148
- return Number.isNaN(local.getTime()) ? text : local.toISOString()
149
- }
150
-
151
- /** One value in the kind of its field: the URL carries strings, and the read takes typed values. */
152
- const valueOf = (path: FilterPath, operator: string, value: unknown): unknown => {
153
- if (typeof value !== 'string') refuse(`asks field "${path.path}" for "${operator}" with a value that is no single value.`)
154
- if (path.kind === 'number' || path.kind === 'integer') {
155
- const number = Number(value)
156
- const integer = path.kind === 'integer'
157
- const held = value.trim() !== '' && (integer ? Number.isInteger(number) && number >= -2147483648 && number <= 2147483647 : Number.isFinite(number))
158
- return held ? number : refuse(`asks field "${path.path}" to equal "${value}", which is not ${integer ? 'a 32-bit integer' : 'a number'}.`)
159
- }
160
- // A decimal stays the text it is, which is the form its row holds.
161
- if (path.kind === 'decimal') return isDecimal(value) ? value : refuse(`asks field "${path.path}" to equal "${value}", which is not a decimal.`)
162
- if (path.kind === 'checkbox') return value === 'true' ? true : value === 'false' ? false : refuse(`asks field "${path.path}" to equal "${value}", which is not true or false.`)
163
- // A moment with no zone is read on the reader's own clock, not handed on for the store to read as UTC.
164
- if (path.kind === 'datetime') return instantOf(value)
165
- return value
166
- }
167
-
168
- /** A `where` off the URL, every condition checked the way the read checks one, so nothing is asked of the server that it would refuse. */
169
- const whereOf = (paths: readonly FilterPath[], tree: unknown): FilterWhere | undefined => {
170
- const where = tree as Record<string, unknown>
171
- if (!Object.keys(where).length) return undefined
172
- const groups = hoisted(where).map((conditions) => ({
173
- and: conditions.flatMap((condition) =>
174
- Object.entries(condition as Record<string, unknown>).flatMap(([name, operators]) => {
175
- const path = paths.find((one) => one.path === name) ?? refuse(`names field "${name}", which this list does not filter by.`)
176
- if (!operators || typeof operators !== 'object' || Array.isArray(operators)) refuse(`names field "${name}" with no operator.`)
177
- return Object.entries(operators as Record<string, unknown>).map(([operator, value]): Condition => {
178
- if (!offers({ paths, path: name, operator })) refuse(`asks field "${name}" for "${operator}", which it does not take.`)
179
- const shape = valueShapeOf(operator)
180
- if (shape === 'list' && !Array.isArray(value)) refuse(`asks field "${name}" for "${operator}" with one value, and "${operator}" takes a list.`)
181
- const typed =
182
- shape === 'boolean' ? valueOf({ ...path, kind: 'checkbox' }, operator, value)
183
- : shape === 'list' ? (value as unknown[]).map((one) => valueOf(path, operator, one))
184
- : valueOf(path, operator, value)
185
- return { [name]: { [operator]: typed } }
186
- })
187
- }),
188
- ),
189
- }))
190
- return { or: groups }
191
- }
192
-
193
- /** What a reader is handed: the URL's own search params, or the router's record of them. */
194
- export type QuerySource = URLSearchParams | Readonly<Record<string, unknown>>
195
-
196
- /**
197
- * The URL's list keys as the strings it carries: every `where[…]` pair,
198
- * `search`, `sort`, `page`, `limit` and `cols`. A route validates its search
199
- * into this before any kayan is in hand; `readQuery` reads it against the list.
200
- */
201
- export type RawQuery = Readonly<Record<string, string>>
202
-
203
- /** A key with a list-of-another-kayan prefix taken off (`sessions.course.page` is `page`), so two lists on one page never read each other's. */
204
- const baseOf = (key: string) => key.replace(/^[^.[\]]+\.[^.[\]]+\./, '')
205
- const listKey = (key: string) => {
206
- const base = baseOf(key)
207
- return base.startsWith('where') || ['search', 'sort', 'page', 'limit', 'cols'].includes(base)
208
- }
209
-
210
- /** The prefix one list node's keys carry in the URL: none for a page's own list, `<of>.<where>.` for a list of another kayan. */
211
- export const prefixOf = (node: { of?: string; where?: string } | undefined) => (node?.of ? `${node.of}.${node.where}.` : '')
212
-
213
- const ownKey = (key: string, prefix: string) => key.startsWith(prefix) && baseOf(key) === key.slice(prefix.length)
214
-
215
- /** One list's own keys off the URL's, its prefix taken off, which is what `readQuery` reads. */
216
- export const listRaw = (raw: RawQuery, prefix: string): RawQuery =>
217
- Object.fromEntries(Object.entries(raw).flatMap(([key, value]) => (ownKey(key, prefix) ? [[key.slice(prefix.length), value]] : [])))
218
-
219
- /** The URL's keys with one list's replaced by what it now asks, every other list's kept. */
220
- export const withList = ({ raw, prefix, written }: { raw: RawQuery; prefix: string; written: Readonly<Record<string, string>> }): Record<string, string> => ({
221
- ...Object.fromEntries(Object.entries(raw).filter(([key]) => !ownKey(key, prefix))),
222
- ...Object.fromEntries(Object.entries(written).map(([key, value]) => [`${prefix}${key}`, value])),
223
- })
224
-
225
- export const readRaw = (source: QuerySource): RawQuery =>
226
- Object.fromEntries(
227
- (source instanceof URLSearchParams ? [...source] : Object.entries(source)).filter(
228
- (pair): pair is [string, string] => listKey(pair[0]) && typeof pair[1] === 'string',
229
- ),
230
- )
231
-
232
- /**
233
- * The whole query, read against the list it is for, or `QueryRefused` for a
234
- * URL that asks for what the list does not offer.
235
- */
236
- export const readQuery = (source: QuerySource, rules: ListRules): ListQuery => {
237
- const raw = readRaw(source)
238
- const where = whereOf(rules.paths, treeOf(Object.entries(raw).filter(([key]) => key.startsWith('where'))))
239
- const { search, sort, cols } = raw
240
- if (sort && !rules.sortable.includes(sort.replace(/^-/, ''))) refuse(`sorts by "${sort.replace(/^-/, '')}", which this list does not sort by.`)
241
- const page = whole(raw.page)
242
- const limit = whole(raw.limit)
243
- const list = (cols ?? '').split(',').map((one) => one.trim()).filter(Boolean)
244
- return {
245
- ...(where && { where }),
246
- ...(search && { search }),
247
- ...(sort && { sort }),
248
- ...(page && { page }),
249
- ...(limit && (limitsUpTo(rules.maxLimit).includes(limit) || limit === rules.limit) && { limit }),
250
- ...(list.length > 0 && { cols: list }),
251
- }
252
- }
253
-
254
- /** A value's pairs in bracket form: `where[or][0][and][0][title][like]`. */
255
- const pairsOf = (prefix: string, value: unknown): [string, string][] =>
256
- value && typeof value === 'object'
257
- ? Object.entries(value).flatMap(([key, one]) => pairsOf(`${prefix}[${key}]`, one))
258
- : value === undefined ? [] : [[prefix, String(value)]]
259
-
260
- /**
261
- * The same query as a URL would carry it, one string per key and no key for a
262
- * value that is not there. Handed to `new URLSearchParams(...)` or to a
263
- * router's own `search`, which is why it is a record rather than a string.
264
- */
265
- export const writeQuery = (query: ListQuery): Record<string, string> => ({
266
- ...Object.fromEntries(pairsOf('where', query.where)),
267
- ...(query.search && { search: query.search }),
268
- ...(query.sort && { sort: query.sort }),
269
- ...(query.page && query.page > 1 && { page: String(query.page) }),
270
- ...(query.limit && { limit: String(query.limit) }),
271
- ...(query.cols?.length && { cols: query.cols.join(',') }),
272
- })
273
-
274
- /**
275
- * The same query with one part changed and the page back at one: each of
276
- * filter, search, sort and limit changes which rows are on which page, and a
277
- * page number that outlived its result is a blank table.
278
- */
279
- export const refined = (query: ListQuery, next: Partial<ListQuery>): ListQuery => ({ ...query, ...next, page: 1 })
280
-
281
- /**
282
- * The per-viewer state the URL does not carry: page size and columns, in one `localStorage` key per kayan.
283
- * Access is in a try/catch: `localStorage` throws in a private window and is absent in a server render.
284
- */
285
- export type Remembered = { limit?: number; cols?: readonly string[] }
286
-
287
- const keyFor = (kayan: string) => `mithaq.list.${kayan}`
288
-
289
- export const remembered = (kayan: string, maxLimit: number): Remembered => {
290
- try {
291
- const held = globalThis.localStorage?.getItem(keyFor(kayan))
292
- const value = held ? (JSON.parse(held) as unknown) : undefined
293
- if (!value || typeof value !== 'object') return {}
294
- const { cols, limit } = value as Remembered
295
- return {
296
- ...(typeof limit === 'number' && limitsUpTo(maxLimit).includes(limit) && { limit }),
297
- ...(Array.isArray(cols) && cols.every((one) => typeof one === 'string') && { cols }),
298
- }
299
- } catch {
300
- return {}
301
- }
302
- }
303
-
304
- export const remember = (kayan: string, value: Remembered): void => {
305
- try {
306
- globalThis.localStorage?.setItem(keyFor(kayan), JSON.stringify(value))
307
- } catch {
308
- // A viewer whose storage is full or blocked keeps the URL's answer, which
309
- // is the whole of what the screen needs to render.
310
- }
311
- }
312
-
313
- /** The half of a kayan a search reads: the two admin keys, the kinds, and where a relationship points. */
314
- export type Searchable = {
315
- name?: string
316
- useAsTitle?: string
317
- /** The rows every list of the kayan on its screen is held to. */
318
- baseFilter?: unknown
319
- fields?: readonly { name: string; kind: string; searchable?: true; relationTo?: string; rules?: { localized?: unknown } }[]
320
- }
321
-
322
- /**
323
- * Whether the viewer may read what a search path names, in this kayan or across one relationship.
324
- * A projection holds only readable fields; a path it lacks would 403 and the search box would refuse every word.
325
- */
326
- const reads = (kayan: Searchable, path: string, client: readonly Searchable[] = []) => {
327
- const [own, far] = path.split('.')
328
- const near = kayan.fields?.find((field) => field.name === own)
329
- if (!near || far === undefined) return near !== undefined
330
- return client.some((one) => one.name === near.relationTo && one.fields?.some((field) => field.name === far))
331
- }
332
-
333
- /**
334
- * The fields a search runs over: the node's `search`, else those declared `searchable`, else `useAsTitle`, else `id`.
335
- * Only the fallbacks need a text or email kind, as the read takes `like` on those alone; a uuid `id` has no search.
336
- */
337
- export const searchableFields = ({ kayan, search, client }: { kayan: Searchable; search?: readonly string[]; client?: readonly Searchable[] }): readonly string[] => {
338
- const declared = search ?? (kayan.fields ?? []).filter((field) => field.searchable).map((field) => field.name)
339
- if (declared.length) return declared.filter((path) => reads(kayan, path, client))
340
- const name = kayan.useAsTitle ?? 'id'
341
- const kind = kayan.fields?.find((field) => field.name === name)?.kind
342
- return kind === 'text' || kind === 'email' ? [name] : []
343
- }
344
-
345
- /**
346
- * A search term as a `where`: one `like` per searchable field, ORed. `like` rather than `contains` because
347
- * the drizzle store splits it on whitespace and runs one `ilike` per word.
348
- */
349
- export const searchWhere = ({
350
- kayan,
351
- search,
352
- fields = searchableFields({ kayan }),
353
- locales = [],
354
- }: {
355
- kayan: Searchable
356
- search: string | undefined
357
- fields?: readonly string[]
358
- locales?: readonly string[]
359
- }): FilterWhere | undefined => {
360
- const term = search?.trim()
361
- // A localized field is one column per locale and a `like` names one of them,
362
- // `name.ar`, so it is searched in every locale the host reads in.
363
- const paths = fields.flatMap((field) =>
364
- kayan.fields?.find((one) => one.name === field)?.rules?.localized === true ? locales.map((locale) => `${field}.${locale}`) : [field],
365
- )
366
- // A kayan with no searchable field is no filter at all, not a `where` of no
367
- // groups, which the server refuses as an empty disjunction.
368
- return term && paths.length ? { or: paths.map((field) => ({ and: [{ [field]: { like: term } }] })) } : undefined
369
- }
370
-
371
- /**
372
- * What one render hands `find`: the panel's `where`, the search's and the
373
- * screen's `baseFilter` for the kayan, ANDed in that order. One alone is passed
374
- * through rather than wrapped.
375
- */
376
- export const findWhere = ({
377
- kayan,
378
- query,
379
- search,
380
- locales,
381
- client,
382
- }: {
383
- kayan: Searchable
384
- query: ListQuery
385
- search?: readonly string[]
386
- locales?: readonly string[]
387
- client?: readonly Searchable[]
388
- }): unknown => {
389
- const searched = searchWhere({ kayan, search: query.search, fields: searchableFields({ kayan, search, client }), locales })
390
- const parts = [query.where, searched, kayan.baseFilter].filter((one) => one !== undefined)
391
- return parts.length > 1 ? { and: parts } : parts[0]
392
- }
393
-
394
- /**
395
- * What one page asks `find`: the panel's and the search's `where`, and the URL's sort and limit over the node's defaults.
396
- * Server and browser both use it, so the first page and every later one share one query and cache key.
397
- */
398
- export const findOf = ({
399
- kayan,
400
- query,
401
- node,
402
- locales,
403
- client,
404
- }: {
405
- kayan: Searchable
406
- query: ListQuery
407
- node?: ListDefaults
408
- locales?: readonly string[]
409
- client?: readonly Searchable[]
410
- }) => {
411
- const where = findWhere({ kayan, query, search: node?.search, locales, client })
412
- const sort = query.sort ?? node?.sort
413
- return {
414
- ...(where !== undefined && { where }),
415
- ...(sort && { sort }),
416
- ...(query.page && { page: query.page }),
417
- // None where neither names one, so the server's own `defaultLimit`
418
- // answers, which the build holds to its `maxLimit`.
419
- ...((query.limit ?? node?.limit) !== undefined && { limit: query.limit ?? node?.limit }),
420
- }
421
- }
422
-
423
- /**
424
- * A page's input held to the rows a list of another kayan draws: those whose
425
- * relationship holds the page's row, ANDed with whatever the URL asked. It is
426
- * part of the one input and the one cache key.
427
- */
428
- export const scopedTo = (input: Readonly<Record<string, unknown>>, where: Readonly<Record<string, unknown>>): Record<string, unknown> => ({
429
- ...input,
430
- where: input.where === undefined ? where : { and: [where, input.where] },
431
- })
432
- // mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 41bf95fa8c90940a8942f07ab193a03ad1381581c59b96c0ab92b784091895bd
@@ -1,74 +0,0 @@
1
- import type { ReactElement } from 'react'
2
- import type { ClientField, ClientKayan } from 'mithaq/admin'
3
- import { Checkbox } from '@/components/ui/checkbox'
4
- import { checkboxSize, readOnlyField, type Translate } from './controls.tsx'
5
-
6
- /**
7
- * The list's checkbox size, with the invisible hit layer grown to 44 pixels below `sm`. Only for
8
- * the list: a checkbox group's options sit 28 pixels apart, so a 44 pixel layer there would tick the option above.
9
- */
10
- export const selectBox = `${checkboxSize} after:-inset-x-3.5 max-sm:after:-inset-y-3.5`
11
-
12
- /** Which fields a bulk update may offer: the ones a person could edit on a row of this kayan. Chrome only: the runner refuses whatever the list draws. */
13
- export const writableFields = (
14
- kayan: ClientKayan,
15
- writes?: (field: string) => boolean,
16
- ): readonly ClientField[] =>
17
- kayan.fields.filter(
18
- (field) =>
19
- !readOnlyField(field, 'update')
20
- && (writes ? writes(field.name) : true),
21
- )
22
-
23
- /** The head checkbox of the selection column; reports `aria-checked="mixed"` while some of the page is ticked. */
24
- export function SelectAll({
25
- ids,
26
- selected,
27
- onSelect,
28
- t,
29
- }: {
30
- ids: readonly string[]
31
- selected: ReadonlySet<string>
32
- onSelect: (ids: ReadonlySet<string>) => void
33
- t: Translate
34
- }): ReactElement {
35
- const ticked = ids.filter((one) => selected.has(one)).length
36
- const all = ids.length > 0 && ticked === ids.length
37
- return (
38
- <Checkbox
39
- className={selectBox}
40
- checked={all}
41
- aria-checked={ticked > 0 && !all ? 'mixed' : all}
42
- aria-label={t('admin.selectAll', 'Select every row on this page') ?? 'Select every row on this page'}
43
- onCheckedChange={(checked: boolean | 'indeterminate') => onSelect(checked === true ? new Set(ids) : new Set())}
44
- />
45
- )
46
- }
47
-
48
- /** One row's checkbox. */
49
- export function SelectRow({
50
- id,
51
- selected,
52
- onSelect,
53
- t,
54
- }: {
55
- id: string
56
- selected: ReadonlySet<string>
57
- onSelect: (ids: ReadonlySet<string>) => void
58
- t: Translate
59
- }): ReactElement {
60
- return (
61
- <Checkbox
62
- className={selectBox}
63
- checked={selected.has(id)}
64
- aria-label={t('admin.selectRow', 'Select this row') ?? 'Select this row'}
65
- onCheckedChange={(checked: boolean | 'indeterminate') => {
66
- const next = new Set(selected)
67
- if (checked === true) next.add(id)
68
- else next.delete(id)
69
- onSelect(next)
70
- }}
71
- />
72
- )
73
- }
74
- // mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 576cedc3611379cc2dcbda75ac13cbf42a5f1d6fbdd43ed6e56fd9d621f3e821