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