@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.
- package/dist/cli.js +1 -1
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +3 -0
- package/dist/deployment.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/account_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/account_staff_channel/channel-routes.js +1 -12
- package/dist/modules/account_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/attendance/channel-routes.d.ts.map +1 -1
- package/dist/modules/attendance/channel-routes.js +1 -12
- package/dist/modules/attendance/channel-routes.js.map +1 -1
- package/dist/modules/backend/design/HANDOFF.md +2 -2
- package/dist/modules/backend/design/auth.css +12 -0
- package/dist/modules/backend/design/client/table-selection.d.mts +3 -3
- package/dist/modules/backend/design/client/table-selection.d.mts.map +1 -1
- package/dist/modules/backend/design/client/table-selection.mjs +1 -1
- package/dist/modules/backend/design/client/table-selection.mjs.map +4 -4
- package/dist/modules/backend/design/content.css +43 -0
- package/dist/modules/backend/design/design-system.css +1 -1
- package/dist/modules/backend/design/forms.css +84 -1
- package/dist/modules/backend/design/lists.css +26 -18
- package/dist/modules/backend/design/record.css +5 -2
- package/dist/modules/backend/design/responsive.css +5 -6
- package/dist/modules/backend/design/user-workflow.css +257 -0
- package/dist/modules/backend/forms.d.ts +38 -1
- package/dist/modules/backend/forms.d.ts.map +1 -1
- package/dist/modules/backend/forms.js +29 -0
- package/dist/modules/backend/forms.js.map +1 -1
- package/dist/modules/backend/index.d.ts +3 -2
- package/dist/modules/backend/index.d.ts.map +1 -1
- package/dist/modules/backend/index.js +3 -2
- package/dist/modules/backend/index.js.map +1 -1
- package/dist/modules/backend/messages.d.ts.map +1 -1
- package/dist/modules/backend/messages.js +42 -2
- package/dist/modules/backend/messages.js.map +1 -1
- package/dist/modules/channel_api/core.d.ts +30 -0
- package/dist/modules/channel_api/core.d.ts.map +1 -1
- package/dist/modules/channel_api/core.js +28 -0
- package/dist/modules/channel_api/core.js.map +1 -1
- package/dist/modules/crm/functions.d.ts +5 -0
- package/dist/modules/crm/functions.d.ts.map +1 -1
- package/dist/modules/crm/functions.js +105 -10
- package/dist/modules/crm/functions.js.map +1 -1
- package/dist/modules/crm/jobs.d.ts.map +1 -1
- package/dist/modules/crm/jobs.js +1 -0
- package/dist/modules/crm/jobs.js.map +1 -1
- package/dist/modules/crm/operations.d.ts.map +1 -1
- package/dist/modules/crm/operations.js +2 -1
- package/dist/modules/crm/operations.js.map +1 -1
- package/dist/modules/crm_backend/index.d.ts.map +1 -1
- package/dist/modules/crm_backend/index.js +5 -2
- package/dist/modules/crm_backend/index.js.map +1 -1
- package/dist/modules/crm_backend/messages.d.ts +116 -0
- package/dist/modules/crm_backend/messages.d.ts.map +1 -1
- package/dist/modules/crm_backend/messages.js +122 -6
- package/dist/modules/crm_backend/messages.js.map +1 -1
- package/dist/modules/crm_backend/routes.d.ts.map +1 -1
- package/dist/modules/crm_backend/routes.js +434 -174
- package/dist/modules/crm_backend/routes.js.map +1 -1
- package/dist/modules/crm_backend/screens/case-create.d.ts +7 -0
- package/dist/modules/crm_backend/screens/case-create.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/case-create.js +13 -4
- package/dist/modules/crm_backend/screens/case-create.js.map +1 -1
- package/dist/modules/crm_backend/screens/case-detail.d.ts +6 -0
- package/dist/modules/crm_backend/screens/case-detail.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/case-detail.js +73 -23
- package/dist/modules/crm_backend/screens/case-detail.js.map +1 -1
- package/dist/modules/crm_backend/screens/configuration.d.ts +21 -3
- package/dist/modules/crm_backend/screens/configuration.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/configuration.js +260 -55
- package/dist/modules/crm_backend/screens/configuration.js.map +1 -1
- package/dist/modules/crm_backend/screens/index.d.ts +3 -3
- package/dist/modules/crm_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/index.js +2 -2
- package/dist/modules/crm_backend/screens/index.js.map +1 -1
- package/dist/modules/crm_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/crm_staff_channel/channel-routes.js +1 -12
- package/dist/modules/crm_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/flow/membership.d.ts.map +1 -1
- package/dist/modules/flow/membership.js +7 -1
- package/dist/modules/flow/membership.js.map +1 -1
- package/dist/modules/flow/operations.d.ts +10 -4
- package/dist/modules/flow/operations.d.ts.map +1 -1
- package/dist/modules/flow/operations.js +10 -4
- package/dist/modules/flow/operations.js.map +1 -1
- package/dist/modules/flow_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/flow_staff_channel/channel-routes.js +1 -20
- package/dist/modules/flow_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/hospitality_core/messages.d.ts.map +1 -1
- package/dist/modules/hospitality_core/messages.js +44 -0
- package/dist/modules/hospitality_core/messages.js.map +1 -1
- package/dist/modules/hospitality_core/night-audit.d.ts.map +1 -1
- package/dist/modules/hospitality_core/night-audit.js +7 -0
- package/dist/modules/hospitality_core/night-audit.js.map +1 -1
- package/dist/modules/hospitality_core/operations.d.ts.map +1 -1
- package/dist/modules/hospitality_core/operations.js +150 -40
- package/dist/modules/hospitality_core/operations.js.map +1 -1
- package/dist/modules/hospitality_core/routes.d.ts.map +1 -1
- package/dist/modules/hospitality_core/routes.js +24 -2
- package/dist/modules/hospitality_core/routes.js.map +1 -1
- package/dist/modules/hospitality_core/screens/check-out-prep.d.ts +46 -0
- package/dist/modules/hospitality_core/screens/check-out-prep.d.ts.map +1 -0
- package/dist/modules/hospitality_core/screens/check-out-prep.js +133 -0
- package/dist/modules/hospitality_core/screens/check-out-prep.js.map +1 -0
- package/dist/modules/hospitality_core/screens/folio-detail.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/folio-detail.js +2 -4
- package/dist/modules/hospitality_core/screens/folio-detail.js.map +1 -1
- package/dist/modules/hospitality_core/screens/index.d.ts +2 -0
- package/dist/modules/hospitality_core/screens/index.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/index.js +1 -0
- package/dist/modules/hospitality_core/screens/index.js.map +1 -1
- package/dist/modules/hospitality_core/screens/reservation-detail.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/reservation-detail.js +8 -1
- package/dist/modules/hospitality_core/screens/reservation-detail.js.map +1 -1
- package/dist/modules/hospitality_core/screens/shared.d.ts +12 -0
- package/dist/modules/hospitality_core/screens/shared.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/shared.js +25 -4
- package/dist/modules/hospitality_core/screens/shared.js.map +1 -1
- package/dist/modules/hospitality_core/services.d.ts +48 -0
- package/dist/modules/hospitality_core/services.d.ts.map +1 -1
- package/dist/modules/hospitality_core/services.js +70 -0
- package/dist/modules/hospitality_core/services.js.map +1 -1
- package/dist/modules/hospitality_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/hospitality_staff_channel/channel-routes.js +1 -12
- package/dist/modules/hospitality_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/inventory_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/inventory_staff_channel/channel-routes.js +1 -12
- package/dist/modules/inventory_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/loyalty/admin-functions.d.ts +2 -0
- package/dist/modules/loyalty/admin-functions.d.ts.map +1 -1
- package/dist/modules/loyalty/admin-functions.js +96 -20
- package/dist/modules/loyalty/admin-functions.js.map +1 -1
- package/dist/modules/loyalty/engine.d.ts +4 -1
- package/dist/modules/loyalty/engine.d.ts.map +1 -1
- package/dist/modules/loyalty/engine.js +125 -18
- package/dist/modules/loyalty/engine.js.map +1 -1
- package/dist/modules/loyalty/index.d.ts.map +1 -1
- package/dist/modules/loyalty/index.js +4 -3
- package/dist/modules/loyalty/index.js.map +1 -1
- package/dist/modules/loyalty/jobs.d.ts.map +1 -1
- package/dist/modules/loyalty/jobs.js +0 -1
- package/dist/modules/loyalty/jobs.js.map +1 -1
- package/dist/modules/loyalty/membership-functions.d.ts +5 -0
- package/dist/modules/loyalty/membership-functions.d.ts.map +1 -1
- package/dist/modules/loyalty/membership-functions.js +37 -23
- package/dist/modules/loyalty/membership-functions.js.map +1 -1
- package/dist/modules/loyalty/messages.d.ts +14 -0
- package/dist/modules/loyalty/messages.d.ts.map +1 -1
- package/dist/modules/loyalty/messages.js +14 -0
- package/dist/modules/loyalty/messages.js.map +1 -1
- package/dist/modules/loyalty/models.d.ts.map +1 -1
- package/dist/modules/loyalty/models.js +17 -0
- package/dist/modules/loyalty/models.js.map +1 -1
- package/dist/modules/loyalty/order-functions.d.ts.map +1 -1
- package/dist/modules/loyalty/order-functions.js +70 -17
- package/dist/modules/loyalty/order-functions.js.map +1 -1
- package/dist/modules/loyalty/program-design.d.ts +11 -0
- package/dist/modules/loyalty/program-design.d.ts.map +1 -0
- package/dist/modules/loyalty/program-design.js +356 -0
- package/dist/modules/loyalty/program-design.js.map +1 -0
- package/dist/modules/loyalty/stats.d.ts +0 -13
- package/dist/modules/loyalty/stats.d.ts.map +1 -1
- package/dist/modules/loyalty/stats.js +11 -6
- package/dist/modules/loyalty/stats.js.map +1 -1
- package/dist/modules/loyalty/stored-value.d.ts.map +1 -1
- package/dist/modules/loyalty/stored-value.js +7 -2
- package/dist/modules/loyalty/stored-value.js.map +1 -1
- package/dist/modules/loyalty/types.d.ts +1 -1
- package/dist/modules/loyalty/types.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/design-messages.d.ts +221 -0
- package/dist/modules/loyalty_backend/design-messages.d.ts.map +1 -0
- package/dist/modules/loyalty_backend/design-messages.js +221 -0
- package/dist/modules/loyalty_backend/design-messages.js.map +1 -0
- package/dist/modules/loyalty_backend/index.js +224 -412
- package/dist/modules/loyalty_backend/index.js.map +1 -1
- package/dist/modules/loyalty_backend/messages.d.ts +642 -214
- package/dist/modules/loyalty_backend/messages.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/messages.js +19 -13
- package/dist/modules/loyalty_backend/messages.js.map +1 -1
- package/dist/modules/loyalty_backend/program-workspace.d.ts +4 -0
- package/dist/modules/loyalty_backend/program-workspace.d.ts.map +1 -0
- package/dist/modules/loyalty_backend/program-workspace.js +600 -0
- package/dist/modules/loyalty_backend/program-workspace.js.map +1 -0
- package/dist/modules/loyalty_backend/screens/index.d.ts +12 -36
- package/dist/modules/loyalty_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/screens/index.js +148 -292
- package/dist/modules/loyalty_backend/screens/index.js.map +1 -1
- package/dist/modules/loyalty_backend/screens/page-frame.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/screens/page-frame.js +2 -2
- package/dist/modules/loyalty_backend/screens/page-frame.js.map +1 -1
- package/dist/modules/loyalty_pos/functions.d.ts.map +1 -1
- package/dist/modules/loyalty_pos/functions.js +1 -0
- package/dist/modules/loyalty_pos/functions.js.map +1 -1
- package/dist/modules/loyalty_pos/index.js +1 -1
- package/dist/modules/loyalty_pos/index.js.map +1 -1
- package/dist/modules/loyalty_sale/functions.d.ts +1 -0
- package/dist/modules/loyalty_sale/functions.d.ts.map +1 -1
- package/dist/modules/loyalty_sale/functions.js +1 -0
- package/dist/modules/loyalty_sale/functions.js.map +1 -1
- package/dist/modules/partner_backend/client/partner.css +3 -1
- package/dist/modules/partner_backend/index.d.ts.map +1 -1
- package/dist/modules/partner_backend/index.js +4 -0
- package/dist/modules/partner_backend/index.js.map +1 -1
- package/dist/modules/partner_backend/routes.d.ts.map +1 -1
- package/dist/modules/partner_backend/routes.js +6 -1
- package/dist/modules/partner_backend/routes.js.map +1 -1
- package/dist/modules/partner_backend/screens/form.d.ts +1 -0
- package/dist/modules/partner_backend/screens/form.d.ts.map +1 -1
- package/dist/modules/partner_backend/screens/form.js +1 -0
- package/dist/modules/partner_backend/screens/form.js.map +1 -1
- package/dist/modules/purchase_staff_channel/order-routes.d.ts.map +1 -1
- package/dist/modules/purchase_staff_channel/order-routes.js +1 -12
- package/dist/modules/purchase_staff_channel/order-routes.js.map +1 -1
- package/dist/modules/quality_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/quality_staff_channel/channel-routes.js +1 -12
- package/dist/modules/quality_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/sale/functions.d.ts.map +1 -1
- package/dist/modules/sale/functions.js +27 -17
- package/dist/modules/sale/functions.js.map +1 -1
- package/dist/modules/sale_staff_channel/channel-routes.d.ts.map +1 -1
- package/dist/modules/sale_staff_channel/channel-routes.js +1 -12
- package/dist/modules/sale_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/sale_staff_channel/order-routes.d.ts.map +1 -1
- package/dist/modules/sale_staff_channel/order-routes.js +1 -12
- package/dist/modules/sale_staff_channel/order-routes.js.map +1 -1
- package/dist/modules/stock_staff_channel/operation-routes.d.ts.map +1 -1
- package/dist/modules/stock_staff_channel/operation-routes.js +1 -12
- package/dist/modules/stock_staff_channel/operation-routes.js.map +1 -1
- package/dist/modules/user/authorization.d.ts +64 -1
- package/dist/modules/user/authorization.d.ts.map +1 -1
- package/dist/modules/user/authorization.js +445 -53
- package/dist/modules/user/authorization.js.map +1 -1
- package/dist/modules/user/functions.d.ts.map +1 -1
- package/dist/modules/user/functions.js +45 -23
- package/dist/modules/user/functions.js.map +1 -1
- package/dist/modules/user/index.d.ts +2 -1
- package/dist/modules/user/index.d.ts.map +1 -1
- package/dist/modules/user/index.js +1 -1
- package/dist/modules/user/index.js.map +1 -1
- package/dist/modules/user/models.d.ts.map +1 -1
- package/dist/modules/user/models.js +5 -0
- package/dist/modules/user/models.js.map +1 -1
- package/dist/modules/user/roles.d.ts.map +1 -1
- package/dist/modules/user/roles.js +2 -1
- package/dist/modules/user/roles.js.map +1 -1
- package/dist/modules/user_backend/access-routes.d.ts +31 -0
- package/dist/modules/user_backend/access-routes.d.ts.map +1 -0
- package/dist/modules/user_backend/access-routes.js +447 -0
- package/dist/modules/user_backend/access-routes.js.map +1 -0
- package/dist/modules/user_backend/index.d.ts +1 -0
- package/dist/modules/user_backend/index.d.ts.map +1 -1
- package/dist/modules/user_backend/index.js +1 -0
- package/dist/modules/user_backend/index.js.map +1 -1
- package/dist/modules/user_backend/messages.d.ts +4 -0
- package/dist/modules/user_backend/messages.d.ts.map +1 -1
- package/dist/modules/user_backend/messages.js +4 -0
- package/dist/modules/user_backend/messages.js.map +1 -1
- package/dist/modules/user_backend/routes.d.ts.map +1 -1
- package/dist/modules/user_backend/routes.js +38 -3
- package/dist/modules/user_backend/routes.js.map +1 -1
- package/dist/modules/user_backend/screens/user-form.d.ts +1 -0
- package/dist/modules/user_backend/screens/user-form.d.ts.map +1 -1
- package/dist/modules/user_backend/screens/user-form.js +28 -0
- package/dist/modules/user_backend/screens/user-form.js.map +1 -1
- package/dist/modules/website/cms.d.ts.map +1 -1
- package/dist/modules/website/cms.js +283 -6
- package/dist/modules/website/cms.js.map +1 -1
- package/dist/modules/website/index.d.ts.map +1 -1
- package/dist/modules/website/index.js +12 -0
- package/dist/modules/website/index.js.map +1 -1
- package/dist/modules/website/media-usage.d.ts +38 -0
- package/dist/modules/website/media-usage.d.ts.map +1 -0
- package/dist/modules/website/media-usage.js +87 -0
- package/dist/modules/website/media-usage.js.map +1 -0
- package/dist/modules/website/paths.js +1 -1
- package/dist/modules/website/paths.js.map +1 -1
- package/dist/modules/website/sections.d.ts.map +1 -1
- package/dist/modules/website/sections.js +12 -1
- package/dist/modules/website/sections.js.map +1 -1
- package/dist/modules/website_backend/index.js +138 -0
- package/dist/modules/website_backend/index.js.map +1 -1
- package/dist/modules/website_backend/menus.d.ts.map +1 -1
- package/dist/modules/website_backend/menus.js +11 -0
- package/dist/modules/website_backend/menus.js.map +1 -1
- package/dist/modules/website_backend/routes.d.ts.map +1 -1
- package/dist/modules/website_backend/routes.js +414 -31
- package/dist/modules/website_backend/routes.js.map +1 -1
- package/dist/modules/website_backend/screens/index.d.ts +98 -6
- package/dist/modules/website_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/website_backend/screens/index.js +600 -154
- package/dist/modules/website_backend/screens/index.js.map +1 -1
- package/dist/modules/website_backend/screens/page-frame.d.ts +3 -1
- package/dist/modules/website_backend/screens/page-frame.d.ts.map +1 -1
- package/dist/modules/website_backend/screens/page-frame.js +1 -1
- package/dist/modules/website_backend/screens/page-frame.js.map +1 -1
- package/dist/modules/website_menu/functions.d.ts.map +1 -1
- package/dist/modules/website_menu/functions.js +51 -0
- package/dist/modules/website_menu/functions.js.map +1 -1
- package/dist/modules/website_menu/index.js +2 -0
- package/dist/modules/website_menu/index.js.map +1 -1
- package/dist/modules/website_search/functions.d.ts.map +1 -1
- package/dist/modules/website_search/functions.js +10 -120
- package/dist/modules/website_search/functions.js.map +1 -1
- package/dist/modules/website_search/index.js +2 -0
- package/dist/modules/website_search/index.js.map +1 -1
- package/dist/modules/website_search/jobs.d.ts +3 -0
- package/dist/modules/website_search/jobs.d.ts.map +1 -0
- package/dist/modules/website_search/jobs.js +101 -0
- package/dist/modules/website_search/jobs.js.map +1 -0
- package/dist/modules/website_search/rebuild.d.ts +33 -0
- package/dist/modules/website_search/rebuild.d.ts.map +1 -0
- package/dist/modules/website_search/rebuild.js +125 -0
- package/dist/modules/website_search/rebuild.js.map +1 -0
- package/dist/permission-catalogue.d.ts.map +1 -1
- package/dist/permission-catalogue.js +21 -1
- package/dist/permission-catalogue.js.map +1 -1
- package/dist/scaffold/index.js +1 -1
- package/dist/ui/chrome.d.ts +8 -0
- package/dist/ui/chrome.d.ts.map +1 -1
- package/dist/ui/chrome.js +9 -1
- package/dist/ui/chrome.js.map +1 -1
- package/dist/ui/client/table-selection-view.d.ts.map +1 -1
- package/dist/ui/client/table-selection-view.js +161 -5
- package/dist/ui/client/table-selection-view.js.map +1 -1
- package/dist/ui/client/user-workflow.d.ts +2 -0
- package/dist/ui/client/user-workflow.d.ts.map +1 -0
- package/dist/ui/client/user-workflow.js +141 -0
- package/dist/ui/client/user-workflow.js.map +1 -0
- package/dist/ui/form.d.ts +9 -0
- package/dist/ui/form.d.ts.map +1 -1
- package/dist/ui/form.js +29 -15
- package/dist/ui/form.js.map +1 -1
- package/dist/ui/icons.d.ts.map +1 -1
- package/dist/ui/icons.js +2 -0
- package/dist/ui/icons.js.map +1 -1
- package/dist/ui/index.d.ts +7 -3
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +6 -3
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/layout.d.ts.map +1 -1
- package/dist/ui/layout.js +14 -4
- package/dist/ui/layout.js.map +1 -1
- package/dist/ui/nav.d.ts.map +1 -1
- package/dist/ui/nav.js +2 -1
- package/dist/ui/nav.js.map +1 -1
- package/dist/ui/state.d.ts +20 -1
- package/dist/ui/state.d.ts.map +1 -1
- package/dist/ui/state.js +17 -0
- package/dist/ui/state.js.map +1 -1
- package/dist/ui/table.js +1 -1
- package/dist/ui/table.js.map +1 -1
- package/dist/ui/user-workflow.d.ts +72 -0
- package/dist/ui/user-workflow.d.ts.map +1 -0
- package/dist/ui/user-workflow.js +144 -0
- package/dist/ui/user-workflow.js.map +1 -0
- 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 {
|
|
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
|
-
?
|
|
131
|
-
|
|
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
|
|
150
|
-
|
|
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', {
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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, {
|
|
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', {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
},
|