@ketvietlab/ketsuite 0.1.3 → 0.1.6

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 (359) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/deployment.d.ts.map +1 -1
  3. package/dist/deployment.js +3 -0
  4. package/dist/deployment.js.map +1 -1
  5. package/dist/index.d.ts +8 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +7 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/modules/account_staff_channel/channel-routes.d.ts.map +1 -1
  10. package/dist/modules/account_staff_channel/channel-routes.js +1 -12
  11. package/dist/modules/account_staff_channel/channel-routes.js.map +1 -1
  12. package/dist/modules/attendance/channel-routes.d.ts.map +1 -1
  13. package/dist/modules/attendance/channel-routes.js +1 -12
  14. package/dist/modules/attendance/channel-routes.js.map +1 -1
  15. package/dist/modules/backend/design/HANDOFF.md +2 -2
  16. package/dist/modules/backend/design/auth.css +12 -0
  17. package/dist/modules/backend/design/client/table-selection.d.mts +3 -3
  18. package/dist/modules/backend/design/client/table-selection.d.mts.map +1 -1
  19. package/dist/modules/backend/design/client/table-selection.mjs +1 -1
  20. package/dist/modules/backend/design/client/table-selection.mjs.map +4 -4
  21. package/dist/modules/backend/design/content.css +43 -0
  22. package/dist/modules/backend/design/design-system.css +1 -1
  23. package/dist/modules/backend/design/forms.css +84 -1
  24. package/dist/modules/backend/design/lists.css +26 -18
  25. package/dist/modules/backend/design/record.css +5 -2
  26. package/dist/modules/backend/design/responsive.css +5 -6
  27. package/dist/modules/backend/design/user-workflow.css +257 -0
  28. package/dist/modules/backend/forms.d.ts +38 -1
  29. package/dist/modules/backend/forms.d.ts.map +1 -1
  30. package/dist/modules/backend/forms.js +29 -0
  31. package/dist/modules/backend/forms.js.map +1 -1
  32. package/dist/modules/backend/index.d.ts +3 -2
  33. package/dist/modules/backend/index.d.ts.map +1 -1
  34. package/dist/modules/backend/index.js +3 -2
  35. package/dist/modules/backend/index.js.map +1 -1
  36. package/dist/modules/backend/messages.d.ts.map +1 -1
  37. package/dist/modules/backend/messages.js +42 -2
  38. package/dist/modules/backend/messages.js.map +1 -1
  39. package/dist/modules/channel_api/core.d.ts +30 -0
  40. package/dist/modules/channel_api/core.d.ts.map +1 -1
  41. package/dist/modules/channel_api/core.js +28 -0
  42. package/dist/modules/channel_api/core.js.map +1 -1
  43. package/dist/modules/crm/functions.d.ts +5 -0
  44. package/dist/modules/crm/functions.d.ts.map +1 -1
  45. package/dist/modules/crm/functions.js +105 -10
  46. package/dist/modules/crm/functions.js.map +1 -1
  47. package/dist/modules/crm/jobs.d.ts.map +1 -1
  48. package/dist/modules/crm/jobs.js +1 -0
  49. package/dist/modules/crm/jobs.js.map +1 -1
  50. package/dist/modules/crm/operations.d.ts.map +1 -1
  51. package/dist/modules/crm/operations.js +2 -1
  52. package/dist/modules/crm/operations.js.map +1 -1
  53. package/dist/modules/crm_backend/index.d.ts.map +1 -1
  54. package/dist/modules/crm_backend/index.js +5 -2
  55. package/dist/modules/crm_backend/index.js.map +1 -1
  56. package/dist/modules/crm_backend/messages.d.ts +116 -0
  57. package/dist/modules/crm_backend/messages.d.ts.map +1 -1
  58. package/dist/modules/crm_backend/messages.js +122 -6
  59. package/dist/modules/crm_backend/messages.js.map +1 -1
  60. package/dist/modules/crm_backend/routes.d.ts.map +1 -1
  61. package/dist/modules/crm_backend/routes.js +434 -174
  62. package/dist/modules/crm_backend/routes.js.map +1 -1
  63. package/dist/modules/crm_backend/screens/case-create.d.ts +7 -0
  64. package/dist/modules/crm_backend/screens/case-create.d.ts.map +1 -1
  65. package/dist/modules/crm_backend/screens/case-create.js +13 -4
  66. package/dist/modules/crm_backend/screens/case-create.js.map +1 -1
  67. package/dist/modules/crm_backend/screens/case-detail.d.ts +6 -0
  68. package/dist/modules/crm_backend/screens/case-detail.d.ts.map +1 -1
  69. package/dist/modules/crm_backend/screens/case-detail.js +73 -23
  70. package/dist/modules/crm_backend/screens/case-detail.js.map +1 -1
  71. package/dist/modules/crm_backend/screens/configuration.d.ts +21 -3
  72. package/dist/modules/crm_backend/screens/configuration.d.ts.map +1 -1
  73. package/dist/modules/crm_backend/screens/configuration.js +260 -55
  74. package/dist/modules/crm_backend/screens/configuration.js.map +1 -1
  75. package/dist/modules/crm_backend/screens/index.d.ts +3 -3
  76. package/dist/modules/crm_backend/screens/index.d.ts.map +1 -1
  77. package/dist/modules/crm_backend/screens/index.js +2 -2
  78. package/dist/modules/crm_backend/screens/index.js.map +1 -1
  79. package/dist/modules/crm_staff_channel/channel-routes.d.ts.map +1 -1
  80. package/dist/modules/crm_staff_channel/channel-routes.js +1 -12
  81. package/dist/modules/crm_staff_channel/channel-routes.js.map +1 -1
  82. package/dist/modules/flow/membership.d.ts.map +1 -1
  83. package/dist/modules/flow/membership.js +7 -1
  84. package/dist/modules/flow/membership.js.map +1 -1
  85. package/dist/modules/flow/operations.d.ts +10 -4
  86. package/dist/modules/flow/operations.d.ts.map +1 -1
  87. package/dist/modules/flow/operations.js +10 -4
  88. package/dist/modules/flow/operations.js.map +1 -1
  89. package/dist/modules/flow_staff_channel/channel-routes.d.ts.map +1 -1
  90. package/dist/modules/flow_staff_channel/channel-routes.js +1 -20
  91. package/dist/modules/flow_staff_channel/channel-routes.js.map +1 -1
  92. package/dist/modules/hospitality_core/messages.d.ts.map +1 -1
  93. package/dist/modules/hospitality_core/messages.js +44 -0
  94. package/dist/modules/hospitality_core/messages.js.map +1 -1
  95. package/dist/modules/hospitality_core/night-audit.d.ts.map +1 -1
  96. package/dist/modules/hospitality_core/night-audit.js +7 -0
  97. package/dist/modules/hospitality_core/night-audit.js.map +1 -1
  98. package/dist/modules/hospitality_core/operations.d.ts.map +1 -1
  99. package/dist/modules/hospitality_core/operations.js +150 -40
  100. package/dist/modules/hospitality_core/operations.js.map +1 -1
  101. package/dist/modules/hospitality_core/routes.d.ts.map +1 -1
  102. package/dist/modules/hospitality_core/routes.js +24 -2
  103. package/dist/modules/hospitality_core/routes.js.map +1 -1
  104. package/dist/modules/hospitality_core/screens/check-out-prep.d.ts +46 -0
  105. package/dist/modules/hospitality_core/screens/check-out-prep.d.ts.map +1 -0
  106. package/dist/modules/hospitality_core/screens/check-out-prep.js +133 -0
  107. package/dist/modules/hospitality_core/screens/check-out-prep.js.map +1 -0
  108. package/dist/modules/hospitality_core/screens/folio-detail.d.ts.map +1 -1
  109. package/dist/modules/hospitality_core/screens/folio-detail.js +2 -4
  110. package/dist/modules/hospitality_core/screens/folio-detail.js.map +1 -1
  111. package/dist/modules/hospitality_core/screens/index.d.ts +2 -0
  112. package/dist/modules/hospitality_core/screens/index.d.ts.map +1 -1
  113. package/dist/modules/hospitality_core/screens/index.js +1 -0
  114. package/dist/modules/hospitality_core/screens/index.js.map +1 -1
  115. package/dist/modules/hospitality_core/screens/reservation-detail.d.ts.map +1 -1
  116. package/dist/modules/hospitality_core/screens/reservation-detail.js +8 -1
  117. package/dist/modules/hospitality_core/screens/reservation-detail.js.map +1 -1
  118. package/dist/modules/hospitality_core/screens/shared.d.ts +12 -0
  119. package/dist/modules/hospitality_core/screens/shared.d.ts.map +1 -1
  120. package/dist/modules/hospitality_core/screens/shared.js +25 -4
  121. package/dist/modules/hospitality_core/screens/shared.js.map +1 -1
  122. package/dist/modules/hospitality_core/services.d.ts +48 -0
  123. package/dist/modules/hospitality_core/services.d.ts.map +1 -1
  124. package/dist/modules/hospitality_core/services.js +70 -0
  125. package/dist/modules/hospitality_core/services.js.map +1 -1
  126. package/dist/modules/hospitality_staff_channel/channel-routes.d.ts.map +1 -1
  127. package/dist/modules/hospitality_staff_channel/channel-routes.js +1 -12
  128. package/dist/modules/hospitality_staff_channel/channel-routes.js.map +1 -1
  129. package/dist/modules/inventory_staff_channel/channel-routes.d.ts.map +1 -1
  130. package/dist/modules/inventory_staff_channel/channel-routes.js +1 -12
  131. package/dist/modules/inventory_staff_channel/channel-routes.js.map +1 -1
  132. package/dist/modules/loyalty/admin-functions.d.ts +2 -0
  133. package/dist/modules/loyalty/admin-functions.d.ts.map +1 -1
  134. package/dist/modules/loyalty/admin-functions.js +96 -20
  135. package/dist/modules/loyalty/admin-functions.js.map +1 -1
  136. package/dist/modules/loyalty/engine.d.ts +4 -1
  137. package/dist/modules/loyalty/engine.d.ts.map +1 -1
  138. package/dist/modules/loyalty/engine.js +125 -18
  139. package/dist/modules/loyalty/engine.js.map +1 -1
  140. package/dist/modules/loyalty/index.d.ts.map +1 -1
  141. package/dist/modules/loyalty/index.js +4 -3
  142. package/dist/modules/loyalty/index.js.map +1 -1
  143. package/dist/modules/loyalty/jobs.d.ts.map +1 -1
  144. package/dist/modules/loyalty/jobs.js +0 -1
  145. package/dist/modules/loyalty/jobs.js.map +1 -1
  146. package/dist/modules/loyalty/membership-functions.d.ts +5 -0
  147. package/dist/modules/loyalty/membership-functions.d.ts.map +1 -1
  148. package/dist/modules/loyalty/membership-functions.js +37 -23
  149. package/dist/modules/loyalty/membership-functions.js.map +1 -1
  150. package/dist/modules/loyalty/messages.d.ts +14 -0
  151. package/dist/modules/loyalty/messages.d.ts.map +1 -1
  152. package/dist/modules/loyalty/messages.js +14 -0
  153. package/dist/modules/loyalty/messages.js.map +1 -1
  154. package/dist/modules/loyalty/models.d.ts.map +1 -1
  155. package/dist/modules/loyalty/models.js +17 -0
  156. package/dist/modules/loyalty/models.js.map +1 -1
  157. package/dist/modules/loyalty/order-functions.d.ts.map +1 -1
  158. package/dist/modules/loyalty/order-functions.js +70 -17
  159. package/dist/modules/loyalty/order-functions.js.map +1 -1
  160. package/dist/modules/loyalty/program-design.d.ts +11 -0
  161. package/dist/modules/loyalty/program-design.d.ts.map +1 -0
  162. package/dist/modules/loyalty/program-design.js +356 -0
  163. package/dist/modules/loyalty/program-design.js.map +1 -0
  164. package/dist/modules/loyalty/stats.d.ts +0 -13
  165. package/dist/modules/loyalty/stats.d.ts.map +1 -1
  166. package/dist/modules/loyalty/stats.js +11 -6
  167. package/dist/modules/loyalty/stats.js.map +1 -1
  168. package/dist/modules/loyalty/stored-value.d.ts.map +1 -1
  169. package/dist/modules/loyalty/stored-value.js +7 -2
  170. package/dist/modules/loyalty/stored-value.js.map +1 -1
  171. package/dist/modules/loyalty/types.d.ts +1 -1
  172. package/dist/modules/loyalty/types.d.ts.map +1 -1
  173. package/dist/modules/loyalty_backend/design-messages.d.ts +221 -0
  174. package/dist/modules/loyalty_backend/design-messages.d.ts.map +1 -0
  175. package/dist/modules/loyalty_backend/design-messages.js +221 -0
  176. package/dist/modules/loyalty_backend/design-messages.js.map +1 -0
  177. package/dist/modules/loyalty_backend/index.js +224 -412
  178. package/dist/modules/loyalty_backend/index.js.map +1 -1
  179. package/dist/modules/loyalty_backend/messages.d.ts +642 -214
  180. package/dist/modules/loyalty_backend/messages.d.ts.map +1 -1
  181. package/dist/modules/loyalty_backend/messages.js +19 -13
  182. package/dist/modules/loyalty_backend/messages.js.map +1 -1
  183. package/dist/modules/loyalty_backend/program-workspace.d.ts +4 -0
  184. package/dist/modules/loyalty_backend/program-workspace.d.ts.map +1 -0
  185. package/dist/modules/loyalty_backend/program-workspace.js +600 -0
  186. package/dist/modules/loyalty_backend/program-workspace.js.map +1 -0
  187. package/dist/modules/loyalty_backend/screens/index.d.ts +12 -36
  188. package/dist/modules/loyalty_backend/screens/index.d.ts.map +1 -1
  189. package/dist/modules/loyalty_backend/screens/index.js +148 -292
  190. package/dist/modules/loyalty_backend/screens/index.js.map +1 -1
  191. package/dist/modules/loyalty_backend/screens/page-frame.d.ts.map +1 -1
  192. package/dist/modules/loyalty_backend/screens/page-frame.js +2 -2
  193. package/dist/modules/loyalty_backend/screens/page-frame.js.map +1 -1
  194. package/dist/modules/loyalty_pos/functions.d.ts.map +1 -1
  195. package/dist/modules/loyalty_pos/functions.js +1 -0
  196. package/dist/modules/loyalty_pos/functions.js.map +1 -1
  197. package/dist/modules/loyalty_pos/index.js +1 -1
  198. package/dist/modules/loyalty_pos/index.js.map +1 -1
  199. package/dist/modules/loyalty_sale/functions.d.ts +1 -0
  200. package/dist/modules/loyalty_sale/functions.d.ts.map +1 -1
  201. package/dist/modules/loyalty_sale/functions.js +1 -0
  202. package/dist/modules/loyalty_sale/functions.js.map +1 -1
  203. package/dist/modules/partner_backend/client/partner.css +3 -1
  204. package/dist/modules/partner_backend/index.d.ts.map +1 -1
  205. package/dist/modules/partner_backend/index.js +4 -0
  206. package/dist/modules/partner_backend/index.js.map +1 -1
  207. package/dist/modules/partner_backend/routes.d.ts.map +1 -1
  208. package/dist/modules/partner_backend/routes.js +6 -1
  209. package/dist/modules/partner_backend/routes.js.map +1 -1
  210. package/dist/modules/partner_backend/screens/form.d.ts +1 -0
  211. package/dist/modules/partner_backend/screens/form.d.ts.map +1 -1
  212. package/dist/modules/partner_backend/screens/form.js +1 -0
  213. package/dist/modules/partner_backend/screens/form.js.map +1 -1
  214. package/dist/modules/purchase_staff_channel/order-routes.d.ts.map +1 -1
  215. package/dist/modules/purchase_staff_channel/order-routes.js +1 -12
  216. package/dist/modules/purchase_staff_channel/order-routes.js.map +1 -1
  217. package/dist/modules/quality_staff_channel/channel-routes.d.ts.map +1 -1
  218. package/dist/modules/quality_staff_channel/channel-routes.js +1 -12
  219. package/dist/modules/quality_staff_channel/channel-routes.js.map +1 -1
  220. package/dist/modules/sale/functions.d.ts.map +1 -1
  221. package/dist/modules/sale/functions.js +27 -17
  222. package/dist/modules/sale/functions.js.map +1 -1
  223. package/dist/modules/sale_staff_channel/channel-routes.d.ts.map +1 -1
  224. package/dist/modules/sale_staff_channel/channel-routes.js +1 -12
  225. package/dist/modules/sale_staff_channel/channel-routes.js.map +1 -1
  226. package/dist/modules/sale_staff_channel/order-routes.d.ts.map +1 -1
  227. package/dist/modules/sale_staff_channel/order-routes.js +1 -12
  228. package/dist/modules/sale_staff_channel/order-routes.js.map +1 -1
  229. package/dist/modules/stock_staff_channel/operation-routes.d.ts.map +1 -1
  230. package/dist/modules/stock_staff_channel/operation-routes.js +1 -12
  231. package/dist/modules/stock_staff_channel/operation-routes.js.map +1 -1
  232. package/dist/modules/user/authorization.d.ts +64 -1
  233. package/dist/modules/user/authorization.d.ts.map +1 -1
  234. package/dist/modules/user/authorization.js +445 -53
  235. package/dist/modules/user/authorization.js.map +1 -1
  236. package/dist/modules/user/functions.d.ts.map +1 -1
  237. package/dist/modules/user/functions.js +45 -23
  238. package/dist/modules/user/functions.js.map +1 -1
  239. package/dist/modules/user/index.d.ts +2 -1
  240. package/dist/modules/user/index.d.ts.map +1 -1
  241. package/dist/modules/user/index.js +1 -1
  242. package/dist/modules/user/index.js.map +1 -1
  243. package/dist/modules/user/models.d.ts.map +1 -1
  244. package/dist/modules/user/models.js +5 -0
  245. package/dist/modules/user/models.js.map +1 -1
  246. package/dist/modules/user/roles.d.ts.map +1 -1
  247. package/dist/modules/user/roles.js +2 -1
  248. package/dist/modules/user/roles.js.map +1 -1
  249. package/dist/modules/user_backend/access-routes.d.ts +31 -0
  250. package/dist/modules/user_backend/access-routes.d.ts.map +1 -0
  251. package/dist/modules/user_backend/access-routes.js +447 -0
  252. package/dist/modules/user_backend/access-routes.js.map +1 -0
  253. package/dist/modules/user_backend/index.d.ts +1 -0
  254. package/dist/modules/user_backend/index.d.ts.map +1 -1
  255. package/dist/modules/user_backend/index.js +1 -0
  256. package/dist/modules/user_backend/index.js.map +1 -1
  257. package/dist/modules/user_backend/messages.d.ts +4 -0
  258. package/dist/modules/user_backend/messages.d.ts.map +1 -1
  259. package/dist/modules/user_backend/messages.js +4 -0
  260. package/dist/modules/user_backend/messages.js.map +1 -1
  261. package/dist/modules/user_backend/routes.d.ts.map +1 -1
  262. package/dist/modules/user_backend/routes.js +38 -3
  263. package/dist/modules/user_backend/routes.js.map +1 -1
  264. package/dist/modules/user_backend/screens/user-form.d.ts +1 -0
  265. package/dist/modules/user_backend/screens/user-form.d.ts.map +1 -1
  266. package/dist/modules/user_backend/screens/user-form.js +28 -0
  267. package/dist/modules/user_backend/screens/user-form.js.map +1 -1
  268. package/dist/modules/website/cms.d.ts.map +1 -1
  269. package/dist/modules/website/cms.js +283 -6
  270. package/dist/modules/website/cms.js.map +1 -1
  271. package/dist/modules/website/index.d.ts.map +1 -1
  272. package/dist/modules/website/index.js +12 -0
  273. package/dist/modules/website/index.js.map +1 -1
  274. package/dist/modules/website/media-usage.d.ts +38 -0
  275. package/dist/modules/website/media-usage.d.ts.map +1 -0
  276. package/dist/modules/website/media-usage.js +87 -0
  277. package/dist/modules/website/media-usage.js.map +1 -0
  278. package/dist/modules/website/paths.js +1 -1
  279. package/dist/modules/website/paths.js.map +1 -1
  280. package/dist/modules/website/sections.d.ts.map +1 -1
  281. package/dist/modules/website/sections.js +12 -1
  282. package/dist/modules/website/sections.js.map +1 -1
  283. package/dist/modules/website_backend/index.js +138 -0
  284. package/dist/modules/website_backend/index.js.map +1 -1
  285. package/dist/modules/website_backend/menus.d.ts.map +1 -1
  286. package/dist/modules/website_backend/menus.js +11 -0
  287. package/dist/modules/website_backend/menus.js.map +1 -1
  288. package/dist/modules/website_backend/routes.d.ts.map +1 -1
  289. package/dist/modules/website_backend/routes.js +414 -31
  290. package/dist/modules/website_backend/routes.js.map +1 -1
  291. package/dist/modules/website_backend/screens/index.d.ts +98 -6
  292. package/dist/modules/website_backend/screens/index.d.ts.map +1 -1
  293. package/dist/modules/website_backend/screens/index.js +600 -154
  294. package/dist/modules/website_backend/screens/index.js.map +1 -1
  295. package/dist/modules/website_backend/screens/page-frame.d.ts +3 -1
  296. package/dist/modules/website_backend/screens/page-frame.d.ts.map +1 -1
  297. package/dist/modules/website_backend/screens/page-frame.js +1 -1
  298. package/dist/modules/website_backend/screens/page-frame.js.map +1 -1
  299. package/dist/modules/website_menu/functions.d.ts.map +1 -1
  300. package/dist/modules/website_menu/functions.js +51 -0
  301. package/dist/modules/website_menu/functions.js.map +1 -1
  302. package/dist/modules/website_menu/index.js +2 -0
  303. package/dist/modules/website_menu/index.js.map +1 -1
  304. package/dist/modules/website_search/functions.d.ts.map +1 -1
  305. package/dist/modules/website_search/functions.js +10 -120
  306. package/dist/modules/website_search/functions.js.map +1 -1
  307. package/dist/modules/website_search/index.js +2 -0
  308. package/dist/modules/website_search/index.js.map +1 -1
  309. package/dist/modules/website_search/jobs.d.ts +3 -0
  310. package/dist/modules/website_search/jobs.d.ts.map +1 -0
  311. package/dist/modules/website_search/jobs.js +101 -0
  312. package/dist/modules/website_search/jobs.js.map +1 -0
  313. package/dist/modules/website_search/rebuild.d.ts +33 -0
  314. package/dist/modules/website_search/rebuild.d.ts.map +1 -0
  315. package/dist/modules/website_search/rebuild.js +125 -0
  316. package/dist/modules/website_search/rebuild.js.map +1 -0
  317. package/dist/permission-catalogue.d.ts.map +1 -1
  318. package/dist/permission-catalogue.js +21 -1
  319. package/dist/permission-catalogue.js.map +1 -1
  320. package/dist/scaffold/index.js +1 -1
  321. package/dist/ui/chrome.d.ts +8 -0
  322. package/dist/ui/chrome.d.ts.map +1 -1
  323. package/dist/ui/chrome.js +9 -1
  324. package/dist/ui/chrome.js.map +1 -1
  325. package/dist/ui/client/table-selection-view.d.ts.map +1 -1
  326. package/dist/ui/client/table-selection-view.js +161 -5
  327. package/dist/ui/client/table-selection-view.js.map +1 -1
  328. package/dist/ui/client/user-workflow.d.ts +2 -0
  329. package/dist/ui/client/user-workflow.d.ts.map +1 -0
  330. package/dist/ui/client/user-workflow.js +141 -0
  331. package/dist/ui/client/user-workflow.js.map +1 -0
  332. package/dist/ui/form.d.ts +9 -0
  333. package/dist/ui/form.d.ts.map +1 -1
  334. package/dist/ui/form.js +29 -15
  335. package/dist/ui/form.js.map +1 -1
  336. package/dist/ui/icons.d.ts.map +1 -1
  337. package/dist/ui/icons.js +2 -0
  338. package/dist/ui/icons.js.map +1 -1
  339. package/dist/ui/index.d.ts +7 -3
  340. package/dist/ui/index.d.ts.map +1 -1
  341. package/dist/ui/index.js +6 -3
  342. package/dist/ui/index.js.map +1 -1
  343. package/dist/ui/layout.d.ts.map +1 -1
  344. package/dist/ui/layout.js +14 -4
  345. package/dist/ui/layout.js.map +1 -1
  346. package/dist/ui/nav.d.ts.map +1 -1
  347. package/dist/ui/nav.js +2 -1
  348. package/dist/ui/nav.js.map +1 -1
  349. package/dist/ui/state.d.ts +20 -1
  350. package/dist/ui/state.d.ts.map +1 -1
  351. package/dist/ui/state.js +17 -0
  352. package/dist/ui/state.js.map +1 -1
  353. package/dist/ui/table.js +1 -1
  354. package/dist/ui/table.js.map +1 -1
  355. package/dist/ui/user-workflow.d.ts +72 -0
  356. package/dist/ui/user-workflow.d.ts.map +1 -0
  357. package/dist/ui/user-workflow.js +144 -0
  358. package/dist/ui/user-workflow.js.map +1 -0
  359. package/package.json +8 -4
@@ -1,7 +1,8 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { text, withHeaders } from '@ketvietlab/ketjs';
3
3
  import { readForm, seeOther } from '../backend/forms.js';
4
- import { contentScreen, entryFormScreen, formEditorScreen, entrySeoSection, preflightScreen, publicationsScreen, redirectsScreen, searchIndexScreen, siteDomainsScreen, siteMembersScreen, submissionRecordScreen, formsScreen, mediaFormScreen, mediaScreen, menuFormScreen, menusScreen, previewScreen, revisionsScreen, siteFormScreen, sitesScreen, submissionsScreen, taxonomyScreen, taxonomyFormScreen, } from './screens/index.js';
4
+ import { PAGE_SIZE, pageOf, pager, searchOf } from '../backend/paging.js';
5
+ import { contentScreen, entryFormScreen, formEditorScreen, entrySeoSection, preflightScreen, publicationsScreen, redirectsScreen, searchIndexScreen, siteDomainsScreen, siteHealthScreen, siteMembersScreen, submissionRecordScreen, formsScreen, mediaFormScreen, mediaScreen, menuFormScreen, menusScreen, previewScreen, revisionsScreen, siteFormScreen, sitesScreen, submissionsScreen, taxonomyScreen, taxonomyFormScreen, } from './screens/index.js';
5
6
  import { csvOf, safeFilename } from './csv.js';
6
7
  import { adminPage, inLocale, localeQuery } from '../backend/screen.js';
7
8
  const sitesOf = (ctx, url, req) => ctx.call('website.listSites', {}, url, req);
@@ -29,6 +30,22 @@ const parseJson = (value) => {
29
30
  return { ok: false };
30
31
  }
31
32
  };
33
+ /**
34
+ * The tokens box, as the contract wants them.
35
+ *
36
+ * An empty box means "no overrides" rather than "leave whatever is stored":
37
+ * this is the only screen that writes them, so a blank field is a decision.
38
+ */
39
+ const siteTokensOf = (value) => {
40
+ if (!value?.trim())
41
+ return {};
42
+ const parsed = parseJson(value);
43
+ // Malformed JSON goes down as the string it is: saveSite answers
44
+ // `invalidTokens` for it, which is the message to show, and the screen shows
45
+ // domain errors already. Sending `{}` instead would erase the site's tokens
46
+ // because somebody mistyped a brace.
47
+ return parsed.ok ? parsed.value : value;
48
+ };
32
49
  const invalidJsonErrors = (form, _) => {
33
50
  const errors = [];
34
51
  if (!parseJson(form.layout).ok)
@@ -103,6 +120,16 @@ const revisionDiffOf = async (ctx, url, req, entryId, rows) => {
103
120
  return result?.ok ? result : null;
104
121
  };
105
122
  const entryOf = (ctx, url, req, id) => ctx.call('website.getEntry', { id }, url, req);
123
+ /**
124
+ * Where an entry's own screen lives.
125
+ *
126
+ * A route shared by pages and posts still has to send the browser to one of
127
+ * them, and `/admin/website/pages/{id}` answers 404 for a post.
128
+ */
129
+ const entryHref = async (ctx, url, req, id) => {
130
+ const detail = await entryOf(ctx, url, req, id);
131
+ return `${detail?.entry.type === 'website.post' ? '/admin/website/posts' : '/admin/website/pages'}/${id}`;
132
+ };
106
133
  const saveEntry = async (ctx, url, req, id, form, type) => {
107
134
  const layout = parseJson(form.layout);
108
135
  const fields = parseJson(form.fields || '{}');
@@ -126,15 +153,20 @@ const renderEntry = async (ctx, url, req, detail, siteId, kind, options = {}) =>
126
153
  // Read beside the entry rather than folded into getEntry: the head tags
127
154
  // belong to website_seo, and the CMS does not get to decide what is public
128
155
  // about a page on that module's behalf.
129
- const seo = detail
130
- ? (await ctx.call('website_seo.getEntrySeo', { entryId: detail.entry.id }, url, req))
131
- : null;
156
+ const [seo, assigned, available] = detail
157
+ ? await Promise.all([
158
+ ctx.call('website_seo.getEntrySeo', { entryId: detail.entry.id }, url, req),
159
+ ctx.call('website.listEntryTerms', { entryId: detail.entry.id }, url, req),
160
+ ctx.call('website.listTaxonomyTerms', { siteId }, url, req),
161
+ ])
162
+ : [null, [], []];
132
163
  return adminPage(ctx, url, req, {
133
164
  title: detail?.entry.title ?? _(`website_backend.${kind.titleKey}.newTitle`),
134
165
  translate: false,
135
166
  body: (_, frame) => entryFormScreen(_, detail, siteId, kind, frame, {
136
167
  ...options,
137
168
  seo,
169
+ terms: detail ? { assigned, available } : null,
138
170
  locale: localeQuery(url),
139
171
  }),
140
172
  });
@@ -146,13 +178,28 @@ const entryRoutes = (kind, type) => ({
146
178
  const _ = ctx.translate(ctx.localeOf(url, req));
147
179
  const sites = await sitesOf(ctx, url, req);
148
180
  const siteId = selectedSite(url, sites);
149
- const rows = siteId
150
- ? (await ctx.call('website.listEntries', { siteId, type }, url, req))
151
- : [];
181
+ const current = pageOf(url);
182
+ // `listEntries` and `countEntries` have taken both of these since they
183
+ // were written and no screen passed either, so a site with three hundred
184
+ // pages could only be read one page of thirty at a time, in date order.
185
+ const search = searchOf(url);
186
+ const status = url.searchParams.get('status');
187
+ const filter = {
188
+ siteId,
189
+ type,
190
+ ...(search ? { search } : {}),
191
+ ...(status && status !== 'all' ? { status } : {}),
192
+ };
193
+ const [rows, total] = siteId
194
+ ? await Promise.all([
195
+ ctx.call('website.listEntries', { ...filter, limit: PAGE_SIZE, offset: (current - 1) * PAGE_SIZE }, url, req),
196
+ ctx.call('website.countEntries', filter, url, req),
197
+ ])
198
+ : [[], { count: 0 }];
152
199
  return adminPage(ctx, url, req, {
153
200
  title: _(`website_backend.${kind.titleKey}.title`),
154
201
  translate: false,
155
- body: (_, frame) => contentScreen(_, rows, siteOptions(sites), siteId, frame, localeQuery(url), kind),
202
+ body: (_, frame) => contentScreen(_, rows, siteOptions(sites), siteId, frame, localeQuery(url), kind, pager(url, current, rows.length, total.count), { search, status: status ?? 'all' }),
156
203
  });
157
204
  },
158
205
  [`${kind.basePath}/new`]: (ctx) => async (url, req) => {
@@ -212,7 +259,14 @@ const entryRoutes = (kind, type) => ({
212
259
  if (!detail || detail.entry.type !== type)
213
260
  return text(_('website_backend.error.notFound'), { status: 404 });
214
261
  const form = await readForm(req);
215
- const result = await ctx.call('website.publishEntry', { id: params.id, expectedRevisionId: form.expectedRevisionId || null }, url, req);
262
+ const result = await ctx.call('website.publishEntry', {
263
+ id: params.id,
264
+ expectedRevisionId: form.expectedRevisionId || null,
265
+ // A blank field means now, which is what the contract's optional
266
+ // `publishAt` has always meant. A datetime-local value carries no
267
+ // zone, so it is read in the server's, same as the job that fires it.
268
+ publishAt: form.publishAt?.trim() ? new Date(form.publishAt).toISOString() : null,
269
+ }, url, req);
216
270
  if (!result.ok)
217
271
  return renderEntry(ctx, url, req, detail, detail.entry.siteId, kind, {
218
272
  errors: resultErrors(result, _),
@@ -233,7 +287,61 @@ const entryRoutes = (kind, type) => ({
233
287
  body: (_, frame) => revisionsScreen(_, detail.entry, rows, frame, localeQuery(url), kind.basePath, diff),
234
288
  });
235
289
  },
290
+ /**
291
+ * Taking a page back down, and cancelling a schedule.
292
+ *
293
+ * publishEntry had no inverse: nothing set `status` back and nothing cleared
294
+ * `publishedRevisionId`, which is what the public resolver's per-entry
295
+ * fallback reads. A page published by mistake had only `noindex`, which
296
+ * asks crawlers to forget it while every visitor with the address reads on.
297
+ */
298
+ [`${kind.basePath}/{id}/unpublish`]: (ctx) => async (url, req, params) => {
299
+ if (req.method !== 'POST')
300
+ return text('POST', { status: 405 });
301
+ const _ = ctx.translate(ctx.localeOf(url, req));
302
+ const detail = await entryOf(ctx, url, req, params.id);
303
+ if (!detail || detail.entry.type !== type)
304
+ return text(_('website_backend.error.notFound'), { status: 404 });
305
+ const result = await ctx.call('website.unpublishEntry', { id: params.id }, url, req);
306
+ if (!result.ok)
307
+ return text(resultErrors(result, _).join('; '), { status: 400 });
308
+ return seeOther(inLocale(url, `${kind.basePath}/${params.id}`));
309
+ },
310
+ /**
311
+ * Out of the way, and back again.
312
+ *
313
+ * Every reader in the module already honoured `trash` and nothing could
314
+ * write it, so a page made by mistake stayed on the list for ever.
315
+ */
316
+ [`${kind.basePath}/{id}/trash`]: (ctx) => async (url, req, params) => {
317
+ if (req.method !== 'POST')
318
+ return text('POST', { status: 405 });
319
+ const _ = ctx.translate(ctx.localeOf(url, req));
320
+ const detail = await entryOf(ctx, url, req, params.id);
321
+ if (!detail || detail.entry.type !== type)
322
+ return text(_('website_backend.error.notFound'), { status: 404 });
323
+ const result = await ctx.call('website.trashEntry', { id: params.id }, url, req);
324
+ if (!result.ok)
325
+ return text(resultErrors(result, _).join('; '), { status: 400 });
326
+ // Back to the list: the page is off it now, and staying on a screen for
327
+ // something you just put away reads as though it did not work.
328
+ return seeOther(inLocale(url, `${kind.basePath}?site=${encodeURIComponent(detail.entry.siteId)}`));
329
+ },
330
+ [`${kind.basePath}/{id}/untrash`]: (ctx) => async (url, req, params) => {
331
+ if (req.method !== 'POST')
332
+ return text('POST', { status: 405 });
333
+ const _ = ctx.translate(ctx.localeOf(url, req));
334
+ const detail = await entryOf(ctx, url, req, params.id);
335
+ if (!detail || detail.entry.type !== type)
336
+ return text(_('website_backend.error.notFound'), { status: 404 });
337
+ const result = await ctx.call('website.untrashEntry', { id: params.id }, url, req);
338
+ if (!result.ok)
339
+ return text(resultErrors(result, _).join('; '), { status: 400 });
340
+ return seeOther(inLocale(url, `${kind.basePath}/${params.id}`));
341
+ },
236
342
  [`${kind.basePath}/{id}/revisions/{revisionId}/restore`]: (ctx) => async (url, req, params) => {
343
+ if (req.method !== 'POST')
344
+ return text('POST', { status: 405 });
237
345
  const _ = ctx.translate(ctx.localeOf(url, req));
238
346
  const result = await ctx.call('website.restoreRevision', { entryId: params.id, revisionId: params.revisionId }, url, req);
239
347
  if (!result.ok)
@@ -243,18 +351,46 @@ const entryRoutes = (kind, type) => ({
243
351
  return seeOther(inLocale(url, `${kind.basePath}/${params.id}`));
244
352
  },
245
353
  [`${kind.basePath}/{id}/preview`]: (ctx) => async (url, req, params) => {
246
- if (req.method !== 'GET')
247
- return text('GET', { status: 405 });
354
+ if (req.method !== 'GET' && req.method !== 'POST')
355
+ return text('GET or POST', { status: 405 });
248
356
  const _ = ctx.translate(ctx.localeOf(url, req));
249
357
  const detail = await entryOf(ctx, url, req, params.id);
250
358
  if (!detail || detail.entry.type !== type)
251
359
  return text(_('website_backend.error.notFound'), { status: 404 });
252
- const preview = (await ctx.call('website.createPreviewToken', { entryId: params.id }, url, req));
360
+ // A GET used to mint. Opening the screen twice, or a link prefetcher
361
+ // touching it once, left tokens behind that nobody knew existed.
362
+ let minted = null;
363
+ if (req.method === 'POST') {
364
+ const form = await readForm(req);
365
+ minted = (await ctx.call('website.createPreviewToken', {
366
+ entryId: params.id,
367
+ ttlSeconds: Number(form.ttlSeconds) || null,
368
+ oneTime: !!form.oneTime,
369
+ }, url, req));
370
+ }
253
371
  return adminPage(ctx, url, req, {
254
372
  title: 'website_backend.preview.title',
255
- body: (_, frame) => previewScreen(_, detail.entry, preview.token, preview.expiresAt, frame, kind.basePath),
373
+ body: (_, frame) => previewScreen(_, detail.entry, minted, frame, kind.basePath, localeQuery(url)),
256
374
  });
257
375
  },
376
+ /**
377
+ * Withdraw every preview link this entry has.
378
+ *
379
+ * Each visit to the preview screen mints another token, so they accumulate,
380
+ * and a link pasted into a chat outlives the reason it was shared.
381
+ * revokePreviewTokens has always been able to call them all back; nothing
382
+ * asked it to. Back to the entry rather than the preview screen, because
383
+ * landing on the preview screen would immediately mint a fresh one.
384
+ */
385
+ [`${kind.basePath}/{id}/preview/revoke`]: (ctx) => async (url, req, params) => {
386
+ if (req.method !== 'POST')
387
+ return text('POST', { status: 405 });
388
+ const _ = ctx.translate(ctx.localeOf(url, req));
389
+ const result = await ctx.call('website.revokePreviewTokens', { entryId: params.id }, url, req);
390
+ if (!result.ok)
391
+ return text(resultErrors(result, _).join('; '), { status: 400 });
392
+ return seeOther(inLocale(url, `${kind.basePath}/${params.id}`));
393
+ },
258
394
  });
259
395
  const PAGES = { basePath: '/admin/website/pages', titleKey: 'pages' };
260
396
  const POSTS = { basePath: '/admin/website/posts', titleKey: 'posts' };
@@ -325,9 +461,14 @@ export const routes = {
325
461
  if (req.method !== 'GET')
326
462
  return text('GET', { status: 405 });
327
463
  const _ = ctx.translate(ctx.localeOf(url, req));
464
+ // Read directly rather than through sitesOf: that one feeds every
465
+ // screen's site switcher, and a switcher that hides suspended sites
466
+ // would make them unreachable rather than merely unlisted.
467
+ const chosen = url.searchParams.get('state');
468
+ const filter = chosen === 'active' || chosen === 'inactive' ? { active: chosen === 'active' } : {};
328
469
  return adminPage(ctx, url, req, {
329
470
  title: 'website_backend.sites.title',
330
- body: async (_, frame) => sitesScreen(_, await sitesOf(ctx, url, req), frame, localeQuery(url)),
471
+ body: async (_, frame) => sitesScreen(_, (await ctx.call('website.listSites', filter, url, req)), frame, localeQuery(url), chosen ?? 'all'),
331
472
  });
332
473
  },
333
474
  '/admin/website/sites/new': (ctx) => async (url, req) => {
@@ -342,6 +483,7 @@ export const routes = {
342
483
  title: form.title,
343
484
  defaultLocale: form.defaultLocale,
344
485
  theme: form.theme,
486
+ tokens: siteTokensOf(form.tokens),
345
487
  active: form.active === '1',
346
488
  }, url, req);
347
489
  if (result.ok)
@@ -377,6 +519,7 @@ export const routes = {
377
519
  title: form.title,
378
520
  defaultLocale: form.defaultLocale,
379
521
  theme: form.theme,
522
+ tokens: siteTokensOf(form.tokens),
380
523
  active: form.active === '1',
381
524
  }, url, req);
382
525
  if (result.ok)
@@ -481,16 +624,24 @@ export const routes = {
481
624
  });
482
625
  },
483
626
  '/admin/website/content/{id}/preview': (ctx) => async (url, req, params) => {
484
- if (req.method !== 'GET')
485
- return text('GET', { status: 405 });
627
+ if (req.method !== 'GET' && req.method !== 'POST')
628
+ return text('GET or POST', { status: 405 });
486
629
  const _ = ctx.translate(ctx.localeOf(url, req));
487
630
  const detail = await entryOf(ctx, url, req, params.id);
488
631
  if (!detail)
489
632
  return text(_('website_backend.error.notFound'), { status: 404 });
490
- const preview = (await ctx.call('website.createPreviewToken', { entryId: params.id }, url, req));
633
+ let minted = null;
634
+ if (req.method === 'POST') {
635
+ const form = await readForm(req);
636
+ minted = (await ctx.call('website.createPreviewToken', {
637
+ entryId: params.id,
638
+ ttlSeconds: Number(form.ttlSeconds) || null,
639
+ oneTime: !!form.oneTime,
640
+ }, url, req));
641
+ }
491
642
  return adminPage(ctx, url, req, {
492
643
  title: 'website_backend.preview.title',
493
- body: (_, frame) => previewScreen(_, detail.entry, preview.token, preview.expiresAt, frame),
644
+ body: (_, frame) => previewScreen(_, detail.entry, minted, frame, '/admin/website/content', localeQuery(url)),
494
645
  });
495
646
  },
496
647
  '/admin/website/taxonomies': (ctx) => async (url, req) => {
@@ -525,6 +676,35 @@ export const routes = {
525
676
  * preflightPublication reads a revision per page, so it is a button rather
526
677
  * than something the content list pays for on every render.
527
678
  */
679
+ /**
680
+ * The taxonomy terms one entry carries.
681
+ *
682
+ * assignTerm shipped with the taxonomy module and no screen ever called it,
683
+ * so the categories and tags a site declares could only be put on a page by
684
+ * an agent - and `listEntryTerms` and `unassignTerm` did not exist at all,
685
+ * which made an assignment invisible and permanent once made.
686
+ */
687
+ '/admin/website/content/{id}/terms': (ctx) => async (url, req, params) => {
688
+ if (req.method !== 'POST')
689
+ return text('POST', { status: 405 });
690
+ const _ = ctx.translate(ctx.localeOf(url, req));
691
+ const form = await readForm(req);
692
+ if (!form.termId)
693
+ return text(_('website_backend.terms.noTerm'), { status: 400 });
694
+ const result = await ctx.call('website.assignTerm', { id: randomUUID(), entryId: params.id, termId: form.termId }, url, req);
695
+ if (!result.ok)
696
+ return text(resultErrors(result, _).join('; '), { status: 400 });
697
+ return seeOther(inLocale(url, await entryHref(ctx, url, req, params.id)));
698
+ },
699
+ '/admin/website/content/{id}/terms/{termId}/remove': (ctx) => async (url, req, params) => {
700
+ if (req.method !== 'POST')
701
+ return text('POST', { status: 405 });
702
+ const _ = ctx.translate(ctx.localeOf(url, req));
703
+ const result = await ctx.call('website.unassignTerm', { entryId: params.id, termId: params.termId }, url, req);
704
+ if (!result.ok)
705
+ return text(resultErrors(result, _).join('; '), { status: 400 });
706
+ return seeOther(inLocale(url, await entryHref(ctx, url, req, params.id)));
707
+ },
528
708
  /**
529
709
  * The head tags for one page.
530
710
  *
@@ -547,7 +727,7 @@ export const routes = {
547
727
  }, url, req);
548
728
  if (!result.ok)
549
729
  return text(resultErrors(result, _).join('; '), { status: 400 });
550
- return seeOther(inLocale(url, `/admin/website/pages/${params.id}`));
730
+ return seeOther(inLocale(url, await entryHref(ctx, url, req, params.id)));
551
731
  },
552
732
  /**
553
733
  * Publishing a set, from a screen.
@@ -563,10 +743,15 @@ export const routes = {
563
743
  const sites = await sitesOf(ctx, url, req);
564
744
  const posted = req.method === 'POST' ? await readForm(req) : null;
565
745
  const siteId = posted?.siteId || selectedSite(url, sites);
746
+ const chosenState = url.searchParams.get('state');
747
+ const state = chosenState && chosenState !== 'all' ? chosenState : null;
566
748
  const render = async (errors, notice) => {
749
+ const live = siteId
750
+ ? (await ctx.call('website.activePublication', { siteId }, url, req))
751
+ : null;
567
752
  const [rows, entries] = siteId
568
753
  ? await Promise.all([
569
- ctx.call('website.listPublications', { siteId }, url, req),
754
+ ctx.call('website.listPublications', { siteId, ...(state ? { state } : {}) }, url, req),
570
755
  ctx.call('website.listEntries', { siteId, status: 'published' }, url, req),
571
756
  ])
572
757
  : [[], []];
@@ -576,6 +761,8 @@ export const routes = {
576
761
  errors,
577
762
  notice,
578
763
  locale: localeQuery(url),
764
+ state: chosenState ?? 'all',
765
+ activeId: live?.id ?? null,
579
766
  }),
580
767
  });
581
768
  };
@@ -598,8 +785,17 @@ export const routes = {
598
785
  return render(resultErrors(result, _));
599
786
  },
600
787
  '/admin/website/publications/{id}/activate': (ctx) => async (url, req, params) => {
788
+ if (req.method !== 'POST')
789
+ return text('POST', { status: 405 });
601
790
  const _ = ctx.translate(ctx.localeOf(url, req));
602
- const result = (await ctx.call('website.activatePublication', { id: params.id }, url, req));
791
+ const form = await readForm(req);
792
+ const result = (await ctx.call('website.activatePublication', {
793
+ id: params.id,
794
+ // What the list said was live when this row was drawn. The empty
795
+ // string is a site that had no active publication then, which is a
796
+ // different claim from "do not check".
797
+ expectedPublicationId: form.expectedPublicationId ?? null,
798
+ }, url, req));
603
799
  if (!result.ok) {
604
800
  const missing = result.missingSections?.length
605
801
  ? `${_('website_backend.publications.missing')}: ${result.missingSections.join(', ')}`
@@ -609,6 +805,8 @@ export const routes = {
609
805
  return seeOther(inLocale(url, '/admin/website/publications'));
610
806
  },
611
807
  '/admin/website/publications/{id}/rollback': (ctx) => async (url, req, params) => {
808
+ if (req.method !== 'POST')
809
+ return text('POST', { status: 405 });
612
810
  const _ = ctx.translate(ctx.localeOf(url, req));
613
811
  const sites = await sitesOf(ctx, url, req);
614
812
  const siteId = selectedSite(url, sites);
@@ -622,6 +820,41 @@ export const routes = {
622
820
  return text(resultErrors(result, _).join('; '), { status: 400 });
623
821
  return seeOther(inLocale(url, `/admin/website/publications?site=${encodeURIComponent(siteId)}`));
624
822
  },
823
+ /**
824
+ * Which site needs looking at, across all of them.
825
+ *
826
+ * Every other screen is scoped to one site because every contract behind
827
+ * them takes a `siteId`, which makes "is anything wrong" a question you can
828
+ * only answer by opening each site in turn and remembering. Read-only, and
829
+ * built entirely from reads that already existed.
830
+ */
831
+ '/admin/website/health': (ctx) => async (url, req) => {
832
+ if (req.method !== 'GET')
833
+ return text('GET', { status: 405 });
834
+ const sites = await sitesOf(ctx, url, req);
835
+ const rows = await Promise.all(sites.map(async (site) => {
836
+ const [domains, publications, index] = await Promise.all([
837
+ ctx.call('website.listDomains', { siteId: site.id }, url, req),
838
+ ctx.call('website.listPublications', { siteId: site.id }, url, req),
839
+ ctx.call('website_search.indexStatus', { siteId: site.id }, url, req),
840
+ ]);
841
+ return {
842
+ siteId: site.id,
843
+ title: site.title ?? site.name,
844
+ active: site.active !== false,
845
+ primaryHost: domains.find((domain) => domain.primary)?.host ?? null,
846
+ domainCount: domains.length,
847
+ indexState: index?.state ?? 'absent',
848
+ indexCurrent: index?.current === true,
849
+ preparedCount: publications.filter((row) => row.state === 'prepared').length,
850
+ hasActivePublication: publications.some((row) => row.state === 'active'),
851
+ };
852
+ }));
853
+ return adminPage(ctx, url, req, {
854
+ title: 'website_backend.health.title',
855
+ body: (_, frame) => siteHealthScreen(_, rows, frame, localeQuery(url)),
856
+ });
857
+ },
625
858
  '/admin/website/preflight': (ctx) => async (url, req) => {
626
859
  if (req.method !== 'GET')
627
860
  return text('GET', { status: 405 });
@@ -630,10 +863,19 @@ export const routes = {
630
863
  const siteId = selectedSite(url, sites);
631
864
  if (!siteId)
632
865
  return text(_('website_backend.content.noSite'), { status: 400 });
633
- const result = (await ctx.call('website.preflightPublication', { siteId }, url, req));
866
+ // `entryIds` has been on this contract since it was written and nothing
867
+ // passed it, so the only question the screen could ask was "every page on
868
+ // the site" - which is the one that hits the scan ceiling and can then
869
+ // only answer "ask again by id". Naming the published set asks a smaller
870
+ // question the contract answers exactly.
871
+ const scope = url.searchParams.get('scope') === 'published' ? 'published' : 'all';
872
+ const published = scope === 'published'
873
+ ? (await ctx.call('website.listEntries', { siteId, status: 'published' }, url, req))
874
+ : [];
875
+ const result = (await ctx.call('website.preflightPublication', { siteId, ...(scope === 'published' ? { entryIds: published.map((row) => row.id) } : {}) }, url, req));
634
876
  return adminPage(ctx, url, req, {
635
877
  title: 'website_backend.preflight.title',
636
- body: (_, frame) => preflightScreen(_, result, siteId, frame, localeQuery(url)),
878
+ body: (_, frame) => preflightScreen(_, result, siteId, frame, localeQuery(url), scope),
637
879
  });
638
880
  },
639
881
  '/admin/website/menus': (ctx) => async (url, req) => {
@@ -686,6 +928,8 @@ export const routes = {
686
928
  return render(form, resultErrors(result, _));
687
929
  },
688
930
  '/admin/website/sites/{id}/members/{memberId}/remove': (ctx) => async (url, req, params) => {
931
+ if (req.method !== 'POST')
932
+ return text('POST', { status: 405 });
689
933
  const _ = ctx.translate(ctx.localeOf(url, req));
690
934
  const result = await ctx.call('website.removeSiteMember', { id: params.memberId }, url, req);
691
935
  if (!result.ok)
@@ -706,9 +950,15 @@ export const routes = {
706
950
  return text(_('website_backend.error.notFound'), { status: 404 });
707
951
  const render = async (values, errors) => {
708
952
  const rows = (await ctx.call('website.listDomains', { siteId: params.id }, url, req));
953
+ const wanted = url.searchParams.get('edit');
709
954
  return adminPage(ctx, url, req, {
710
955
  title: 'website_backend.domains.title',
711
- body: (_, frame) => siteDomainsScreen(_, site, rows, frame, { values, errors, locale: localeQuery(url) }),
956
+ body: (_, frame) => siteDomainsScreen(_, site, rows, frame, {
957
+ values,
958
+ errors,
959
+ locale: localeQuery(url),
960
+ editing: wanted ? (rows.find((row) => row.id === wanted) ?? null) : null,
961
+ }),
712
962
  });
713
963
  };
714
964
  if (req.method === 'GET')
@@ -727,6 +977,40 @@ export const routes = {
727
977
  return seeOther(inLocale(url, `/admin/website/sites/${params.id}/domains`));
728
978
  return render(form, resultErrors(result, _));
729
979
  },
980
+ /**
981
+ * Correcting a host that is already attached.
982
+ *
983
+ * `saveDomain` is an upsert that promotes a new primary and demotes the old
984
+ * one in the same transaction, and the create route minted a fresh id every
985
+ * time - so re-submitting an existing host only collided with the unique
986
+ * index, and neither the primary nor `redirectToPrimary` could be changed
987
+ * once set.
988
+ */
989
+ '/admin/website/sites/{id}/domains/{domainId}': (ctx) => async (url, req, params) => {
990
+ if (req.method !== 'POST')
991
+ return text('POST', { status: 405 });
992
+ const _ = ctx.translate(ctx.localeOf(url, req));
993
+ const form = await readForm(req);
994
+ const result = await ctx.call('website.saveDomain', {
995
+ id: params.domainId,
996
+ siteId: params.id,
997
+ host: form.host,
998
+ primary: !!form.primary,
999
+ redirectToPrimary: !!form.redirectToPrimary,
1000
+ }, url, req);
1001
+ if (!result.ok)
1002
+ return text(resultErrors(result, _).join('; '), { status: 400 });
1003
+ return seeOther(inLocale(url, `/admin/website/sites/${params.id}/domains`));
1004
+ },
1005
+ '/admin/website/sites/{id}/domains/{domainId}/remove': (ctx) => async (url, req, params) => {
1006
+ if (req.method !== 'POST')
1007
+ return text('POST', { status: 405 });
1008
+ const _ = ctx.translate(ctx.localeOf(url, req));
1009
+ const result = await ctx.call('website.deleteDomain', { id: params.domainId }, url, req);
1010
+ if (!result.ok)
1011
+ return text(resultErrors(result, _).join('; '), { status: 400 });
1012
+ return seeOther(inLocale(url, `/admin/website/sites/${params.id}/domains`));
1013
+ },
730
1014
  /**
731
1015
  * Whether search answers from the content that is actually live.
732
1016
  *
@@ -765,15 +1049,24 @@ export const routes = {
765
1049
  const posted = req.method === 'POST' ? await readForm(req) : null;
766
1050
  const siteId = posted?.siteId || selectedSite(url, sites);
767
1051
  const render = async (values, errors) => {
1052
+ // `listRedirects` has always taken this filter and nothing passed it,
1053
+ // which was of a piece with the route writing `active: true` every
1054
+ // time: there were no inactive rows to look at.
1055
+ const state = url.searchParams.get('state');
768
1056
  const rows = siteId
769
- ? (await ctx.call('website.listRedirects', { siteId }, url, req))
1057
+ ? (await ctx.call('website.listRedirects', {
1058
+ siteId,
1059
+ ...(state === 'active' || state === 'inactive' ? { active: state === 'active' } : {}),
1060
+ }, url, req))
770
1061
  : [];
1062
+ const wanted = url.searchParams.get('edit');
771
1063
  return adminPage(ctx, url, req, {
772
1064
  title: 'website_backend.redirects.title',
773
1065
  body: (_, frame) => redirectsScreen(_, rows, siteOptions(sites), siteId, frame, {
774
1066
  values,
775
1067
  errors,
776
1068
  locale: localeQuery(url),
1069
+ editing: wanted ? (rows.find((row) => row.id === wanted) ?? null) : null,
777
1070
  }),
778
1071
  });
779
1072
  };
@@ -796,6 +1089,54 @@ export const routes = {
796
1089
  return seeOther(inLocale(url, `/admin/website/redirects?site=${encodeURIComponent(siteId)}`));
797
1090
  return render(form, resultErrors(result, _));
798
1091
  },
1092
+ /**
1093
+ * Correcting one that is already there.
1094
+ *
1095
+ * `saveRedirect` is an upsert and the create route minted a fresh id every
1096
+ * time, so a typo could not be fixed: the correction collided with the
1097
+ * unique index on `fromPath` and the wrong row kept the address.
1098
+ */
1099
+ '/admin/website/redirects/{id}': (ctx) => async (url, req, params) => {
1100
+ if (req.method !== 'POST')
1101
+ return text('POST', { status: 405 });
1102
+ const _ = ctx.translate(ctx.localeOf(url, req));
1103
+ const form = await readForm(req);
1104
+ const siteId = form.siteId;
1105
+ if (!siteId)
1106
+ return text(_('website_backend.content.noSite'), { status: 400 });
1107
+ const current = (await ctx.call('website.listRedirects', { siteId }, url, req)).find((row) => row.id === params.id);
1108
+ if (!current)
1109
+ return text(_('website_backend.error.notFound'), { status: 404 });
1110
+ const result = await ctx.call('website.saveRedirect', {
1111
+ id: params.id,
1112
+ siteId,
1113
+ fromPath: form.fromPath,
1114
+ toPath: form.toPath,
1115
+ permanent: !!form.permanent,
1116
+ // An edit is about where the address goes, not whether it is on.
1117
+ active: current.active,
1118
+ }, url, req);
1119
+ if (!result.ok)
1120
+ return text(resultErrors(result, _).join('; '), { status: 400 });
1121
+ return seeOther(inLocale(url, `/admin/website/redirects?site=${encodeURIComponent(siteId)}`));
1122
+ },
1123
+ '/admin/website/redirects/{id}/state': (ctx) => async (url, req, params) => {
1124
+ if (req.method !== 'POST')
1125
+ return text('POST', { status: 405 });
1126
+ const _ = ctx.translate(ctx.localeOf(url, req));
1127
+ const form = await readForm(req);
1128
+ const sites = await sitesOf(ctx, url, req);
1129
+ const siteId = selectedSite(url, sites);
1130
+ if (!siteId)
1131
+ return text(_('website_backend.content.noSite'), { status: 400 });
1132
+ const current = (await ctx.call('website.listRedirects', { siteId }, url, req)).find((row) => row.id === params.id);
1133
+ if (!current)
1134
+ return text(_('website_backend.error.notFound'), { status: 404 });
1135
+ const result = await ctx.call('website.saveRedirect', { ...current, active: form.action === 'activate' }, url, req);
1136
+ if (!result.ok)
1137
+ return text(resultErrors(result, _).join('; '), { status: 400 });
1138
+ return seeOther(inLocale(url, `/admin/website/redirects?site=${encodeURIComponent(siteId)}`));
1139
+ },
799
1140
  '/admin/website/taxonomies/new': (ctx) => async (url, req) => {
800
1141
  const _ = ctx.translate(ctx.localeOf(url, req));
801
1142
  const sites = await sitesOf(ctx, url, req);
@@ -938,10 +1279,11 @@ export const routes = {
938
1279
  }
939
1280
  if (req.method !== 'GET')
940
1281
  return text('GET or POST', { status: 405 });
1282
+ const usage = (await ctx.call('website.mediaUsage', { id: params.id }, url, req));
941
1283
  return adminPage(ctx, url, req, {
942
1284
  title: row.attachmentId,
943
1285
  translate: false,
944
- body: (_, frame) => mediaFormScreen(_, row, frame, { locale: localeQuery(url) }),
1286
+ body: (_, frame) => mediaFormScreen(_, row, frame, { locale: localeQuery(url), usage }),
945
1287
  });
946
1288
  },
947
1289
  '/admin/website/media/{id}/delete': (ctx) => async (url, req, params) => {
@@ -958,6 +1300,24 @@ export const routes = {
958
1300
  },
959
1301
  '/admin/website/menus/new': menuEditRoute(),
960
1302
  '/admin/website/menus/{id}': menuEditRoute(),
1303
+ /**
1304
+ * One place up or down.
1305
+ *
1306
+ * Reordering meant opening each item and typing a number into `position`,
1307
+ * which is arithmetic rather than editing and gets worse the longer the menu
1308
+ * is. The move is a POST because it writes.
1309
+ */
1310
+ '/admin/website/menus/{id}/move': (ctx) => async (url, req, params) => {
1311
+ if (req.method !== 'POST')
1312
+ return text('POST', { status: 405 });
1313
+ const _ = ctx.translate(ctx.localeOf(url, req));
1314
+ const form = await readForm(req);
1315
+ const siteId = form.site || url.searchParams.get('site') || '';
1316
+ const result = await ctx.call('website_menu.moveMenuItem', { id: params.id, direction: form.action }, url, req);
1317
+ if (!result.ok)
1318
+ return text(resultErrors(result, _).join('; '), { status: 400 });
1319
+ return seeOther(inLocale(url, `/admin/website/menus?site=${encodeURIComponent(siteId)}`));
1320
+ },
961
1321
  '/admin/website/menus/{id}/delete': (ctx) => async (url, req, params) => {
962
1322
  if (req.method !== 'POST')
963
1323
  return text('POST', { status: 405 });
@@ -976,10 +1336,16 @@ export const routes = {
976
1336
  const _ = ctx.translate(ctx.localeOf(url, req));
977
1337
  const sites = await sitesOf(ctx, url, req);
978
1338
  const siteId = selectedSite(url, sites);
979
- const rows = siteId ? (await ctx.call('website_form.listForms', { siteId }, url, req)) : [];
1339
+ const chosen = url.searchParams.get('state');
1340
+ const rows = siteId
1341
+ ? (await ctx.call('website_form.listForms', {
1342
+ siteId,
1343
+ ...(chosen === 'active' || chosen === 'inactive' ? { active: chosen === 'active' } : {}),
1344
+ }, url, req))
1345
+ : [];
980
1346
  return adminPage(ctx, url, req, {
981
1347
  title: 'website_backend.forms.title',
982
- body: (_, frame) => formsScreen(_, rows, siteId, frame, localeQuery(url)),
1348
+ body: (_, frame) => formsScreen(_, rows, siteId, frame, localeQuery(url), chosen ?? 'all'),
983
1349
  });
984
1350
  },
985
1351
  '/admin/website/forms/new': (ctx) => async (url, req) => {
@@ -1098,13 +1464,17 @@ export const routes = {
1098
1464
  .filter(Boolean);
1099
1465
  if (!fields.length)
1100
1466
  return text(_('website_backend.error.invalid'), { status: 400 });
1101
- const result = (await ctx.call('website_form.exportSubmissions', { formId: params.id, fields, reason: 'admin.export' }, url, req));
1467
+ // The same narrowing the list is showing. Without it the download and
1468
+ // the screen disagreed and only one of them said so.
1469
+ const chosen = url.searchParams.get('status');
1470
+ const status = chosen && chosen !== 'all' ? chosen : null;
1471
+ const result = (await ctx.call('website_form.exportSubmissions', { formId: params.id, fields, ...(status ? { status } : {}), reason: 'admin.export' }, url, req));
1102
1472
  if (!result.ok)
1103
1473
  return text(resultErrors(result, _).join('; '), { status: 400 });
1104
1474
  const columns = ['_id', '_createdAt', '_status', ...(result.fields ?? [])];
1105
1475
  return withHeaders(text(csvOf(columns, result.rows ?? [])), {
1106
1476
  'content-type': 'text/csv; charset=utf-8',
1107
- 'content-disposition': `attachment; filename="${safeFilename(params.id)}-submissions.csv"`,
1477
+ 'content-disposition': `attachment; filename="${safeFilename(`${params.id}${status ? `-${status}` : ''}`)}-submissions.csv"`,
1108
1478
  });
1109
1479
  },
1110
1480
  /**
@@ -1153,7 +1523,18 @@ export const routes = {
1153
1523
  if (req.method !== 'GET')
1154
1524
  return text('GET', { status: 405 });
1155
1525
  const _ = ctx.translate(ctx.localeOf(url, req));
1156
- const rows = (await ctx.call('website_form.listSubmissions', { formId: params.id }, url, req));
1526
+ const current = pageOf(url);
1527
+ const chosen = url.searchParams.get('status');
1528
+ const status = chosen && chosen !== 'all' ? chosen : null;
1529
+ const [rows, total] = await Promise.all([
1530
+ ctx.call('website_form.listSubmissions', {
1531
+ formId: params.id,
1532
+ ...(status ? { status } : {}),
1533
+ limit: PAGE_SIZE,
1534
+ offset: (current - 1) * PAGE_SIZE,
1535
+ }, url, req),
1536
+ ctx.call('website_form.countSubmissions', { formId: params.id, ...(status ? { status } : {}) }, url, req),
1537
+ ]);
1157
1538
  const sites = await sitesOf(ctx, url, req);
1158
1539
  const siteId = url.searchParams.get('site') || selectedSite(url, sites);
1159
1540
  const forms = siteId
@@ -1176,6 +1557,8 @@ export const routes = {
1176
1557
  fields: form?.summaryFields?.length ? form.summaryFields : schemaFields,
1177
1558
  retentionDays: form?.retentionDays ?? null,
1178
1559
  locale: localeQuery(url),
1560
+ pager: pager(url, current, rows.length, total.count),
1561
+ status: chosen ?? 'all',
1179
1562
  }),
1180
1563
  });
1181
1564
  },