@ketvietlab/ketsuite 0.1.29 → 0.1.30
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/modules/account_backend/index.js +0 -6
- package/dist/modules/account_backend/index.js.map +1 -1
- package/dist/modules/account_backend/screens/account-defaults.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/account-defaults.js +1 -1
- package/dist/modules/account_backend/screens/account-defaults.js.map +1 -1
- package/dist/modules/account_backend/screens/account-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/account-form.js +3 -1
- package/dist/modules/account_backend/screens/account-form.js.map +1 -1
- package/dist/modules/account_backend/screens/accounts-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/accounts-list.js +1 -1
- package/dist/modules/account_backend/screens/accounts-list.js.map +1 -1
- package/dist/modules/account_backend/screens/customer-invoice-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/customer-invoice-form.js +1 -1
- package/dist/modules/account_backend/screens/customer-invoice-form.js.map +1 -1
- package/dist/modules/account_backend/screens/customer-invoices-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/customer-invoices-list.js +1 -1
- package/dist/modules/account_backend/screens/customer-invoices-list.js.map +1 -1
- package/dist/modules/account_backend/screens/journal-entries-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/journal-entries-list.js +1 -1
- package/dist/modules/account_backend/screens/journal-entries-list.js.map +1 -1
- package/dist/modules/account_backend/screens/journal-entry-create.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/journal-entry-create.js +1 -1
- package/dist/modules/account_backend/screens/journal-entry-create.js.map +1 -1
- package/dist/modules/account_backend/screens/journal-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/journal-form.js +3 -3
- package/dist/modules/account_backend/screens/journal-form.js.map +1 -1
- package/dist/modules/account_backend/screens/journals-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/journals-list.js +1 -1
- package/dist/modules/account_backend/screens/journals-list.js.map +1 -1
- package/dist/modules/account_backend/screens/move-detail.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/move-detail.js +3 -1
- package/dist/modules/account_backend/screens/move-detail.js.map +1 -1
- package/dist/modules/account_backend/screens/opening-balances.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/opening-balances.js +2 -2
- package/dist/modules/account_backend/screens/opening-balances.js.map +1 -1
- package/dist/modules/account_backend/screens/overview.js +4 -2
- package/dist/modules/account_backend/screens/overview.js.map +1 -1
- package/dist/modules/account_backend/screens/payment-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/payment-form.js +1 -1
- package/dist/modules/account_backend/screens/payment-form.js.map +1 -1
- package/dist/modules/account_backend/screens/payment-term-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/payment-term-form.js +4 -2
- package/dist/modules/account_backend/screens/payment-term-form.js.map +1 -1
- package/dist/modules/account_backend/screens/payment-terms-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/payment-terms-list.js +1 -1
- package/dist/modules/account_backend/screens/payment-terms-list.js.map +1 -1
- package/dist/modules/account_backend/screens/payments-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/payments-list.js +1 -1
- package/dist/modules/account_backend/screens/payments-list.js.map +1 -1
- package/dist/modules/account_backend/screens/period-closes.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/period-closes.js +1 -1
- package/dist/modules/account_backend/screens/period-closes.js.map +1 -1
- package/dist/modules/account_backend/screens/tax-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/tax-form.js +3 -1
- package/dist/modules/account_backend/screens/tax-form.js.map +1 -1
- package/dist/modules/account_backend/screens/taxes-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/taxes-list.js +1 -1
- package/dist/modules/account_backend/screens/taxes-list.js.map +1 -1
- package/dist/modules/account_backend/screens/vendor-bill-form.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/vendor-bill-form.js +1 -1
- package/dist/modules/account_backend/screens/vendor-bill-form.js.map +1 -1
- package/dist/modules/account_backend/screens/vendor-bills-list.d.ts.map +1 -1
- package/dist/modules/account_backend/screens/vendor-bills-list.js +1 -1
- package/dist/modules/account_backend/screens/vendor-bills-list.js.map +1 -1
- package/dist/modules/account_partner_backend/screens/accounting-terms.d.ts.map +1 -1
- package/dist/modules/account_partner_backend/screens/accounting-terms.js +1 -1
- package/dist/modules/account_partner_backend/screens/accounting-terms.js.map +1 -1
- package/dist/modules/activity_backend/index.d.ts.map +1 -1
- package/dist/modules/activity_backend/index.js +0 -2
- package/dist/modules/activity_backend/index.js.map +1 -1
- package/dist/modules/activity_backend/screens/activities-list.d.ts.map +1 -1
- package/dist/modules/activity_backend/screens/activities-list.js +1 -1
- package/dist/modules/activity_backend/screens/activities-list.js.map +1 -1
- package/dist/modules/address_backend/index.d.ts.map +1 -1
- package/dist/modules/address_backend/index.js +0 -2
- package/dist/modules/address_backend/index.js.map +1 -1
- package/dist/modules/address_backend/screens/catalogs-list.d.ts.map +1 -1
- package/dist/modules/address_backend/screens/catalogs-list.js +1 -1
- package/dist/modules/address_backend/screens/catalogs-list.js.map +1 -1
- package/dist/modules/attendance_backend/screens/credentials.d.ts.map +1 -1
- package/dist/modules/attendance_backend/screens/credentials.js +1 -1
- package/dist/modules/attendance_backend/screens/credentials.js.map +1 -1
- package/dist/modules/attendance_backend/screens/period.d.ts.map +1 -1
- package/dist/modules/attendance_backend/screens/period.js +1 -1
- package/dist/modules/attendance_backend/screens/period.js.map +1 -1
- package/dist/modules/backend/current-company.d.ts +4 -0
- package/dist/modules/backend/current-company.d.ts.map +1 -0
- package/dist/modules/backend/current-company.js +27 -0
- package/dist/modules/backend/current-company.js.map +1 -0
- package/dist/modules/backend/design/client/backend-shell.d.mts +2 -2
- package/dist/modules/backend/design/client/backend-shell.d.mts.map +1 -1
- package/dist/modules/backend/design/client/backend-shell.mjs +1 -1
- package/dist/modules/backend/design/client/backend-shell.mjs.map +4 -4
- package/dist/modules/backend/design/client/ket-table.d.mts +2 -2
- package/dist/modules/backend/design/client/ket-table.d.mts.map +1 -1
- package/dist/modules/backend/design/client/ket-table.mjs +1 -1
- package/dist/modules/backend/design/client/ket-table.mjs.map +3 -3
- package/dist/modules/backend/design/client/search-filter.d.mts +2 -2
- package/dist/modules/backend/design/client/search-filter.d.mts.map +1 -1
- package/dist/modules/backend/design/client/search-filter.mjs +1 -1
- package/dist/modules/backend/design/client/search-filter.mjs.map +3 -3
- package/dist/modules/backend/design/forms.css +31 -0
- package/dist/modules/backend/index.d.ts +1 -0
- package/dist/modules/backend/index.d.ts.map +1 -1
- package/dist/modules/backend/index.js +1 -0
- package/dist/modules/backend/index.js.map +1 -1
- package/dist/modules/backend/optional-read.d.ts +4 -0
- package/dist/modules/backend/optional-read.d.ts.map +1 -0
- package/dist/modules/backend/optional-read.js +7 -0
- package/dist/modules/backend/optional-read.js.map +1 -0
- package/dist/modules/company_backend/index.d.ts.map +1 -1
- package/dist/modules/company_backend/index.js +0 -2
- package/dist/modules/company_backend/index.js.map +1 -1
- package/dist/modules/company_backend/screens/branch-form.d.ts.map +1 -1
- package/dist/modules/company_backend/screens/branch-form.js +3 -1
- package/dist/modules/company_backend/screens/branch-form.js.map +1 -1
- package/dist/modules/company_backend/screens/companies-list.d.ts.map +1 -1
- package/dist/modules/company_backend/screens/companies-list.js +1 -1
- package/dist/modules/company_backend/screens/companies-list.js.map +1 -1
- package/dist/modules/company_backend/screens/company-form.d.ts.map +1 -1
- package/dist/modules/company_backend/screens/company-form.js +1 -3
- package/dist/modules/company_backend/screens/company-form.js.map +1 -1
- package/dist/modules/company_backend/screens/context.d.ts.map +1 -1
- package/dist/modules/company_backend/screens/context.js +1 -1
- package/dist/modules/company_backend/screens/context.js.map +1 -1
- package/dist/modules/company_backend/screens/hierarchy.d.ts.map +1 -1
- package/dist/modules/company_backend/screens/hierarchy.js +1 -1
- package/dist/modules/company_backend/screens/hierarchy.js.map +1 -1
- package/dist/modules/crm_backend/client/crm-case-modal.d.mts +7 -7
- package/dist/modules/crm_backend/client/crm-case-modal.d.mts.map +1 -1
- package/dist/modules/crm_backend/client/crm-case-modal.mjs +1 -1
- package/dist/modules/crm_backend/client/crm-case-modal.mjs.map +3 -3
- package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts +50 -50
- package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts.map +1 -1
- package/dist/modules/crm_backend/client/crm-configuration-modal.mjs +1 -1
- package/dist/modules/crm_backend/client/crm-configuration-modal.mjs.map +3 -3
- package/dist/modules/crm_backend/messages.d.ts +0 -10
- package/dist/modules/crm_backend/messages.d.ts.map +1 -1
- package/dist/modules/crm_backend/messages.js +0 -10
- package/dist/modules/crm_backend/messages.js.map +1 -1
- package/dist/modules/crm_backend/screens/activity-planner.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/activity-planner.js +1 -1
- package/dist/modules/crm_backend/screens/activity-planner.js.map +1 -1
- package/dist/modules/crm_backend/screens/case-create.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/case-create.js +1 -1
- package/dist/modules/crm_backend/screens/case-create.js.map +1 -1
- package/dist/modules/crm_backend/screens/case-detail.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/case-detail.js +2 -2
- package/dist/modules/crm_backend/screens/case-detail.js.map +1 -1
- package/dist/modules/crm_backend/screens/cases-list.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/cases-list.js +1 -1
- package/dist/modules/crm_backend/screens/cases-list.js.map +1 -1
- package/dist/modules/crm_backend/screens/configuration.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/configuration.js +1 -1
- package/dist/modules/crm_backend/screens/configuration.js.map +1 -1
- package/dist/modules/crm_backend/screens/leaderboard.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/leaderboard.js +1 -1
- package/dist/modules/crm_backend/screens/leaderboard.js.map +1 -1
- package/dist/modules/crm_backend/screens/pipeline.d.ts.map +1 -1
- package/dist/modules/crm_backend/screens/pipeline.js +1 -1
- package/dist/modules/crm_backend/screens/pipeline.js.map +1 -1
- package/dist/modules/flow/domain/command-check.d.ts +3 -0
- package/dist/modules/flow/domain/command-check.d.ts.map +1 -0
- package/dist/modules/flow/domain/command-check.js +9 -0
- package/dist/modules/flow/domain/command-check.js.map +1 -0
- package/dist/modules/flow/domain/command.d.ts +33 -0
- package/dist/modules/flow/domain/command.d.ts.map +1 -0
- package/dist/modules/flow/domain/command.js +35 -0
- package/dist/modules/flow/domain/command.js.map +1 -0
- package/dist/modules/flow/domain/issue-bulk.d.ts +50 -0
- package/dist/modules/flow/domain/issue-bulk.d.ts.map +1 -0
- package/dist/modules/flow/domain/issue-bulk.js +97 -0
- package/dist/modules/flow/domain/issue-bulk.js.map +1 -0
- package/dist/modules/flow/domain/issue-dependency.d.ts +19 -0
- package/dist/modules/flow/domain/issue-dependency.d.ts.map +1 -0
- package/dist/modules/flow/domain/issue-dependency.js +59 -0
- package/dist/modules/flow/domain/issue-dependency.js.map +1 -0
- package/dist/modules/flow/domain/issue-discussion.d.ts +52 -0
- package/dist/modules/flow/domain/issue-discussion.d.ts.map +1 -0
- package/dist/modules/flow/domain/issue-discussion.js +114 -0
- package/dist/modules/flow/domain/issue-discussion.js.map +1 -0
- package/dist/modules/flow/domain/issue-transfer.d.ts +62 -0
- package/dist/modules/flow/domain/issue-transfer.d.ts.map +1 -0
- package/dist/modules/flow/domain/issue-transfer.js +171 -0
- package/dist/modules/flow/domain/issue-transfer.js.map +1 -0
- package/dist/modules/flow/domain/issue-validation.d.ts +68 -0
- package/dist/modules/flow/domain/issue-validation.d.ts.map +1 -0
- package/dist/modules/flow/domain/issue-validation.js +136 -0
- package/dist/modules/flow/domain/issue-validation.js.map +1 -0
- package/dist/modules/flow/domain/issue-write.d.ts +118 -0
- package/dist/modules/flow/domain/issue-write.d.ts.map +1 -0
- package/dist/modules/flow/domain/issue-write.js +372 -0
- package/dist/modules/flow/domain/issue-write.js.map +1 -0
- package/dist/modules/flow/domain/sprint-lifecycle.d.ts +61 -0
- package/dist/modules/flow/domain/sprint-lifecycle.d.ts.map +1 -0
- package/dist/modules/flow/domain/sprint-lifecycle.js +145 -0
- package/dist/modules/flow/domain/sprint-lifecycle.js.map +1 -0
- package/dist/modules/flow/functions/board.d.ts +7 -0
- package/dist/modules/flow/functions/board.d.ts.map +1 -0
- package/dist/modules/flow/functions/board.js +59 -0
- package/dist/modules/flow/functions/board.js.map +1 -0
- package/dist/modules/flow/functions/column.d.ts +8 -0
- package/dist/modules/flow/functions/column.d.ts.map +1 -0
- package/dist/modules/flow/functions/column.js +64 -0
- package/dist/modules/flow/functions/column.js.map +1 -0
- package/dist/modules/flow/functions/effects.d.ts +21 -0
- package/dist/modules/flow/functions/effects.d.ts.map +1 -0
- package/dist/modules/flow/functions/effects.js +96 -0
- package/dist/modules/flow/functions/effects.js.map +1 -0
- package/dist/modules/flow/functions/entity-save.d.ts +4 -0
- package/dist/modules/flow/functions/entity-save.d.ts.map +1 -0
- package/dist/modules/flow/functions/entity-save.js +60 -0
- package/dist/modules/flow/functions/entity-save.js.map +1 -0
- package/dist/modules/flow/functions/epic.d.ts +26 -0
- package/dist/modules/flow/functions/epic.d.ts.map +1 -0
- package/dist/modules/flow/functions/epic.js +151 -0
- package/dist/modules/flow/functions/epic.js.map +1 -0
- package/dist/modules/flow/functions/field.d.ts +9 -0
- package/dist/modules/flow/functions/field.d.ts.map +1 -0
- package/dist/modules/flow/functions/field.js +117 -0
- package/dist/modules/flow/functions/field.js.map +1 -0
- package/dist/modules/flow/functions/index.d.ts +5 -0
- package/dist/modules/flow/functions/index.d.ts.map +1 -0
- package/dist/modules/flow/functions/index.js +42 -0
- package/dist/modules/flow/functions/index.js.map +1 -0
- package/dist/modules/flow/functions/issue-batch.d.ts +6 -0
- package/dist/modules/flow/functions/issue-batch.d.ts.map +1 -0
- package/dist/modules/flow/functions/issue-batch.js +149 -0
- package/dist/modules/flow/functions/issue-batch.js.map +1 -0
- package/dist/modules/flow/functions/issue-dependency.d.ts +8 -0
- package/dist/modules/flow/functions/issue-dependency.d.ts.map +1 -0
- package/dist/modules/flow/functions/issue-dependency.js +54 -0
- package/dist/modules/flow/functions/issue-dependency.js.map +1 -0
- package/dist/modules/flow/functions/issue-discussion.d.ts +6 -0
- package/dist/modules/flow/functions/issue-discussion.d.ts.map +1 -0
- package/dist/modules/flow/functions/issue-discussion.js +86 -0
- package/dist/modules/flow/functions/issue-discussion.js.map +1 -0
- package/dist/modules/flow/functions/issue-read.d.ts +18 -0
- package/dist/modules/flow/functions/issue-read.d.ts.map +1 -0
- package/dist/modules/flow/functions/issue-read.js +159 -0
- package/dist/modules/flow/functions/issue-read.js.map +1 -0
- package/dist/modules/flow/functions/issue-write.d.ts +9 -0
- package/dist/modules/flow/functions/issue-write.d.ts.map +1 -0
- package/dist/modules/flow/functions/issue-write.js +149 -0
- package/dist/modules/flow/functions/issue-write.js.map +1 -0
- package/dist/modules/flow/functions/issueType.d.ts +8 -0
- package/dist/modules/flow/functions/issueType.d.ts.map +1 -0
- package/dist/modules/flow/functions/issueType.js +66 -0
- package/dist/modules/flow/functions/issueType.js.map +1 -0
- package/dist/modules/flow/functions/page.d.ts +22 -0
- package/dist/modules/flow/functions/page.d.ts.map +1 -0
- package/dist/modules/flow/functions/page.js +202 -0
- package/dist/modules/flow/functions/page.js.map +1 -0
- package/dist/modules/flow/functions/project.d.ts +26 -0
- package/dist/modules/flow/functions/project.d.ts.map +1 -0
- package/dist/modules/flow/functions/project.js +381 -0
- package/dist/modules/flow/functions/project.js.map +1 -0
- package/dist/modules/flow/functions/sprint.d.ts +17 -0
- package/dist/modules/flow/functions/sprint.d.ts.map +1 -0
- package/dist/modules/flow/functions/sprint.js +173 -0
- package/dist/modules/flow/functions/sprint.js.map +1 -0
- package/dist/modules/flow/functions/tag.d.ts +14 -0
- package/dist/modules/flow/functions/tag.d.ts.map +1 -0
- package/dist/modules/flow/functions/tag.js +88 -0
- package/dist/modules/flow/functions/tag.js.map +1 -0
- package/dist/modules/flow/index.d.ts +8 -2
- package/dist/modules/flow/index.d.ts.map +1 -1
- package/dist/modules/flow/index.js +7 -2
- package/dist/modules/flow/index.js.map +1 -1
- package/dist/modules/flow/pages.d.ts +1 -1
- package/dist/modules/flow/pages.d.ts.map +1 -1
- package/dist/modules/flow/pages.js +1 -1
- package/dist/modules/flow/pages.js.map +1 -1
- package/dist/modules/flow/queries/calendar.d.ts +21 -0
- package/dist/modules/flow/queries/calendar.d.ts.map +1 -0
- package/dist/modules/flow/queries/calendar.js +31 -0
- package/dist/modules/flow/queries/calendar.js.map +1 -0
- package/dist/modules/flow/queries/issue-dependencies.d.ts +17 -0
- package/dist/modules/flow/queries/issue-dependencies.d.ts.map +1 -0
- package/dist/modules/flow/queries/issue-dependencies.js +46 -0
- package/dist/modules/flow/queries/issue-dependencies.js.map +1 -0
- package/dist/modules/flow/queries/issue-followers.d.ts +6 -0
- package/dist/modules/flow/queries/issue-followers.d.ts.map +1 -0
- package/dist/modules/flow/queries/issue-followers.js +18 -0
- package/dist/modules/flow/queries/issue-followers.js.map +1 -0
- package/dist/modules/flow/queries/issue-list.d.ts +153 -0
- package/dist/modules/flow/queries/issue-list.d.ts.map +1 -0
- package/dist/modules/flow/queries/issue-list.js +439 -0
- package/dist/modules/flow/queries/issue-list.js.map +1 -0
- package/dist/modules/flow/queries/options.d.ts +4 -0
- package/dist/modules/flow/queries/options.d.ts.map +1 -0
- package/dist/modules/flow/queries/options.js +13 -0
- package/dist/modules/flow/queries/options.js.map +1 -0
- package/dist/modules/flow/queries/progress.d.ts +24 -0
- package/dist/modules/flow/queries/progress.d.ts.map +1 -0
- package/dist/modules/flow/queries/progress.js +54 -0
- package/dist/modules/flow/queries/progress.js.map +1 -0
- package/dist/modules/flow/queries/project-list.d.ts +4 -0
- package/dist/modules/flow/queries/project-list.d.ts.map +1 -0
- package/dist/modules/flow/queries/project-list.js +19 -0
- package/dist/modules/flow/queries/project-list.js.map +1 -0
- package/dist/modules/flow_backend/messages.d.ts +1 -2
- package/dist/modules/flow_backend/messages.d.ts.map +1 -1
- package/dist/modules/flow_backend/messages.js +0 -2
- package/dist/modules/flow_backend/messages.js.map +1 -1
- package/dist/modules/flow_backend/routes.js +1 -1
- package/dist/modules/flow_backend/routes.js.map +1 -1
- package/dist/modules/flow_backend/screens/all-epics.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/all-epics.js +1 -1
- package/dist/modules/flow_backend/screens/all-epics.js.map +1 -1
- package/dist/modules/flow_backend/screens/all-pages.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/all-pages.js +1 -1
- package/dist/modules/flow_backend/screens/all-pages.js.map +1 -1
- package/dist/modules/flow_backend/screens/epic-detail.js +1 -1
- package/dist/modules/flow_backend/screens/epic-detail.js.map +1 -1
- package/dist/modules/flow_backend/screens/gantt.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/gantt.js +1 -1
- package/dist/modules/flow_backend/screens/gantt.js.map +1 -1
- package/dist/modules/flow_backend/screens/issue-detail.js +2 -1
- package/dist/modules/flow_backend/screens/issue-detail.js.map +1 -1
- package/dist/modules/flow_backend/screens/issues.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/issues.js +1 -1
- package/dist/modules/flow_backend/screens/issues.js.map +1 -1
- package/dist/modules/flow_backend/screens/map.js +1 -1
- package/dist/modules/flow_backend/screens/map.js.map +1 -1
- package/dist/modules/flow_backend/screens/my-work.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/my-work.js +1 -1
- package/dist/modules/flow_backend/screens/my-work.js.map +1 -1
- package/dist/modules/flow_backend/screens/page-detail.js +1 -1
- package/dist/modules/flow_backend/screens/page-detail.js.map +1 -1
- package/dist/modules/flow_backend/screens/project-create.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/project-create.js +1 -1
- package/dist/modules/flow_backend/screens/project-create.js.map +1 -1
- package/dist/modules/flow_backend/screens/project-pages.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/project-pages.js +1 -1
- package/dist/modules/flow_backend/screens/project-pages.js.map +1 -1
- package/dist/modules/flow_backend/screens/projects-list.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/projects-list.js +1 -1
- package/dist/modules/flow_backend/screens/projects-list.js.map +1 -1
- package/dist/modules/flow_backend/screens/sprints.d.ts.map +1 -1
- package/dist/modules/flow_backend/screens/sprints.js +1 -1
- package/dist/modules/flow_backend/screens/sprints.js.map +1 -1
- package/dist/modules/flow_staff_channel/channel-routes.js +1 -1
- package/dist/modules/flow_staff_channel/channel-routes.js.map +1 -1
- package/dist/modules/hospitality_billing/messages.d.ts.map +1 -1
- package/dist/modules/hospitality_billing/messages.js +0 -2
- package/dist/modules/hospitality_billing/messages.js.map +1 -1
- package/dist/modules/hospitality_billing/screens/billing.d.ts.map +1 -1
- package/dist/modules/hospitality_billing/screens/billing.js +1 -1
- package/dist/modules/hospitality_billing/screens/billing.js.map +1 -1
- package/dist/modules/hospitality_billing/screens/page-frame.d.ts +1 -2
- package/dist/modules/hospitality_billing/screens/page-frame.d.ts.map +1 -1
- package/dist/modules/hospitality_billing/screens/page-frame.js +1 -1
- package/dist/modules/hospitality_billing/screens/page-frame.js.map +1 -1
- package/dist/modules/hospitality_core/messages.js +2 -2
- package/dist/modules/hospitality_core/messages.js.map +1 -1
- package/dist/modules/hospitality_core/screens/check-out-prep.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/check-out-prep.js +1 -1
- package/dist/modules/hospitality_core/screens/check-out-prep.js.map +1 -1
- package/dist/modules/hospitality_core/screens/front-desk.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/front-desk.js +40 -40
- package/dist/modules/hospitality_core/screens/front-desk.js.map +1 -1
- package/dist/modules/hospitality_core/screens/page-frame.d.ts +2 -3
- package/dist/modules/hospitality_core/screens/page-frame.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/page-frame.js +3 -3
- package/dist/modules/hospitality_core/screens/page-frame.js.map +1 -1
- package/dist/modules/hospitality_core/screens/tape-chart.d.ts.map +1 -1
- package/dist/modules/hospitality_core/screens/tape-chart.js +3 -12
- package/dist/modules/hospitality_core/screens/tape-chart.js.map +1 -1
- package/dist/modules/hr_backend/screens/employee-form.d.ts.map +1 -1
- package/dist/modules/hr_backend/screens/employee-form.js +1 -1
- package/dist/modules/hr_backend/screens/employee-form.js.map +1 -1
- package/dist/modules/hr_backend/screens/leaves-list.d.ts.map +1 -1
- package/dist/modules/hr_backend/screens/leaves-list.js +1 -1
- package/dist/modules/hr_backend/screens/leaves-list.js.map +1 -1
- package/dist/modules/livedoc/islands.d.ts +9 -0
- package/dist/modules/livedoc/islands.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/messages.d.ts +0 -10
- package/dist/modules/loyalty_backend/messages.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/messages.js +0 -10
- package/dist/modules/loyalty_backend/messages.js.map +1 -1
- package/dist/modules/loyalty_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/loyalty_backend/screens/index.js +5 -5
- package/dist/modules/loyalty_backend/screens/index.js.map +1 -1
- package/dist/modules/loyalty_backend/screens/page-frame.d.ts +2 -3
- 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/mail_backend/index.d.ts.map +1 -1
- package/dist/modules/mail_backend/index.js +0 -2
- package/dist/modules/mail_backend/index.js.map +1 -1
- package/dist/modules/mail_backend/screens/inbox-list.d.ts.map +1 -1
- package/dist/modules/mail_backend/screens/inbox-list.js +1 -1
- package/dist/modules/mail_backend/screens/inbox-list.js.map +1 -1
- package/dist/modules/mail_inbound_backend/index.d.ts.map +1 -1
- package/dist/modules/mail_inbound_backend/index.js +0 -2
- package/dist/modules/mail_inbound_backend/index.js.map +1 -1
- package/dist/modules/mail_inbound_backend/screens/inbound-list.d.ts.map +1 -1
- package/dist/modules/mail_inbound_backend/screens/inbound-list.js +1 -1
- package/dist/modules/mail_inbound_backend/screens/inbound-list.js.map +1 -1
- package/dist/modules/mail_transport_backend/index.d.ts.map +1 -1
- package/dist/modules/mail_transport_backend/index.js +0 -2
- package/dist/modules/mail_transport_backend/index.js.map +1 -1
- package/dist/modules/mail_transport_backend/screens/outbox-list.d.ts.map +1 -1
- package/dist/modules/mail_transport_backend/screens/outbox-list.js +1 -1
- package/dist/modules/mail_transport_backend/screens/outbox-list.js.map +1 -1
- package/dist/modules/manufacturing_backend/screens/order-execution.d.ts.map +1 -1
- package/dist/modules/manufacturing_backend/screens/order-execution.js +1 -1
- package/dist/modules/manufacturing_backend/screens/order-execution.js.map +1 -1
- package/dist/modules/oauth_backend/messages.d.ts +0 -4
- package/dist/modules/oauth_backend/messages.d.ts.map +1 -1
- package/dist/modules/oauth_backend/messages.js +0 -4
- package/dist/modules/oauth_backend/messages.js.map +1 -1
- package/dist/modules/oauth_backend/screens/index.d.ts.map +1 -1
- package/dist/modules/oauth_backend/screens/index.js +5 -5
- package/dist/modules/oauth_backend/screens/index.js.map +1 -1
- package/dist/modules/partner_backend/screens/form.d.ts.map +1 -1
- package/dist/modules/partner_backend/screens/form.js +3 -1
- package/dist/modules/partner_backend/screens/form.js.map +1 -1
- package/dist/modules/partner_backend/screens/list.d.ts.map +1 -1
- package/dist/modules/partner_backend/screens/list.js +1 -1
- package/dist/modules/partner_backend/screens/list.js.map +1 -1
- package/dist/modules/partner_backend/screens/new.d.ts.map +1 -1
- package/dist/modules/partner_backend/screens/new.js +1 -1
- package/dist/modules/partner_backend/screens/new.js.map +1 -1
- package/dist/modules/pricing_backend/index.js +0 -2
- package/dist/modules/pricing_backend/index.js.map +1 -1
- package/dist/modules/pricing_backend/screens/pricelist-detail.d.ts.map +1 -1
- package/dist/modules/pricing_backend/screens/pricelist-detail.js +1 -1
- package/dist/modules/pricing_backend/screens/pricelist-detail.js.map +1 -1
- package/dist/modules/pricing_backend/screens/pricelists-list.d.ts.map +1 -1
- package/dist/modules/pricing_backend/screens/pricelists-list.js +1 -1
- package/dist/modules/pricing_backend/screens/pricelists-list.js.map +1 -1
- package/dist/modules/product_backend/client/attribute-modal.d.mts +4 -4
- package/dist/modules/product_backend/client/attribute-modal.d.mts.map +1 -1
- package/dist/modules/product_backend/client/attribute-modal.mjs +1 -1
- package/dist/modules/product_backend/client/attribute-modal.mjs.map +3 -3
- package/dist/modules/product_backend/client/product-modal.d.mts +8 -8
- package/dist/modules/product_backend/client/product-modal.d.mts.map +1 -1
- package/dist/modules/product_backend/client/product-modal.mjs +1 -1
- package/dist/modules/product_backend/client/product-modal.mjs.map +3 -3
- package/dist/modules/product_backend/client/variant-editor.d.mts +3 -3
- package/dist/modules/product_backend/client/variant-editor.d.mts.map +1 -1
- package/dist/modules/product_backend/client/variant-editor.mjs +1 -1
- package/dist/modules/product_backend/client/variant-editor.mjs.map +3 -3
- package/dist/modules/product_backend/screens/detail.d.ts.map +1 -1
- package/dist/modules/product_backend/screens/detail.js +3 -2
- package/dist/modules/product_backend/screens/detail.js.map +1 -1
- package/dist/modules/product_backend/screens/variant.d.ts.map +1 -1
- package/dist/modules/product_backend/screens/variant.js +3 -2
- package/dist/modules/product_backend/screens/variant.js.map +1 -1
- package/dist/modules/purchase_backend/client/purchase-order-modal.d.mts +185 -0
- package/dist/modules/purchase_backend/client/purchase-order-modal.d.mts.map +1 -0
- package/dist/modules/purchase_backend/client/purchase-order-modal.mjs +2 -0
- package/dist/modules/purchase_backend/client/purchase-order-modal.mjs.map +7 -0
- package/dist/modules/purchase_backend/client/vendor-pricelist-modal.d.mts +52 -0
- package/dist/modules/purchase_backend/client/vendor-pricelist-modal.d.mts.map +1 -0
- package/dist/modules/purchase_backend/client/vendor-pricelist-modal.mjs +2 -0
- package/dist/modules/purchase_backend/client/vendor-pricelist-modal.mjs.map +7 -0
- package/dist/modules/purchase_backend/index.js +83 -14
- package/dist/modules/purchase_backend/index.js.map +1 -1
- package/dist/modules/purchase_backend/modal/context.d.ts +4 -0
- package/dist/modules/purchase_backend/modal/context.d.ts.map +1 -0
- package/dist/modules/purchase_backend/modal/context.js +94 -0
- package/dist/modules/purchase_backend/modal/context.js.map +1 -0
- package/dist/modules/purchase_backend/modal/order-modal-view.d.ts +18 -0
- package/dist/modules/purchase_backend/modal/order-modal-view.d.ts.map +1 -0
- package/dist/modules/purchase_backend/modal/order-modal-view.js +290 -0
- package/dist/modules/purchase_backend/modal/order-modal-view.js.map +1 -0
- package/dist/modules/purchase_backend/modal/pricelist-context.d.ts +3 -0
- package/dist/modules/purchase_backend/modal/pricelist-context.d.ts.map +1 -0
- package/dist/modules/purchase_backend/modal/pricelist-context.js +36 -0
- package/dist/modules/purchase_backend/modal/pricelist-context.js.map +1 -0
- package/dist/modules/purchase_backend/modal/pricelist-view.d.ts +12 -0
- package/dist/modules/purchase_backend/modal/pricelist-view.d.ts.map +1 -0
- package/dist/modules/purchase_backend/modal/pricelist-view.js +78 -0
- package/dist/modules/purchase_backend/modal/pricelist-view.js.map +1 -0
- package/dist/modules/purchase_backend/screens/order-detail.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/order-detail.js +4 -2
- package/dist/modules/purchase_backend/screens/order-detail.js.map +1 -1
- package/dist/modules/purchase_backend/screens/overview.d.ts +5 -0
- package/dist/modules/purchase_backend/screens/overview.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/overview.js +27 -6
- package/dist/modules/purchase_backend/screens/overview.js.map +1 -1
- package/dist/modules/purchase_backend/screens/rfq-create.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/rfq-create.js +1 -1
- package/dist/modules/purchase_backend/screens/rfq-create.js.map +1 -1
- package/dist/modules/purchase_backend/screens/rfqs-list.d.ts +1 -1
- package/dist/modules/purchase_backend/screens/rfqs-list.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/rfqs-list.js +1 -1
- package/dist/modules/purchase_backend/screens/rfqs-list.js.map +1 -1
- package/dist/modules/purchase_backend/screens/vendor-pricelist-create.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/vendor-pricelist-create.js +1 -1
- package/dist/modules/purchase_backend/screens/vendor-pricelist-create.js.map +1 -1
- package/dist/modules/purchase_backend/screens/vendor-pricelists-list.d.ts +2 -1
- package/dist/modules/purchase_backend/screens/vendor-pricelists-list.d.ts.map +1 -1
- package/dist/modules/purchase_backend/screens/vendor-pricelists-list.js +2 -2
- package/dist/modules/purchase_backend/screens/vendor-pricelists-list.js.map +1 -1
- package/dist/modules/purchase_backend/search.d.ts +1 -1
- package/dist/modules/sale/functions/line-edit.d.ts +4 -0
- package/dist/modules/sale/functions/line-edit.d.ts.map +1 -0
- package/dist/modules/sale/functions/line-edit.js +120 -0
- package/dist/modules/sale/functions/line-edit.js.map +1 -0
- package/dist/modules/sale/index.d.ts.map +1 -1
- package/dist/modules/sale/index.js +16 -1
- package/dist/modules/sale/index.js.map +1 -1
- package/dist/modules/sale_backend/client/invoicing-policy-modal.d.mts +53 -0
- package/dist/modules/sale_backend/client/invoicing-policy-modal.d.mts.map +1 -0
- package/dist/modules/sale_backend/client/invoicing-policy-modal.mjs +2 -0
- package/dist/modules/sale_backend/client/invoicing-policy-modal.mjs.map +7 -0
- package/dist/modules/sale_backend/client/sale-order-modal.d.mts +161 -0
- package/dist/modules/sale_backend/client/sale-order-modal.d.mts.map +1 -0
- package/dist/modules/sale_backend/client/sale-order-modal.mjs +2 -0
- package/dist/modules/sale_backend/client/sale-order-modal.mjs.map +7 -0
- package/dist/modules/sale_backend/index.js +81 -20
- package/dist/modules/sale_backend/index.js.map +1 -1
- package/dist/modules/sale_backend/modal/context.d.ts +4 -0
- package/dist/modules/sale_backend/modal/context.d.ts.map +1 -0
- package/dist/modules/sale_backend/modal/context.js +94 -0
- package/dist/modules/sale_backend/modal/context.js.map +1 -0
- package/dist/modules/sale_backend/modal/order-modal-view.d.ts +18 -0
- package/dist/modules/sale_backend/modal/order-modal-view.d.ts.map +1 -0
- package/dist/modules/sale_backend/modal/order-modal-view.js +289 -0
- package/dist/modules/sale_backend/modal/order-modal-view.js.map +1 -0
- package/dist/modules/sale_backend/modal/policy-context.d.ts +3 -0
- package/dist/modules/sale_backend/modal/policy-context.d.ts.map +1 -0
- package/dist/modules/sale_backend/modal/policy-context.js +26 -0
- package/dist/modules/sale_backend/modal/policy-context.js.map +1 -0
- package/dist/modules/sale_backend/modal/policy-view.d.ts +11 -0
- package/dist/modules/sale_backend/modal/policy-view.d.ts.map +1 -0
- package/dist/modules/sale_backend/modal/policy-view.js +70 -0
- package/dist/modules/sale_backend/modal/policy-view.js.map +1 -0
- package/dist/modules/sale_backend/screens/invoicing-policies-list.d.ts +1 -1
- package/dist/modules/sale_backend/screens/invoicing-policies-list.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/invoicing-policies-list.js +1 -1
- package/dist/modules/sale_backend/screens/invoicing-policies-list.js.map +1 -1
- package/dist/modules/sale_backend/screens/invoicing-policy-create.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/invoicing-policy-create.js +1 -1
- package/dist/modules/sale_backend/screens/invoicing-policy-create.js.map +1 -1
- package/dist/modules/sale_backend/screens/order-detail.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/order-detail.js +3 -1
- package/dist/modules/sale_backend/screens/order-detail.js.map +1 -1
- package/dist/modules/sale_backend/screens/overview.d.ts +3 -0
- package/dist/modules/sale_backend/screens/overview.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/overview.js +22 -14
- package/dist/modules/sale_backend/screens/overview.js.map +1 -1
- package/dist/modules/sale_backend/screens/quotation-create.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/quotation-create.js +1 -1
- package/dist/modules/sale_backend/screens/quotation-create.js.map +1 -1
- package/dist/modules/sale_backend/screens/quotations-list.d.ts +1 -1
- package/dist/modules/sale_backend/screens/quotations-list.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/quotations-list.js +1 -1
- package/dist/modules/sale_backend/screens/quotations-list.js.map +1 -1
- package/dist/modules/sale_backend/screens/sales-orders-list.d.ts.map +1 -1
- package/dist/modules/sale_backend/screens/sales-orders-list.js +1 -1
- package/dist/modules/sale_backend/screens/sales-orders-list.js.map +1 -1
- package/dist/modules/stock/functions/inventory-preview.d.ts +4 -0
- package/dist/modules/stock/functions/inventory-preview.d.ts.map +1 -0
- package/dist/modules/stock/functions/inventory-preview.js +68 -0
- package/dist/modules/stock/functions/inventory-preview.js.map +1 -0
- package/dist/modules/stock/index.d.ts.map +1 -1
- package/dist/modules/stock/index.js +14 -1
- package/dist/modules/stock/index.js.map +1 -1
- package/dist/modules/stock_backend/client/inventory-count-modal.d.mts +60 -0
- package/dist/modules/stock_backend/client/inventory-count-modal.d.mts.map +1 -0
- package/dist/modules/stock_backend/client/inventory-count-modal.mjs +2 -0
- package/dist/modules/stock_backend/client/inventory-count-modal.mjs.map +7 -0
- package/dist/modules/stock_backend/client/stock-configuration-modal.d.mts +105 -0
- package/dist/modules/stock_backend/client/stock-configuration-modal.d.mts.map +1 -0
- package/dist/modules/stock_backend/client/stock-configuration-modal.mjs +2 -0
- package/dist/modules/stock_backend/client/stock-configuration-modal.mjs.map +7 -0
- package/dist/modules/stock_backend/client/stock-transfer-modal.d.mts +130 -0
- package/dist/modules/stock_backend/client/stock-transfer-modal.d.mts.map +1 -0
- package/dist/modules/stock_backend/client/stock-transfer-modal.mjs +2 -0
- package/dist/modules/stock_backend/client/stock-transfer-modal.mjs.map +7 -0
- package/dist/modules/stock_backend/functions/index.d.ts +10 -0
- package/dist/modules/stock_backend/functions/index.d.ts.map +1 -0
- package/dist/modules/stock_backend/functions/index.js +20 -0
- package/dist/modules/stock_backend/functions/index.js.map +1 -0
- package/dist/modules/stock_backend/functions.d.ts +1 -9
- package/dist/modules/stock_backend/functions.d.ts.map +1 -1
- package/dist/modules/stock_backend/functions.js +1 -18
- package/dist/modules/stock_backend/functions.js.map +1 -1
- package/dist/modules/stock_backend/index.js +52 -0
- package/dist/modules/stock_backend/index.js.map +1 -1
- package/dist/modules/stock_backend/islands.d.ts +8 -0
- package/dist/modules/stock_backend/islands.d.ts.map +1 -1
- package/dist/modules/stock_backend/islands.js +41 -0
- package/dist/modules/stock_backend/islands.js.map +1 -1
- package/dist/modules/stock_backend/modal/configuration-context.d.ts +3 -0
- package/dist/modules/stock_backend/modal/configuration-context.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/configuration-context.js +65 -0
- package/dist/modules/stock_backend/modal/configuration-context.js.map +1 -0
- package/dist/modules/stock_backend/modal/configuration-view.d.ts +21 -0
- package/dist/modules/stock_backend/modal/configuration-view.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/configuration-view.js +201 -0
- package/dist/modules/stock_backend/modal/configuration-view.js.map +1 -0
- package/dist/modules/stock_backend/modal/configuration.d.ts +20 -0
- package/dist/modules/stock_backend/modal/configuration.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/configuration.js +117 -0
- package/dist/modules/stock_backend/modal/configuration.js.map +1 -0
- package/dist/modules/stock_backend/modal/inventory-context.d.ts +3 -0
- package/dist/modules/stock_backend/modal/inventory-context.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/inventory-context.js +37 -0
- package/dist/modules/stock_backend/modal/inventory-context.js.map +1 -0
- package/dist/modules/stock_backend/modal/inventory-view.d.ts +14 -0
- package/dist/modules/stock_backend/modal/inventory-view.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/inventory-view.js +142 -0
- package/dist/modules/stock_backend/modal/inventory-view.js.map +1 -0
- package/dist/modules/stock_backend/modal/transfer-context.d.ts +3 -0
- package/dist/modules/stock_backend/modal/transfer-context.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/transfer-context.js +54 -0
- package/dist/modules/stock_backend/modal/transfer-context.js.map +1 -0
- package/dist/modules/stock_backend/modal/transfer-view.d.ts +15 -0
- package/dist/modules/stock_backend/modal/transfer-view.d.ts.map +1 -0
- package/dist/modules/stock_backend/modal/transfer-view.js +252 -0
- package/dist/modules/stock_backend/modal/transfer-view.js.map +1 -0
- package/dist/modules/stock_backend/routes.d.ts.map +1 -1
- package/dist/modules/stock_backend/routes.js +117 -34
- package/dist/modules/stock_backend/routes.js.map +1 -1
- package/dist/modules/stock_backend/screens/forecast.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/forecast.js +5 -32
- package/dist/modules/stock_backend/screens/forecast.js.map +1 -1
- package/dist/modules/stock_backend/screens/inventory.d.ts +3 -1
- package/dist/modules/stock_backend/screens/inventory.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/inventory.js +27 -74
- package/dist/modules/stock_backend/screens/inventory.js.map +1 -1
- package/dist/modules/stock_backend/screens/location-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/location-create.js +1 -1
- package/dist/modules/stock_backend/screens/location-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/locations-list.d.ts +1 -1
- package/dist/modules/stock_backend/screens/locations-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/locations-list.js +1 -1
- package/dist/modules/stock_backend/screens/locations-list.js.map +1 -1
- package/dist/modules/stock_backend/screens/lot-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/lot-create.js +1 -1
- package/dist/modules/stock_backend/screens/lot-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/lot-detail.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/lot-detail.js +3 -2
- package/dist/modules/stock_backend/screens/lot-detail.js.map +1 -1
- package/dist/modules/stock_backend/screens/lots-list.d.ts +1 -1
- package/dist/modules/stock_backend/screens/lots-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/lots-list.js +1 -1
- package/dist/modules/stock_backend/screens/lots-list.js.map +1 -1
- package/dist/modules/stock_backend/screens/overview.d.ts +9 -0
- package/dist/modules/stock_backend/screens/overview.d.ts.map +1 -0
- package/dist/modules/stock_backend/screens/overview.js +38 -0
- package/dist/modules/stock_backend/screens/overview.js.map +1 -0
- package/dist/modules/stock_backend/screens/picking-type-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/picking-type-create.js +1 -1
- package/dist/modules/stock_backend/screens/picking-type-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/picking-types-list.d.ts +1 -1
- package/dist/modules/stock_backend/screens/picking-types-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/picking-types-list.js +1 -1
- package/dist/modules/stock_backend/screens/picking-types-list.js.map +1 -1
- package/dist/modules/stock_backend/screens/replenishment-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/replenishment-create.js +1 -1
- package/dist/modules/stock_backend/screens/replenishment-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/replenishment-list.d.ts +2 -2
- package/dist/modules/stock_backend/screens/replenishment-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/replenishment-list.js +2 -2
- package/dist/modules/stock_backend/screens/replenishment-list.js.map +1 -1
- package/dist/modules/stock_backend/screens/stock-route-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/stock-route-create.js +1 -1
- package/dist/modules/stock_backend/screens/stock-route-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/stock-route-detail.js +2 -1
- package/dist/modules/stock_backend/screens/stock-route-detail.js.map +1 -1
- package/dist/modules/stock_backend/screens/stock-routes-list.d.ts +1 -1
- package/dist/modules/stock_backend/screens/stock-routes-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/stock-routes-list.js +1 -1
- package/dist/modules/stock_backend/screens/stock-routes-list.js.map +1 -1
- package/dist/modules/stock_backend/screens/transfer-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/transfer-create.js +1 -1
- package/dist/modules/stock_backend/screens/transfer-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/transfer-detail.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/transfer-detail.js +3 -2
- package/dist/modules/stock_backend/screens/transfer-detail.js.map +1 -1
- package/dist/modules/stock_backend/screens/transfers-list.d.ts +1 -1
- package/dist/modules/stock_backend/screens/transfers-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/transfers-list.js +1 -1
- package/dist/modules/stock_backend/screens/transfers-list.js.map +1 -1
- package/dist/modules/stock_backend/screens/warehouse-create.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/warehouse-create.js +1 -1
- package/dist/modules/stock_backend/screens/warehouse-create.js.map +1 -1
- package/dist/modules/stock_backend/screens/warehouses-list.d.ts +1 -1
- package/dist/modules/stock_backend/screens/warehouses-list.d.ts.map +1 -1
- package/dist/modules/stock_backend/screens/warehouses-list.js +1 -1
- package/dist/modules/stock_backend/screens/warehouses-list.js.map +1 -1
- package/dist/modules/stock_backend/search.d.ts +29 -0
- package/dist/modules/stock_backend/search.d.ts.map +1 -1
- package/dist/modules/stock_backend/search.js +22 -0
- package/dist/modules/stock_backend/search.js.map +1 -1
- package/dist/modules/user_backend/client/access-policy-modal.d.mts +4 -4
- package/dist/modules/user_backend/client/access-policy-modal.d.mts.map +1 -1
- package/dist/modules/user_backend/client/access-policy-modal.mjs +1 -1
- package/dist/modules/user_backend/client/access-policy-modal.mjs.map +3 -3
- package/dist/modules/user_backend/client/role-modal.d.mts +6 -6
- package/dist/modules/user_backend/client/role-modal.d.mts.map +1 -1
- package/dist/modules/user_backend/client/role-modal.mjs +1 -1
- package/dist/modules/user_backend/client/role-modal.mjs.map +3 -3
- package/dist/modules/user_backend/client/user-modal.d.mts +8 -8
- package/dist/modules/user_backend/client/user-modal.d.mts.map +1 -1
- package/dist/modules/user_backend/client/user-modal.mjs +1 -1
- package/dist/modules/user_backend/client/user-modal.mjs.map +3 -3
- package/dist/modules/user_backend/messages.d.ts +0 -6
- package/dist/modules/user_backend/messages.d.ts.map +1 -1
- package/dist/modules/user_backend/messages.js +0 -6
- package/dist/modules/user_backend/messages.js.map +1 -1
- package/dist/modules/user_backend/screens/access-policies-list.d.ts.map +1 -1
- package/dist/modules/user_backend/screens/access-policies-list.js +1 -1
- package/dist/modules/user_backend/screens/access-policies-list.js.map +1 -1
- package/dist/modules/user_backend/screens/profile-form.d.ts.map +1 -1
- package/dist/modules/user_backend/screens/profile-form.js +1 -1
- package/dist/modules/user_backend/screens/profile-form.js.map +1 -1
- package/dist/modules/user_backend/screens/roles-list.d.ts.map +1 -1
- package/dist/modules/user_backend/screens/roles-list.js +1 -1
- package/dist/modules/user_backend/screens/roles-list.js.map +1 -1
- package/dist/modules/user_backend/screens/users-list.d.ts.map +1 -1
- package/dist/modules/user_backend/screens/users-list.js +1 -1
- package/dist/modules/user_backend/screens/users-list.js.map +1 -1
- package/dist/modules/website_backend/screens/page-frame.d.ts +2 -3
- package/dist/modules/website_backend/screens/page-frame.d.ts.map +1 -1
- package/dist/modules/website_backend/screens/page-frame.js +2 -2
- package/dist/modules/website_backend/screens/page-frame.js.map +1 -1
- package/dist/permission-catalogue.d.ts.map +1 -1
- package/dist/permission-catalogue.js +2 -0
- package/dist/permission-catalogue.js.map +1 -1
- package/dist/role-templates.d.ts.map +1 -1
- package/dist/role-templates.js +23 -3
- package/dist/role-templates.js.map +1 -1
- package/dist/scaffold/index.js +1 -1
- package/dist/ui/client/live-doc-shell.d.ts +27 -1
- package/dist/ui/client/live-doc-shell.d.ts.map +1 -1
- package/dist/ui/client/live-doc-shell.js +43 -5
- package/dist/ui/client/live-doc-shell.js.map +1 -1
- package/dist/ui/client/live-doc-tail.d.ts +4 -0
- package/dist/ui/client/live-doc-tail.d.ts.map +1 -0
- package/dist/ui/client/live-doc-tail.js +21 -0
- package/dist/ui/client/live-doc-tail.js.map +1 -0
- package/dist/ui/client/live-doc-view.d.ts +18 -0
- package/dist/ui/client/live-doc-view.d.ts.map +1 -1
- package/dist/ui/client/live-doc-view.js +183 -4
- package/dist/ui/client/live-doc-view.js.map +1 -1
- package/dist/ui/client/live-doc.css +81 -0
- package/dist/ui/client/live-doc.d.mts +6 -3
- package/dist/ui/client/live-doc.d.mts.map +1 -1
- package/dist/ui/client/live-doc.mjs +14 -10
- package/dist/ui/client/live-doc.mjs.map +4 -4
- package/dist/ui/layout.d.ts +2 -2
- package/dist/ui/layout.d.ts.map +1 -1
- package/dist/ui/layout.js +3 -3
- package/dist/ui/layout.js.map +1 -1
- package/package.json +4 -4
- package/dist/modules/flow/functions.d.ts +0 -3
- package/dist/modules/flow/functions.d.ts.map +0 -1
- package/dist/modules/flow/functions.js +0 -1857
- package/dist/modules/flow/functions.js.map +0 -1
- package/dist/modules/flow/operations.d.ts +0 -414
- package/dist/modules/flow/operations.d.ts.map +0 -1
- package/dist/modules/flow/operations.js +0 -1671
- package/dist/modules/flow/operations.js.map +0 -1
|
@@ -1,1857 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { deleteFrom, defineFn, desc, eq, from, inArray } from '@ketvietlab/ketjs';
|
|
3
|
-
import { FIELD_KINDS } from './types.js';
|
|
4
|
-
import { actorRequired, addComment, bulkIssues, copyIssue, epicTotals, sprintTotals, archiveIssue, restoreIssue, startFollowing, stopFollowing, addDependency, assignSprint, closeSprint, deleteSprint, commandKey, dependenciesFor, groupIssues, issueBuckets, issue, invalid, issueDetail, listIssues, moveIssue, n, normalized, saveIssue, startSprint, transferIssue, } from './operations.js';
|
|
5
|
-
import { emptyIssueListState } from './search.js';
|
|
6
|
-
import { addMember, canReadProject, membersOf, readableProject, readableRow, removeMember, visibleProjects, visibleRows, } from './membership.js';
|
|
7
|
-
import { projectsWithMyWork, projectStateOf, projectStats } from './projects.js';
|
|
8
|
-
import { archivePage, listAllPages, listPages, movePage, pageDetail, reorderPage, restorePage, savePage, } from './pages.js';
|
|
9
|
-
/**
|
|
10
|
-
* What passing the membership gate costs in effects.
|
|
11
|
-
*
|
|
12
|
-
* Spread into every command that touches a project's data. Named because it is
|
|
13
|
-
* not a capability one function happens to need — it is the condition all of
|
|
14
|
-
* them read under, and a list copied at thirty call sites is a list that will
|
|
15
|
-
* disagree with itself.
|
|
16
|
-
*/
|
|
17
|
-
const membershipEffects = [
|
|
18
|
-
'read:flow.Project',
|
|
19
|
-
'read:flow.ProjectMember',
|
|
20
|
-
'read:flow.ProjectAccessGrant',
|
|
21
|
-
'read:user.User',
|
|
22
|
-
];
|
|
23
|
-
const flowReadEffects = [
|
|
24
|
-
'read:flow.Project',
|
|
25
|
-
// The membership gate every Flow read now passes through — see membership.ts.
|
|
26
|
-
// Named here rather than at each call site because it is not a capability one
|
|
27
|
-
// function happens to need; it is the condition all of them read under.
|
|
28
|
-
'read:flow.ProjectMember',
|
|
29
|
-
'read:flow.ProjectAccessGrant',
|
|
30
|
-
'read:flow.Column',
|
|
31
|
-
'read:flow.IssueType',
|
|
32
|
-
'read:flow.FieldDef',
|
|
33
|
-
'read:mail.Follower',
|
|
34
|
-
'read:flow.IssueFieldValue',
|
|
35
|
-
'read:flow.Epic',
|
|
36
|
-
'read:flow.Sprint',
|
|
37
|
-
'read:flow.Issue',
|
|
38
|
-
'read:flow.IssueDependency',
|
|
39
|
-
'read:flow.Tag',
|
|
40
|
-
'read:flow.IssueTag',
|
|
41
|
-
'read:user.User',
|
|
42
|
-
'read:mail.Thread',
|
|
43
|
-
'read:mail.Message',
|
|
44
|
-
];
|
|
45
|
-
/**
|
|
46
|
-
* What writing one system entry to an issue's thread costs in effects.
|
|
47
|
-
*
|
|
48
|
-
* `postMessage` resolves the author, addresses the followers and writes the
|
|
49
|
-
* notification, so a command that leaves a timeline entry touches the same mail
|
|
50
|
-
* tables a comment does. Named here because three commands now do it.
|
|
51
|
-
*/
|
|
52
|
-
const timelineEntryEffects = [
|
|
53
|
-
'read:mail.Thread',
|
|
54
|
-
'read:mail.Follower',
|
|
55
|
-
'write:mail.Follower',
|
|
56
|
-
'read:mail.FollowerSubtype',
|
|
57
|
-
'write:mail.FollowerSubtype',
|
|
58
|
-
'read:mail.Subtype',
|
|
59
|
-
'write:mail.Message',
|
|
60
|
-
'write:mail.TrackingValue',
|
|
61
|
-
'write:mail.Notification',
|
|
62
|
-
'read:user.User',
|
|
63
|
-
'read:partner.Partner',
|
|
64
|
-
];
|
|
65
|
-
const issueWriteEffects = [
|
|
66
|
-
...flowReadEffects,
|
|
67
|
-
'write:flow.Issue',
|
|
68
|
-
'write:flow.IssueTag',
|
|
69
|
-
'write:flow.IssueFieldValue',
|
|
70
|
-
'write:mail.Thread',
|
|
71
|
-
// Saving an issue now subscribes its author and its assignee to the thread,
|
|
72
|
-
// and writes a system entry when it changes hands — so it touches the same
|
|
73
|
-
// mail tables a comment does.
|
|
74
|
-
'read:mail.Follower',
|
|
75
|
-
'write:mail.Follower',
|
|
76
|
-
'read:mail.FollowerSubtype',
|
|
77
|
-
'write:mail.FollowerSubtype',
|
|
78
|
-
'read:mail.Subtype',
|
|
79
|
-
'write:mail.Message',
|
|
80
|
-
'write:mail.Mention',
|
|
81
|
-
'write:mail.MessageAttachment',
|
|
82
|
-
'write:mail.TrackingValue',
|
|
83
|
-
'write:mail.Notification',
|
|
84
|
-
'read:partner.Partner',
|
|
85
|
-
];
|
|
86
|
-
const commentEffects = [
|
|
87
|
-
'read:flow.Issue',
|
|
88
|
-
'read:mail.Thread',
|
|
89
|
-
'write:mail.Thread',
|
|
90
|
-
'read:mail.Message',
|
|
91
|
-
'write:mail.Message',
|
|
92
|
-
'read:mail.Follower',
|
|
93
|
-
'read:mail.FollowerSubtype',
|
|
94
|
-
'read:mail.Subtype',
|
|
95
|
-
'write:mail.Mention',
|
|
96
|
-
'write:mail.MessageAttachment',
|
|
97
|
-
'write:mail.TrackingValue',
|
|
98
|
-
'write:mail.Notification',
|
|
99
|
-
'write:mail.Follower',
|
|
100
|
-
'write:mail.FollowerSubtype',
|
|
101
|
-
'read:user.User',
|
|
102
|
-
'read:partner.Partner',
|
|
103
|
-
];
|
|
104
|
-
const command = (ctx, key) => {
|
|
105
|
-
if (!actorRequired(ctx))
|
|
106
|
-
return invalid(issue('actor', 'flow.error.actorRequired'));
|
|
107
|
-
if (!commandKey(key))
|
|
108
|
-
return invalid(issue('idempotencyKey', 'flow.error.idempotencyRequired'));
|
|
109
|
-
return null;
|
|
110
|
-
};
|
|
111
|
-
/** Small pickers read this shape: active first, filtered by name, capped to a page. */
|
|
112
|
-
const optionRows = async (ctx, model, args, order) => {
|
|
113
|
-
const rows = await ctx.db.select(model, args.includeArchived === true ? {} : { active: true });
|
|
114
|
-
const needle = normalized(args.search);
|
|
115
|
-
return rows
|
|
116
|
-
.filter((row) => !needle || normalized(row.name).includes(needle) || normalized(row.code).includes(needle))
|
|
117
|
-
.sort((a, b) => (order ? order(a, b) : 0) ||
|
|
118
|
-
String(a.name ?? '').localeCompare(String(b.name ?? '')) ||
|
|
119
|
-
String(a.id).localeCompare(String(b.id)))
|
|
120
|
-
.slice(Math.max(0, n(args.cursor ?? 0)), Math.max(0, n(args.cursor ?? 0)) + Math.max(1, Math.min(200, n(args.limit ?? 80))));
|
|
121
|
-
};
|
|
122
|
-
/** Project visibility and collection filters must run before either paging or counting. */
|
|
123
|
-
const matchingProjects = async (ctx, args) => {
|
|
124
|
-
const visible = await visibleProjects(ctx);
|
|
125
|
-
if (visible !== null && !visible.length)
|
|
126
|
-
return [];
|
|
127
|
-
const visibleIds = visible === null ? null : new Set(visible);
|
|
128
|
-
const mine = args.mine === true ? await projectsWithMyWork(ctx) : null;
|
|
129
|
-
const needle = normalized(args.search);
|
|
130
|
-
const rows = await ctx.db.select('flow.Project', args.archivedOnly === true ? { active: false } : args.includeArchived === true ? {} : { active: true });
|
|
131
|
-
return rows
|
|
132
|
-
.filter((row) => (visibleIds === null || visibleIds.has(String(row.id))) &&
|
|
133
|
-
(mine === null || mine.has(String(row.id))) &&
|
|
134
|
-
(!needle || normalized(row.name).includes(needle) || normalized(row.key).includes(needle)))
|
|
135
|
-
.sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? '')) || String(a.id).localeCompare(String(b.id)));
|
|
136
|
-
};
|
|
137
|
-
/** Plain upsert for the entities with no CAS field — no concurrent editor to race against. */
|
|
138
|
-
const saveEntity = (model, fields, required, defaults) => defineFn({
|
|
139
|
-
input: { values: 'json', idempotencyKey: 'text' },
|
|
140
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
141
|
-
effects: [
|
|
142
|
-
`read:${model}`,
|
|
143
|
-
`write:${model}`,
|
|
144
|
-
'read:flow.Project',
|
|
145
|
-
'write:flow.ProjectMember',
|
|
146
|
-
'read:flow.ProjectMember',
|
|
147
|
-
'read:flow.ProjectAccessGrant',
|
|
148
|
-
'read:user.User',
|
|
149
|
-
],
|
|
150
|
-
idempotent: true,
|
|
151
|
-
agent: true,
|
|
152
|
-
handler: async (ctx, args) => {
|
|
153
|
-
const error = command(ctx, args.idempotencyKey);
|
|
154
|
-
if (error)
|
|
155
|
-
return error;
|
|
156
|
-
if (!args.values || typeof args.values !== 'object')
|
|
157
|
-
return invalid(issue('values', 'flow.error.required'));
|
|
158
|
-
const values = args.values;
|
|
159
|
-
const id = String(values.id ?? '');
|
|
160
|
-
if (!id)
|
|
161
|
-
return invalid(issue('id', 'flow.error.required'));
|
|
162
|
-
// Two shapes, one rule. A project's own row is named by its id; everything
|
|
163
|
-
// else this builds — columns, issue types, epics — names its project in a
|
|
164
|
-
// column. Either way, writing into a project a caller cannot see is the
|
|
165
|
-
// thing this refuses, and it refuses it as "not found".
|
|
166
|
-
const target = model === 'flow.Project' ? id : String(values.projectId ?? '');
|
|
167
|
-
const existing = (await ctx.db.select(model, { id }))[0];
|
|
168
|
-
const known = model === 'flow.Project' ? Boolean(existing) : true;
|
|
169
|
-
if (known && target && !(await canReadProject(ctx, target)))
|
|
170
|
-
return invalid(issue('id', 'flow.error.notFound'));
|
|
171
|
-
const cs = ctx
|
|
172
|
-
.change(model, { ...defaults(values, existing), ...values }, existing ?? null)
|
|
173
|
-
.cast(fields);
|
|
174
|
-
const withRequired = required.length ? cs.required(required) : cs;
|
|
175
|
-
if (!withRequired.valid)
|
|
176
|
-
return { ok: false, errors: withRequired.errors };
|
|
177
|
-
await ctx.db.commit(withRequired, existing ? { id } : undefined);
|
|
178
|
-
// A project nobody can see is not a project anybody asked for, and there
|
|
179
|
-
// is no other door: membership is what makes it visible, so the person
|
|
180
|
-
// who created it has to walk through first.
|
|
181
|
-
if (model === 'flow.Project' && !existing && ctx.actor)
|
|
182
|
-
await addMember(ctx, {
|
|
183
|
-
projectId: id,
|
|
184
|
-
userId: ctx.actor,
|
|
185
|
-
addedByUserId: ctx.actor,
|
|
186
|
-
at: new Date().toISOString(),
|
|
187
|
-
});
|
|
188
|
-
return { ok: true, id };
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
export const functions = {
|
|
192
|
-
'project.list': defineFn({
|
|
193
|
-
input: {
|
|
194
|
-
search: 'text?',
|
|
195
|
-
limit: 'int?',
|
|
196
|
-
includeArchived: 'bool?',
|
|
197
|
-
archivedOnly: 'bool?',
|
|
198
|
-
/** Where in the ordered list this page starts — see project.count. */
|
|
199
|
-
cursor: 'int?',
|
|
200
|
-
/** Only projects the caller has an issue in — see the note on projectsWithMyWork. */
|
|
201
|
-
mine: 'bool?',
|
|
202
|
-
},
|
|
203
|
-
output: { id: 'id', key: 'text', name: 'text', description: 'text?', active: 'bool' },
|
|
204
|
-
effects: [
|
|
205
|
-
'read:flow.Project',
|
|
206
|
-
'read:flow.Issue',
|
|
207
|
-
'read:flow.ProjectMember',
|
|
208
|
-
'read:flow.ProjectAccessGrant',
|
|
209
|
-
'read:user.User',
|
|
210
|
-
],
|
|
211
|
-
agent: true,
|
|
212
|
-
handler: async (ctx, args) => {
|
|
213
|
-
const rows = await matchingProjects(ctx, args);
|
|
214
|
-
const offset = Math.max(0, Math.trunc(n(args.cursor ?? 0)));
|
|
215
|
-
const limit = Math.max(1, Math.min(200, Math.trunc(n(args.limit ?? 80))));
|
|
216
|
-
return rows.slice(offset, offset + limit);
|
|
217
|
-
},
|
|
218
|
-
}),
|
|
219
|
-
/**
|
|
220
|
-
* Who is on a project.
|
|
221
|
-
*
|
|
222
|
-
* Reading the membership of a project is reading the project, so it is gated
|
|
223
|
-
* the same way: somebody who cannot see the project cannot see who is on it,
|
|
224
|
-
* and finds out nothing by asking.
|
|
225
|
-
*/
|
|
226
|
-
'project.member.list': defineFn({
|
|
227
|
-
input: { projectId: 'id' },
|
|
228
|
-
output: { id: 'id', projectId: 'id', userId: 'id', userName: 'text', addedAt: 'datetime' },
|
|
229
|
-
effects: [...membershipEffects],
|
|
230
|
-
agent: true,
|
|
231
|
-
handler: async (ctx, args) => {
|
|
232
|
-
if (!(await canReadProject(ctx, args.projectId)))
|
|
233
|
-
return [];
|
|
234
|
-
return membersOf(ctx, String(args.projectId));
|
|
235
|
-
},
|
|
236
|
-
}),
|
|
237
|
-
/**
|
|
238
|
-
* Put somebody on a project, or take them off.
|
|
239
|
-
*
|
|
240
|
-
* Configuration rather than everyday work: adding a person decides what they
|
|
241
|
-
* may read, which is a different act from moving their cards around. The
|
|
242
|
-
* caller has to be able to see the project first — you cannot staff a project
|
|
243
|
-
* you are not on unless you hold the company-wide grant.
|
|
244
|
-
*/
|
|
245
|
-
'project.member.add': defineFn({
|
|
246
|
-
input: { projectId: 'id', userId: 'id', idempotencyKey: 'text' },
|
|
247
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
248
|
-
effects: [...membershipEffects, 'write:flow.ProjectMember'],
|
|
249
|
-
idempotent: true,
|
|
250
|
-
agent: true,
|
|
251
|
-
handler: async (ctx, args) => {
|
|
252
|
-
const error = command(ctx, args.idempotencyKey);
|
|
253
|
-
if (error)
|
|
254
|
-
return error;
|
|
255
|
-
if (!(await canReadProject(ctx, args.projectId)))
|
|
256
|
-
return invalid(issue('projectId', 'flow.error.notFound'));
|
|
257
|
-
const user = (await ctx.db.select('user.User', { id: args.userId, active: true }))[0];
|
|
258
|
-
if (!user)
|
|
259
|
-
return invalid(issue('userId', 'flow.error.notFound'));
|
|
260
|
-
await addMember(ctx, {
|
|
261
|
-
projectId: String(args.projectId),
|
|
262
|
-
userId: String(args.userId),
|
|
263
|
-
addedByUserId: ctx.actor,
|
|
264
|
-
at: new Date().toISOString(),
|
|
265
|
-
});
|
|
266
|
-
return { ok: true, id: `${String(args.projectId)}:${String(args.userId)}` };
|
|
267
|
-
},
|
|
268
|
-
}),
|
|
269
|
-
'project.member.remove': defineFn({
|
|
270
|
-
input: { projectId: 'id', userId: 'id', idempotencyKey: 'text' },
|
|
271
|
-
output: { ok: 'bool', errors: 'json?' },
|
|
272
|
-
effects: [...membershipEffects, 'write:flow.ProjectMember'],
|
|
273
|
-
idempotent: true,
|
|
274
|
-
agent: true,
|
|
275
|
-
handler: async (ctx, args) => {
|
|
276
|
-
const error = command(ctx, args.idempotencyKey);
|
|
277
|
-
if (error)
|
|
278
|
-
return error;
|
|
279
|
-
if (!(await canReadProject(ctx, args.projectId)))
|
|
280
|
-
return invalid(issue('projectId', 'flow.error.notFound'));
|
|
281
|
-
// Removing the last member is allowed and is not an accident to guard
|
|
282
|
-
// against: a project with nobody on it is closed, which is what somebody
|
|
283
|
-
// clearing out a project wants. The company-wide grant is how it is
|
|
284
|
-
// reopened, and that is the point of having one.
|
|
285
|
-
const removed = await removeMember(ctx, String(args.projectId), String(args.userId));
|
|
286
|
-
return removed ? { ok: true } : invalid(issue('userId', 'flow.error.notFound'));
|
|
287
|
-
},
|
|
288
|
-
}),
|
|
289
|
-
/**
|
|
290
|
-
* Who reads every project in the company, and the two commands that decide it.
|
|
291
|
-
*
|
|
292
|
-
* The widest reach Flow grants, so it is `security` risk with an authority of
|
|
293
|
-
* its own: this is the row that makes somebody able to read a project nobody
|
|
294
|
-
* added them to. It exists so membership can be administered at all, and so a
|
|
295
|
-
* project whose members have left is not unreachable.
|
|
296
|
-
*/
|
|
297
|
-
'project.access.list': defineFn({
|
|
298
|
-
input: {},
|
|
299
|
-
output: { id: 'id', userId: 'id', addedAt: 'datetime' },
|
|
300
|
-
effects: ['read:flow.ProjectAccessGrant'],
|
|
301
|
-
agent: true,
|
|
302
|
-
handler: (ctx) => ctx.db.select('flow.ProjectAccessGrant', {}),
|
|
303
|
-
}),
|
|
304
|
-
'project.access.grant': defineFn({
|
|
305
|
-
input: { userId: 'id', idempotencyKey: 'text' },
|
|
306
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
307
|
-
effects: ['read:flow.ProjectAccessGrant', 'write:flow.ProjectAccessGrant', 'read:user.User'],
|
|
308
|
-
idempotent: true,
|
|
309
|
-
agent: true,
|
|
310
|
-
handler: async (ctx, args) => {
|
|
311
|
-
const error = command(ctx, args.idempotencyKey);
|
|
312
|
-
if (error)
|
|
313
|
-
return error;
|
|
314
|
-
const user = (await ctx.db.select('user.User', { id: args.userId, active: true }))[0];
|
|
315
|
-
if (!user)
|
|
316
|
-
return invalid(issue('userId', 'flow.error.notFound'));
|
|
317
|
-
await ctx.db.insertIfAbsent('flow.ProjectAccessGrant', {
|
|
318
|
-
id: String(args.userId),
|
|
319
|
-
userId: args.userId,
|
|
320
|
-
addedAt: new Date().toISOString(),
|
|
321
|
-
addedByUserId: ctx.actor,
|
|
322
|
-
});
|
|
323
|
-
return { ok: true, id: String(args.userId) };
|
|
324
|
-
},
|
|
325
|
-
}),
|
|
326
|
-
'project.access.revoke': defineFn({
|
|
327
|
-
input: { userId: 'id', idempotencyKey: 'text' },
|
|
328
|
-
output: { ok: 'bool', errors: 'json?' },
|
|
329
|
-
effects: ['read:flow.ProjectAccessGrant', 'write:flow.ProjectAccessGrant'],
|
|
330
|
-
idempotent: true,
|
|
331
|
-
agent: true,
|
|
332
|
-
handler: async (ctx, args) => {
|
|
333
|
-
const error = command(ctx, args.idempotencyKey);
|
|
334
|
-
if (error)
|
|
335
|
-
return error;
|
|
336
|
-
const held = (await ctx.db.select('flow.ProjectAccessGrant', { userId: args.userId }))[0];
|
|
337
|
-
if (!held)
|
|
338
|
-
return invalid(issue('userId', 'flow.error.notFound'));
|
|
339
|
-
const G = ctx.table('flow.ProjectAccessGrant');
|
|
340
|
-
await ctx.db.del(deleteFrom(G).where(eq(G.id, held.id)));
|
|
341
|
-
return { ok: true };
|
|
342
|
-
},
|
|
343
|
-
}),
|
|
344
|
-
/**
|
|
345
|
-
* How many projects there are to page through.
|
|
346
|
-
*
|
|
347
|
-
* The list screen used to show the length of the page it had — capped at two
|
|
348
|
-
* hundred — as though it were the total, so a company with more projects was
|
|
349
|
-
* told a number that was simply wrong, and the rest were unreachable. This
|
|
350
|
-
* answers the real figure, through the same membership filter the list uses
|
|
351
|
-
* (FLW-039).
|
|
352
|
-
*/
|
|
353
|
-
'project.count': defineFn({
|
|
354
|
-
input: { search: 'text?', includeArchived: 'bool?', archivedOnly: 'bool?', mine: 'bool?' },
|
|
355
|
-
output: { total: 'int' },
|
|
356
|
-
effects: [
|
|
357
|
-
'read:flow.Project',
|
|
358
|
-
'read:flow.Issue',
|
|
359
|
-
'read:flow.ProjectMember',
|
|
360
|
-
'read:flow.ProjectAccessGrant',
|
|
361
|
-
'read:user.User',
|
|
362
|
-
],
|
|
363
|
-
agent: true,
|
|
364
|
-
handler: async (ctx, args) => ({ total: (await matchingProjects(ctx, args)).length }),
|
|
365
|
-
}),
|
|
366
|
-
/**
|
|
367
|
-
* The issue counts behind a list of projects, in two reads rather than one
|
|
368
|
-
* per project — see `projectStats`.
|
|
369
|
-
*/
|
|
370
|
-
'project.stats': defineFn({
|
|
371
|
-
input: { projectIds: 'json' },
|
|
372
|
-
output: { id: 'id', total: 'int', done: 'int', state: 'text' },
|
|
373
|
-
effects: [
|
|
374
|
-
'read:flow.Issue',
|
|
375
|
-
'read:flow.Column',
|
|
376
|
-
'read:flow.Project',
|
|
377
|
-
'read:flow.ProjectMember',
|
|
378
|
-
'read:flow.ProjectAccessGrant',
|
|
379
|
-
'read:user.User',
|
|
380
|
-
],
|
|
381
|
-
agent: true,
|
|
382
|
-
handler: async (ctx, args) => {
|
|
383
|
-
const asked = Array.isArray(args.projectIds) ? args.projectIds.map(String) : [];
|
|
384
|
-
// A count is a reading. Asking for the totals of a project you are not on
|
|
385
|
-
// would answer "how much work is in there", which is most of what the
|
|
386
|
-
// project is.
|
|
387
|
-
const visible = await visibleProjects(ctx);
|
|
388
|
-
const ids = visible === null ? asked : asked.filter((id) => visible.includes(id));
|
|
389
|
-
const stats = await projectStats(ctx, ids);
|
|
390
|
-
return [...stats].map(([id, counted]) => ({
|
|
391
|
-
id,
|
|
392
|
-
total: counted.total,
|
|
393
|
-
done: counted.done,
|
|
394
|
-
state: projectStateOf(counted),
|
|
395
|
-
}));
|
|
396
|
-
},
|
|
397
|
-
}),
|
|
398
|
-
/**
|
|
399
|
-
* One project by id, the way `issue.get` answers one issue.
|
|
400
|
-
*
|
|
401
|
-
* Every project-scoped screen needs the project behind the id in its URL.
|
|
402
|
-
* Resolving that through `project.list` means listing and filtering, and
|
|
403
|
-
* `optionRows` caps at 200 rows sorted by name — so the 201st project by
|
|
404
|
-
* name would answer "not found" on its own board.
|
|
405
|
-
*/
|
|
406
|
-
'project.get': defineFn({
|
|
407
|
-
input: { id: 'id' },
|
|
408
|
-
output: {
|
|
409
|
-
id: 'id',
|
|
410
|
-
key: 'text',
|
|
411
|
-
name: 'text',
|
|
412
|
-
description: 'text?',
|
|
413
|
-
previewText: 'text?',
|
|
414
|
-
contentAttachmentId: 'id?',
|
|
415
|
-
active: 'bool',
|
|
416
|
-
},
|
|
417
|
-
effects: [
|
|
418
|
-
'read:flow.Project',
|
|
419
|
-
'read:flow.ProjectMember',
|
|
420
|
-
'read:flow.ProjectAccessGrant',
|
|
421
|
-
'read:user.User',
|
|
422
|
-
],
|
|
423
|
-
agent: true,
|
|
424
|
-
// Nothing rather than a refusal: that a project exists is itself the half of
|
|
425
|
-
// the answer a hidden project must not give away — see readableProject.
|
|
426
|
-
handler: async (ctx, args) => (await readableProject(ctx, args.id)) ?? null,
|
|
427
|
-
}),
|
|
428
|
-
/**
|
|
429
|
-
* Rewriting a project's brief, as a permission key of its own — separate
|
|
430
|
-
* from `project.save`, which renames the project and archives it.
|
|
431
|
-
*/
|
|
432
|
-
'project.editContent': defineFn({
|
|
433
|
-
input: { id: 'id' },
|
|
434
|
-
// The fields as they are actually returned, not a `value` wrapper the
|
|
435
|
-
// handler never builds: output is projected against these keys, so
|
|
436
|
-
// declaring `value` and answering `{ id, contentAttachmentId }` threw both
|
|
437
|
-
// away and handed the caller `{}`. Live Doc reads `contentAttachmentId`
|
|
438
|
-
// off this to find the stored snapshot, so an empty answer read as "never
|
|
439
|
-
// written" — and the next push started from a blank document and flattened
|
|
440
|
-
// it over the real one.
|
|
441
|
-
output: { id: 'id?', contentAttachmentId: 'id?' },
|
|
442
|
-
effects: ['read:flow.Project', ...membershipEffects],
|
|
443
|
-
agent: true,
|
|
444
|
-
handler: async (ctx, args) => {
|
|
445
|
-
const row = await readableProject(ctx, args.id);
|
|
446
|
-
return row ? { id: row.id, contentAttachmentId: row.contentAttachmentId ?? null } : null;
|
|
447
|
-
},
|
|
448
|
-
}),
|
|
449
|
-
/**
|
|
450
|
-
* Destroy a project and everything in it.
|
|
451
|
-
*
|
|
452
|
-
* Archiving is the default and stays the default — this is the other thing,
|
|
453
|
-
* for when somebody has asked for the data to be gone rather than hidden
|
|
454
|
-
* (FLW-DEC-018). It is not `flow.configure`: configuring a project and
|
|
455
|
-
* ending one are not the same act, and a role that does the first every week
|
|
456
|
-
* should not be able to do the second by accident.
|
|
457
|
-
*
|
|
458
|
-
* Three things happen here and the order is the point. The name typed by the
|
|
459
|
-
* caller has to match the project's own, so that destroying the wrong
|
|
460
|
-
* project takes more than a mis-click on a list. The record of the request
|
|
461
|
-
* is written **before** anything else, because a record that appears only on
|
|
462
|
-
* success misses the case worth auditing. Only then is the work queued —
|
|
463
|
-
* fifteen tables, every thread, and the bytes behind every document do not
|
|
464
|
-
* fit in a request, and the blob store is reachable only from a job.
|
|
465
|
-
*/
|
|
466
|
-
'project.delete': defineFn({
|
|
467
|
-
input: { projectId: 'id', confirmName: 'text', reason: 'text?', idempotencyKey: 'text' },
|
|
468
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
469
|
-
effects: [
|
|
470
|
-
...membershipEffects,
|
|
471
|
-
'read:flow.ProjectDeletion',
|
|
472
|
-
'write:flow.ProjectDeletion',
|
|
473
|
-
'enqueue:flow.purgeProject',
|
|
474
|
-
],
|
|
475
|
-
idempotent: true,
|
|
476
|
-
agent: true,
|
|
477
|
-
handler: async (ctx, args) => {
|
|
478
|
-
const error = command(ctx, args.idempotencyKey);
|
|
479
|
-
if (error)
|
|
480
|
-
return error;
|
|
481
|
-
// Through the membership gate like everything else: a project you cannot
|
|
482
|
-
// see is a project you cannot end, and it answers the same "not found" a
|
|
483
|
-
// project that was never there would.
|
|
484
|
-
const project = await readableProject(ctx, args.projectId);
|
|
485
|
-
if (!project)
|
|
486
|
-
return invalid(issue('projectId', 'flow.error.notFound'));
|
|
487
|
-
// Exactly the name, untrimmed of meaning: a confirmation that accepts a
|
|
488
|
-
// near-match is a confirmation that will be typed without reading.
|
|
489
|
-
if (String(args.confirmName ?? '').trim() !== String(project.name ?? ''))
|
|
490
|
-
return invalid(issue('confirmName', 'flow.error.confirmNameMismatch'));
|
|
491
|
-
const id = randomUUID();
|
|
492
|
-
await ctx.db.insert('flow.ProjectDeletion', {
|
|
493
|
-
id,
|
|
494
|
-
projectId: String(project.id),
|
|
495
|
-
// Copied, not referenced. After the purge there is nowhere left to
|
|
496
|
-
// read these from, and an audit row naming an id nobody recognises
|
|
497
|
-
// answers no question anybody will ask of it.
|
|
498
|
-
projectKey: String(project.key ?? ''),
|
|
499
|
-
projectName: String(project.name ?? ''),
|
|
500
|
-
requestedAt: new Date().toISOString(),
|
|
501
|
-
requestedByUserId: ctx.actor,
|
|
502
|
-
reason: args.reason ? String(args.reason) : null,
|
|
503
|
-
state: 'requested',
|
|
504
|
-
completedAt: null,
|
|
505
|
-
removed: null,
|
|
506
|
-
});
|
|
507
|
-
await ctx.jobs.enqueue('flow.purgeProject', { projectId: String(project.id), deletionId: id },
|
|
508
|
-
// One purge per project. Asking twice while the first is still running
|
|
509
|
-
// is the same request, and running two at once over the same rows is
|
|
510
|
-
// two jobs racing to delete what the other is reading.
|
|
511
|
-
{ uniqueKey: `flow.purgeProject:${String(project.id)}` });
|
|
512
|
-
return { ok: true, id };
|
|
513
|
-
},
|
|
514
|
-
}),
|
|
515
|
-
/** What was asked to be deleted, and what became of it. */
|
|
516
|
-
'project.deletion.list': defineFn({
|
|
517
|
-
input: { limit: 'int?' },
|
|
518
|
-
output: {
|
|
519
|
-
id: 'id',
|
|
520
|
-
projectId: 'id',
|
|
521
|
-
projectKey: 'text',
|
|
522
|
-
projectName: 'text',
|
|
523
|
-
requestedAt: 'datetime',
|
|
524
|
-
requestedByUserId: 'id?',
|
|
525
|
-
reason: 'text?',
|
|
526
|
-
state: 'text',
|
|
527
|
-
completedAt: 'datetime?',
|
|
528
|
-
},
|
|
529
|
-
effects: ['read:flow.ProjectDeletion'],
|
|
530
|
-
agent: true,
|
|
531
|
-
handler: async (ctx, args) => {
|
|
532
|
-
const D = ctx.table('flow.ProjectDeletion');
|
|
533
|
-
// No membership filter, and it cannot have one: the projects these rows
|
|
534
|
-
// name do not exist any more, so there is nothing left to be a member of.
|
|
535
|
-
// That is why reading this list is its own authority rather than `view`.
|
|
536
|
-
return ctx.db.all(from(D)
|
|
537
|
-
.orderBy(desc(D.requestedAt), desc(D.id))
|
|
538
|
-
.limit(Math.max(1, Math.min(200, n(args.limit ?? 50)))));
|
|
539
|
-
},
|
|
540
|
-
}),
|
|
541
|
-
'project.save': saveEntity('flow.Project', ['id', 'key', 'name', 'description', 'active'], ['key', 'name'], (args, existing) => ({ active: existing?.active ?? true, ...args })),
|
|
542
|
-
'column.list': defineFn({
|
|
543
|
-
input: { projectId: 'id', includeArchived: 'bool?' },
|
|
544
|
-
output: {
|
|
545
|
-
id: 'id',
|
|
546
|
-
projectId: 'id',
|
|
547
|
-
code: 'text',
|
|
548
|
-
name: 'text',
|
|
549
|
-
sequence: 'int',
|
|
550
|
-
terminalState: 'bool',
|
|
551
|
-
active: 'bool',
|
|
552
|
-
},
|
|
553
|
-
effects: [
|
|
554
|
-
'read:flow.Column',
|
|
555
|
-
'read:flow.Project',
|
|
556
|
-
'read:flow.ProjectMember',
|
|
557
|
-
'read:flow.ProjectAccessGrant',
|
|
558
|
-
'read:user.User',
|
|
559
|
-
...membershipEffects,
|
|
560
|
-
],
|
|
561
|
-
agent: true,
|
|
562
|
-
handler: async (ctx, args) => {
|
|
563
|
-
// A project's configuration is the project. Answering with its columns
|
|
564
|
-
// for somebody who cannot see the project describes it to them.
|
|
565
|
-
if (!(await canReadProject(ctx, args.projectId)))
|
|
566
|
-
return [];
|
|
567
|
-
const rows = await ctx.db.select('flow.Column', args.includeArchived === true
|
|
568
|
-
? { projectId: args.projectId }
|
|
569
|
-
: { projectId: args.projectId, active: true });
|
|
570
|
-
return rows.sort((a, b) => n(a.sequence) - n(b.sequence) || String(a.id).localeCompare(String(b.id)));
|
|
571
|
-
},
|
|
572
|
-
}),
|
|
573
|
-
'column.save': saveEntity('flow.Column', ['id', 'projectId', 'code', 'name', 'sequence', 'terminalState', 'active'], ['projectId', 'code', 'name'], (args, existing) => ({
|
|
574
|
-
sequence: existing?.sequence ?? 10,
|
|
575
|
-
terminalState: existing?.terminalState ?? false,
|
|
576
|
-
active: existing?.active ?? true,
|
|
577
|
-
...args,
|
|
578
|
-
})),
|
|
579
|
-
'column.archive': defineFn({
|
|
580
|
-
input: { id: 'id' },
|
|
581
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
582
|
-
effects: ['read:flow.Column', 'write:flow.Column', 'read:flow.Issue', ...membershipEffects],
|
|
583
|
-
idempotent: true,
|
|
584
|
-
agent: true,
|
|
585
|
-
handler: async (ctx, args) => {
|
|
586
|
-
const existing = await readableRow(ctx, 'flow.Column', args.id);
|
|
587
|
-
if (!existing)
|
|
588
|
-
return invalid(issue('id', 'flow.error.notFound'));
|
|
589
|
-
const held = await ctx.db.select('flow.Issue', { columnId: args.id, active: true });
|
|
590
|
-
if (held.length)
|
|
591
|
-
return invalid(issue('id', 'flow.error.columnHasIssues'));
|
|
592
|
-
await ctx.db.update('flow.Column', { id: args.id }, { active: false });
|
|
593
|
-
return { ok: true, id: args.id };
|
|
594
|
-
},
|
|
595
|
-
}),
|
|
596
|
-
'issueType.list': defineFn({
|
|
597
|
-
input: { projectId: 'id', includeArchived: 'bool?' },
|
|
598
|
-
output: {
|
|
599
|
-
id: 'id',
|
|
600
|
-
projectId: 'id',
|
|
601
|
-
code: 'text',
|
|
602
|
-
name: 'text',
|
|
603
|
-
color: 'text?',
|
|
604
|
-
sequence: 'int',
|
|
605
|
-
active: 'bool',
|
|
606
|
-
},
|
|
607
|
-
effects: [
|
|
608
|
-
'read:flow.IssueType',
|
|
609
|
-
'read:flow.Project',
|
|
610
|
-
'read:flow.ProjectMember',
|
|
611
|
-
'read:flow.ProjectAccessGrant',
|
|
612
|
-
'read:user.User',
|
|
613
|
-
...membershipEffects,
|
|
614
|
-
],
|
|
615
|
-
agent: true,
|
|
616
|
-
handler: async (ctx, args) => {
|
|
617
|
-
if (!(await canReadProject(ctx, args.projectId)))
|
|
618
|
-
return [];
|
|
619
|
-
const rows = await ctx.db.select('flow.IssueType', args.includeArchived === true
|
|
620
|
-
? { projectId: args.projectId }
|
|
621
|
-
: { projectId: args.projectId, active: true });
|
|
622
|
-
return rows.sort((a, b) => n(a.sequence) - n(b.sequence) || String(a.id).localeCompare(String(b.id)));
|
|
623
|
-
},
|
|
624
|
-
}),
|
|
625
|
-
'issueType.save': saveEntity('flow.IssueType', ['id', 'projectId', 'code', 'name', 'color', 'sequence', 'active'], ['projectId', 'code', 'name'], (args, existing) => ({
|
|
626
|
-
sequence: existing?.sequence ?? 10,
|
|
627
|
-
active: existing?.active ?? true,
|
|
628
|
-
...args,
|
|
629
|
-
})),
|
|
630
|
-
/**
|
|
631
|
-
* Archiving a type in use would leave those issues pointing at a row no
|
|
632
|
-
* screen lists any more, so they would read as untyped while still carrying
|
|
633
|
-
* it — the same refusal `column.archive` makes, for the same reason.
|
|
634
|
-
*/
|
|
635
|
-
'issueType.archive': defineFn({
|
|
636
|
-
input: { id: 'id' },
|
|
637
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
638
|
-
effects: ['read:flow.IssueType', 'write:flow.IssueType', 'read:flow.Issue', ...membershipEffects],
|
|
639
|
-
idempotent: true,
|
|
640
|
-
agent: true,
|
|
641
|
-
handler: async (ctx, args) => {
|
|
642
|
-
const existing = await readableRow(ctx, 'flow.IssueType', args.id);
|
|
643
|
-
if (!existing)
|
|
644
|
-
return invalid(issue('id', 'flow.error.notFound'));
|
|
645
|
-
const held = await ctx.db.select('flow.Issue', { typeId: args.id, active: true });
|
|
646
|
-
if (held.length)
|
|
647
|
-
return invalid(issue('id', 'flow.error.typeHasIssues'));
|
|
648
|
-
await ctx.db.update('flow.IssueType', { id: args.id }, { active: false });
|
|
649
|
-
return { ok: true, id: args.id };
|
|
650
|
-
},
|
|
651
|
-
}),
|
|
652
|
-
/**
|
|
653
|
-
* The project this reader's board last showed, if any.
|
|
654
|
-
*
|
|
655
|
-
* Answers null rather than guessing a project: the board asks them to pick
|
|
656
|
-
* once, and a wrong guess would show one team's work to somebody who wanted
|
|
657
|
-
* another's.
|
|
658
|
-
*/
|
|
659
|
-
'board.scope': defineFn({
|
|
660
|
-
input: {},
|
|
661
|
-
output: { projectId: 'id?' },
|
|
662
|
-
effects: ['read:flow.BoardScope', ...membershipEffects],
|
|
663
|
-
agent: true,
|
|
664
|
-
handler: async (ctx) => {
|
|
665
|
-
if (!ctx.actor)
|
|
666
|
-
return { projectId: null };
|
|
667
|
-
const held = (await ctx.db.select('flow.BoardScope', { userId: ctx.actor }))[0];
|
|
668
|
-
if (!held)
|
|
669
|
-
return { projectId: null };
|
|
670
|
-
// Where they were last is not where they may still go: somebody taken off
|
|
671
|
-
// a project keeps the row that remembers it, and answering with it would
|
|
672
|
-
// send the screen to a board that is no longer theirs — and would name a
|
|
673
|
-
// project they can no longer be told exists.
|
|
674
|
-
return (await canReadProject(ctx, held.projectId)) ? { projectId: held.projectId } : { projectId: null };
|
|
675
|
-
},
|
|
676
|
-
}),
|
|
677
|
-
'board.remember': defineFn({
|
|
678
|
-
input: { projectId: 'id' },
|
|
679
|
-
output: { ok: 'bool', errors: 'json?' },
|
|
680
|
-
effects: ['read:flow.BoardScope', 'write:flow.BoardScope', 'read:flow.Project', ...membershipEffects],
|
|
681
|
-
idempotent: true,
|
|
682
|
-
agent: true,
|
|
683
|
-
handler: async (ctx, args) => {
|
|
684
|
-
if (!ctx.actor)
|
|
685
|
-
return invalid(issue('actor', 'flow.error.actorRequired'));
|
|
686
|
-
const project = await readableProject(ctx, args.projectId);
|
|
687
|
-
if (!project || project.active !== true)
|
|
688
|
-
return invalid(issue('projectId', 'flow.error.notFound'));
|
|
689
|
-
// One row per reader **per company**. The id used to be the user id
|
|
690
|
-
// alone, which read as "one row per reader" and was not: the primary key
|
|
691
|
-
// is not company-scoped, so somebody who works in two companies got one
|
|
692
|
-
// row in whichever they opened first, and every later company answered
|
|
693
|
-
// `ok: true` and remembered nothing. Silent, and only visible as a board
|
|
694
|
-
// that would not stay where you left it (FLW-023).
|
|
695
|
-
const id = `${String(ctx.scope.company)}:${String(ctx.actor)}`;
|
|
696
|
-
const row = { id, userId: ctx.actor, projectId: args.projectId, updatedAt: new Date().toISOString() };
|
|
697
|
-
await ctx.db.insertIfAbsent('flow.BoardScope', row);
|
|
698
|
-
await ctx.db.update('flow.BoardScope', { id }, row);
|
|
699
|
-
return { ok: true };
|
|
700
|
-
},
|
|
701
|
-
}),
|
|
702
|
-
'field.list': defineFn({
|
|
703
|
-
input: { projectId: 'id', includeArchived: 'bool?' },
|
|
704
|
-
output: {
|
|
705
|
-
id: 'id',
|
|
706
|
-
projectId: 'id',
|
|
707
|
-
code: 'text',
|
|
708
|
-
name: 'text',
|
|
709
|
-
kind: 'text',
|
|
710
|
-
config: 'json?',
|
|
711
|
-
sequence: 'int',
|
|
712
|
-
active: 'bool',
|
|
713
|
-
},
|
|
714
|
-
effects: [
|
|
715
|
-
'read:flow.FieldDef',
|
|
716
|
-
'read:flow.Project',
|
|
717
|
-
'read:flow.ProjectMember',
|
|
718
|
-
'read:flow.ProjectAccessGrant',
|
|
719
|
-
'read:user.User',
|
|
720
|
-
...membershipEffects,
|
|
721
|
-
],
|
|
722
|
-
agent: true,
|
|
723
|
-
handler: async (ctx, args) => {
|
|
724
|
-
if (!(await canReadProject(ctx, args.projectId)))
|
|
725
|
-
return [];
|
|
726
|
-
const rows = await ctx.db.select('flow.FieldDef', args.includeArchived === true
|
|
727
|
-
? { projectId: args.projectId }
|
|
728
|
-
: { projectId: args.projectId, active: true });
|
|
729
|
-
return rows.sort((a, b) => n(a.sequence) - n(b.sequence) || String(a.id).localeCompare(String(b.id)));
|
|
730
|
-
},
|
|
731
|
-
}),
|
|
732
|
-
/**
|
|
733
|
-
* `kind` is checked here rather than left to the changeset: it is what
|
|
734
|
-
* `saveIssue` branches on to decide whether a value is well-formed, so a
|
|
735
|
-
* kind nothing knows how to check would be a field that accepts anything.
|
|
736
|
-
*/
|
|
737
|
-
'field.save': defineFn({
|
|
738
|
-
input: {
|
|
739
|
-
id: 'id',
|
|
740
|
-
projectId: 'id',
|
|
741
|
-
code: 'text',
|
|
742
|
-
name: 'text',
|
|
743
|
-
kind: 'text',
|
|
744
|
-
config: 'json?',
|
|
745
|
-
sequence: 'int?',
|
|
746
|
-
idempotencyKey: 'text',
|
|
747
|
-
},
|
|
748
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
749
|
-
effects: ['read:flow.FieldDef', 'write:flow.FieldDef', ...membershipEffects],
|
|
750
|
-
idempotent: true,
|
|
751
|
-
agent: true,
|
|
752
|
-
handler: async (ctx, args) => {
|
|
753
|
-
const error = command(ctx, args.idempotencyKey);
|
|
754
|
-
if (error)
|
|
755
|
-
return error;
|
|
756
|
-
const kind = String(args.kind);
|
|
757
|
-
if (!FIELD_KINDS.includes(kind))
|
|
758
|
-
return invalid(issue('kind', 'flow.error.fieldKind'));
|
|
759
|
-
// A select with no options is a control nobody can answer.
|
|
760
|
-
const options = args.config?.options;
|
|
761
|
-
if (kind === 'select' && (!Array.isArray(options) || options.length === 0))
|
|
762
|
-
return invalid(issue('config', 'flow.error.fieldOptionsRequired'));
|
|
763
|
-
const id = String(args.id);
|
|
764
|
-
const existing = (await ctx.db.select('flow.FieldDef', { id }))[0];
|
|
765
|
-
const row = {
|
|
766
|
-
id,
|
|
767
|
-
projectId: args.projectId,
|
|
768
|
-
code: String(args.code),
|
|
769
|
-
name: String(args.name),
|
|
770
|
-
kind,
|
|
771
|
-
config: args.config ?? null,
|
|
772
|
-
sequence: args.sequence == null ? (existing?.sequence ?? 10) : Number(args.sequence),
|
|
773
|
-
active: existing?.active ?? true,
|
|
774
|
-
};
|
|
775
|
-
if (existing)
|
|
776
|
-
await ctx.db.update('flow.FieldDef', { id }, row);
|
|
777
|
-
else {
|
|
778
|
-
const inserted = await ctx.db.insertIfAbsent('flow.FieldDef', row);
|
|
779
|
-
if (!('inserted' in inserted) || !inserted.inserted)
|
|
780
|
-
return invalid(issue('code', 'flow.error.fieldCodeUnique'));
|
|
781
|
-
}
|
|
782
|
-
return { ok: true, id };
|
|
783
|
-
},
|
|
784
|
-
}),
|
|
785
|
-
/**
|
|
786
|
-
* Archiving a field keeps the values already recorded against it.
|
|
787
|
-
*
|
|
788
|
-
* Unlike a column or a type, nothing points *at* a field from a row anyone
|
|
789
|
-
* reads — the values point the other way. So the answers stay, unlisted, and
|
|
790
|
-
* come back if the field is ever restored. Deleting them would be the one
|
|
791
|
-
* irreversible thing on this screen.
|
|
792
|
-
*/
|
|
793
|
-
'field.archive': defineFn({
|
|
794
|
-
input: { id: 'id' },
|
|
795
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
796
|
-
effects: ['read:flow.FieldDef', 'write:flow.FieldDef', ...membershipEffects],
|
|
797
|
-
idempotent: true,
|
|
798
|
-
agent: true,
|
|
799
|
-
handler: async (ctx, args) => {
|
|
800
|
-
const existing = (await ctx.db.select('flow.FieldDef', { id: args.id }))[0];
|
|
801
|
-
if (!existing)
|
|
802
|
-
return invalid(issue('id', 'flow.error.notFound'));
|
|
803
|
-
await ctx.db.update('flow.FieldDef', { id: args.id }, { active: false });
|
|
804
|
-
return { ok: true, id: args.id };
|
|
805
|
-
},
|
|
806
|
-
}),
|
|
807
|
-
'epic.list': defineFn({
|
|
808
|
-
input: { projectId: 'id', id: 'id?', search: 'text?', limit: 'int?', includeArchived: 'bool?' },
|
|
809
|
-
output: {
|
|
810
|
-
id: 'id',
|
|
811
|
-
projectId: 'id',
|
|
812
|
-
title: 'text',
|
|
813
|
-
color: 'text?',
|
|
814
|
-
previewText: 'text?',
|
|
815
|
-
contentAttachmentId: 'id?',
|
|
816
|
-
active: 'bool',
|
|
817
|
-
total: 'int',
|
|
818
|
-
done: 'int',
|
|
819
|
-
estimate: 'decimal',
|
|
820
|
-
estimateDone: 'decimal',
|
|
821
|
-
},
|
|
822
|
-
effects: ['read:flow.Epic', 'read:flow.Issue', 'read:flow.Column', ...membershipEffects],
|
|
823
|
-
agent: true,
|
|
824
|
-
handler: async (ctx, args) => {
|
|
825
|
-
if (!(await canReadProject(ctx, args.projectId)))
|
|
826
|
-
return [];
|
|
827
|
-
const where = { projectId: args.projectId };
|
|
828
|
-
if (args.id)
|
|
829
|
-
where.id = args.id;
|
|
830
|
-
if (args.includeArchived !== true)
|
|
831
|
-
where.active = true;
|
|
832
|
-
const [rows, totals] = await Promise.all([
|
|
833
|
-
ctx.db.select('flow.Epic', where),
|
|
834
|
-
epicTotals(ctx, String(args.projectId)),
|
|
835
|
-
]);
|
|
836
|
-
const needle = normalized(args.search);
|
|
837
|
-
const filtered = rows
|
|
838
|
-
.filter((row) => !needle || normalized(row.title).includes(needle))
|
|
839
|
-
.map((row) => ({
|
|
840
|
-
...row,
|
|
841
|
-
...(totals.get(String(row.id)) ?? { total: 0, done: 0, estimate: 0, estimateDone: 0 }),
|
|
842
|
-
}));
|
|
843
|
-
return args.id ? filtered : filtered.slice(0, Math.max(1, Math.min(200, n(args.limit ?? 80))));
|
|
844
|
-
},
|
|
845
|
-
}),
|
|
846
|
-
/**
|
|
847
|
-
* The menu-level epic collection, paged after one company-scoped read.
|
|
848
|
-
*
|
|
849
|
-
* `epic.list` remains project-scoped for relation controls and project
|
|
850
|
-
* screens. Folding those calls together in the backend inherited both its
|
|
851
|
-
* 80-row default and `project.list`'s 200-row cap, so an "all" screen could
|
|
852
|
-
* silently omit valid records.
|
|
853
|
-
*/
|
|
854
|
-
'epic.listAll': defineFn({
|
|
855
|
-
input: { search: 'text?', cursor: 'int?', limit: 'int?' },
|
|
856
|
-
output: { rows: 'json', total: 'int' },
|
|
857
|
-
effects: [
|
|
858
|
-
'read:flow.Epic',
|
|
859
|
-
'read:flow.Project',
|
|
860
|
-
'read:flow.ProjectMember',
|
|
861
|
-
'read:flow.ProjectAccessGrant',
|
|
862
|
-
'read:user.User',
|
|
863
|
-
],
|
|
864
|
-
agent: true,
|
|
865
|
-
handler: async (ctx, args) => {
|
|
866
|
-
// Every project's epics means every project this caller may see. The
|
|
867
|
-
// screen behind it is the cross-project one, so nothing else narrows it.
|
|
868
|
-
const [epics, projects] = await Promise.all([
|
|
869
|
-
visibleRows(ctx, 'flow.Epic', { active: true }),
|
|
870
|
-
ctx.db.select('flow.Project', { active: true }),
|
|
871
|
-
]);
|
|
872
|
-
const named = new Map(projects.map((project) => [String(project.id), String(project.name ?? '')]));
|
|
873
|
-
const needle = normalized(args.search);
|
|
874
|
-
const rows = epics
|
|
875
|
-
.filter((epic) => named.has(String(epic.projectId)) && (!needle || normalized(epic.title).includes(needle)))
|
|
876
|
-
.map((epic) => ({
|
|
877
|
-
...epic,
|
|
878
|
-
projectName: named.get(String(epic.projectId)) ?? '',
|
|
879
|
-
}))
|
|
880
|
-
.sort((a, b) => String(a.projectName).localeCompare(String(b.projectName)) ||
|
|
881
|
-
String(a.title ?? '').localeCompare(String(b.title ?? '')) ||
|
|
882
|
-
String(a.id).localeCompare(String(b.id)));
|
|
883
|
-
const cursor = Math.max(0, n(args.cursor ?? 0));
|
|
884
|
-
const limit = Math.max(1, Math.min(200, n(args.limit ?? 50)));
|
|
885
|
-
return { rows: rows.slice(cursor, cursor + limit), total: rows.length };
|
|
886
|
-
},
|
|
887
|
-
}),
|
|
888
|
-
/**
|
|
889
|
-
* One epic by id — what Live Doc reads to find its stored document, and what
|
|
890
|
-
* the epic's own screen is built from.
|
|
891
|
-
*/
|
|
892
|
-
'epic.get': defineFn({
|
|
893
|
-
input: { id: 'id' },
|
|
894
|
-
output: { value: 'json?' },
|
|
895
|
-
effects: [
|
|
896
|
-
'read:flow.Epic',
|
|
897
|
-
'read:flow.Project',
|
|
898
|
-
'read:flow.ProjectMember',
|
|
899
|
-
'read:flow.ProjectAccessGrant',
|
|
900
|
-
'read:user.User',
|
|
901
|
-
],
|
|
902
|
-
agent: true,
|
|
903
|
-
handler: async (ctx, args) => {
|
|
904
|
-
const held = (await ctx.db.select('flow.Epic', { id: args.id }))[0] ?? null;
|
|
905
|
-
return { value: held && (await canReadProject(ctx, held.projectId)) ? held : null };
|
|
906
|
-
},
|
|
907
|
-
}),
|
|
908
|
-
/**
|
|
909
|
-
* Rewriting an epic's document, as a permission key of its own — the same
|
|
910
|
-
* split `page.editContent` makes, and for the same reason.
|
|
911
|
-
*/
|
|
912
|
-
'epic.editContent': defineFn({
|
|
913
|
-
input: { id: 'id' },
|
|
914
|
-
// The fields as they are actually returned, not a `value` wrapper the
|
|
915
|
-
// handler never builds: output is projected against these keys, so
|
|
916
|
-
// declaring `value` and answering `{ id, contentAttachmentId }` threw both
|
|
917
|
-
// away and handed the caller `{}`. Live Doc reads `contentAttachmentId`
|
|
918
|
-
// off this to find the stored snapshot, so an empty answer read as "never
|
|
919
|
-
// written" — and the next push started from a blank document and flattened
|
|
920
|
-
// it over the real one.
|
|
921
|
-
output: { id: 'id?', contentAttachmentId: 'id?' },
|
|
922
|
-
effects: ['read:flow.Epic', ...membershipEffects],
|
|
923
|
-
agent: true,
|
|
924
|
-
handler: async (ctx, args) => {
|
|
925
|
-
const row = await readableRow(ctx, 'flow.Epic', args.id);
|
|
926
|
-
return row ? { id: row.id, contentAttachmentId: row.contentAttachmentId ?? null } : null;
|
|
927
|
-
},
|
|
928
|
-
}),
|
|
929
|
-
'epic.save': saveEntity('flow.Epic', ['id', 'projectId', 'title', 'color', 'active'], ['projectId', 'title'], (args, existing) => ({ active: existing?.active ?? true, ...args })),
|
|
930
|
-
'epic.archive': defineFn({
|
|
931
|
-
input: { id: 'id' },
|
|
932
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
933
|
-
effects: ['read:flow.Epic', 'write:flow.Epic', ...membershipEffects],
|
|
934
|
-
idempotent: true,
|
|
935
|
-
agent: true,
|
|
936
|
-
handler: async (ctx, args) => {
|
|
937
|
-
const existing = await readableRow(ctx, 'flow.Epic', args.id);
|
|
938
|
-
if (!existing)
|
|
939
|
-
return invalid(issue('id', 'flow.error.notFound'));
|
|
940
|
-
await ctx.db.update('flow.Epic', { id: args.id }, { active: false });
|
|
941
|
-
return { ok: true, id: args.id };
|
|
942
|
-
},
|
|
943
|
-
}),
|
|
944
|
-
/**
|
|
945
|
-
* Every page in a project, flat — the screen assembles the tree.
|
|
946
|
-
*
|
|
947
|
-
* See listPages for why the whole project comes back at once rather than a
|
|
948
|
-
* level per request.
|
|
949
|
-
*/
|
|
950
|
-
'page.list': defineFn({
|
|
951
|
-
input: { projectId: 'id?', search: 'text?', includeArchived: 'bool?', limit: 'int?' },
|
|
952
|
-
output: {
|
|
953
|
-
id: 'id',
|
|
954
|
-
projectId: 'id',
|
|
955
|
-
parentPageId: 'id?',
|
|
956
|
-
title: 'text',
|
|
957
|
-
previewText: 'text?',
|
|
958
|
-
contentAttachmentId: 'id?',
|
|
959
|
-
contentUpdatedAt: 'datetime?',
|
|
960
|
-
sequence: 'int',
|
|
961
|
-
active: 'bool',
|
|
962
|
-
version: 'int',
|
|
963
|
-
updatedAt: 'datetime',
|
|
964
|
-
childCount: 'int',
|
|
965
|
-
},
|
|
966
|
-
effects: [
|
|
967
|
-
'read:flow.Page',
|
|
968
|
-
'read:flow.Project',
|
|
969
|
-
'read:flow.ProjectMember',
|
|
970
|
-
'read:flow.ProjectAccessGrant',
|
|
971
|
-
'read:user.User',
|
|
972
|
-
...membershipEffects,
|
|
973
|
-
],
|
|
974
|
-
agent: true,
|
|
975
|
-
handler: async (ctx, args) => {
|
|
976
|
-
// Without a project this is the cross-project search, which listPages
|
|
977
|
-
// filters itself; with one it is that project's tree.
|
|
978
|
-
if (args.projectId != null && !(await canReadProject(ctx, args.projectId)))
|
|
979
|
-
return [];
|
|
980
|
-
return listPages(ctx, {
|
|
981
|
-
projectId: args.projectId == null ? null : String(args.projectId),
|
|
982
|
-
search: args.search == null ? null : String(args.search),
|
|
983
|
-
includeArchived: args.includeArchived === true,
|
|
984
|
-
limit: args.limit == null ? undefined : n(args.limit),
|
|
985
|
-
});
|
|
986
|
-
},
|
|
987
|
-
}),
|
|
988
|
-
/**
|
|
989
|
-
* Every project's pages, paged and counted — see listAllPages.
|
|
990
|
-
*
|
|
991
|
-
* `page.list` answers "this project's tree" and stays that. This answers
|
|
992
|
-
* "every document there is", a different question needing a different shape:
|
|
993
|
-
* a total the pager can trust, and the project name beside each row.
|
|
994
|
-
*/
|
|
995
|
-
'page.listAll': defineFn({
|
|
996
|
-
input: { search: 'text?', cursor: 'int?', limit: 'int?' },
|
|
997
|
-
output: { rows: 'json', total: 'int' },
|
|
998
|
-
effects: ['read:flow.Page', ...membershipEffects],
|
|
999
|
-
agent: true,
|
|
1000
|
-
handler: (ctx, args) => listAllPages(ctx, {
|
|
1001
|
-
search: args.search == null ? null : String(args.search),
|
|
1002
|
-
cursor: args.cursor == null ? undefined : n(args.cursor),
|
|
1003
|
-
limit: args.limit == null ? undefined : n(args.limit),
|
|
1004
|
-
}),
|
|
1005
|
-
}),
|
|
1006
|
-
'page.get': defineFn({
|
|
1007
|
-
input: { id: 'id' },
|
|
1008
|
-
output: { value: 'json?' },
|
|
1009
|
-
effects: [
|
|
1010
|
-
'read:flow.Page',
|
|
1011
|
-
'read:flow.Project',
|
|
1012
|
-
'read:flow.ProjectMember',
|
|
1013
|
-
'read:flow.ProjectAccessGrant',
|
|
1014
|
-
'read:user.User',
|
|
1015
|
-
],
|
|
1016
|
-
agent: true,
|
|
1017
|
-
handler: async (ctx, args) => {
|
|
1018
|
-
const held = await pageDetail(ctx, String(args.id));
|
|
1019
|
-
return { value: held && (await canReadProject(ctx, held.projectId)) ? held : null };
|
|
1020
|
-
},
|
|
1021
|
-
}),
|
|
1022
|
-
'page.save': defineFn({
|
|
1023
|
-
input: {
|
|
1024
|
-
id: 'id',
|
|
1025
|
-
projectId: 'id',
|
|
1026
|
-
title: 'text',
|
|
1027
|
-
parentPageId: 'id?',
|
|
1028
|
-
sequence: 'int?',
|
|
1029
|
-
expectedVersion: 'int?',
|
|
1030
|
-
idempotencyKey: 'text',
|
|
1031
|
-
},
|
|
1032
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
1033
|
-
effects: ['read:flow.Page', 'write:flow.Page', 'read:flow.Project', ...membershipEffects],
|
|
1034
|
-
idempotent: true,
|
|
1035
|
-
agent: true,
|
|
1036
|
-
handler: (ctx, args) => savePage(ctx, {
|
|
1037
|
-
id: String(args.id),
|
|
1038
|
-
projectId: String(args.projectId),
|
|
1039
|
-
title: String(args.title),
|
|
1040
|
-
parentPageId: args.parentPageId === undefined ? undefined : args.parentPageId,
|
|
1041
|
-
sequence: args.sequence == null ? null : n(args.sequence),
|
|
1042
|
-
expectedVersion: args.expectedVersion == null ? undefined : n(args.expectedVersion),
|
|
1043
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1044
|
-
}),
|
|
1045
|
-
}),
|
|
1046
|
-
/**
|
|
1047
|
-
* Re-parenting, as its own key.
|
|
1048
|
-
*
|
|
1049
|
-
* A hierarchy is only useful if it can be rearranged, and rearranging is a
|
|
1050
|
-
* different right from writing: someone may be trusted to edit a page
|
|
1051
|
-
* without being trusted to move a whole branch of the wiki.
|
|
1052
|
-
*/
|
|
1053
|
-
'page.move': defineFn({
|
|
1054
|
-
input: { id: 'id', parentPageId: 'id?', sequence: 'int?' },
|
|
1055
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
1056
|
-
effects: ['read:flow.Page', 'write:flow.Page', ...membershipEffects],
|
|
1057
|
-
idempotent: true,
|
|
1058
|
-
agent: true,
|
|
1059
|
-
handler: (ctx, args) => movePage(ctx, {
|
|
1060
|
-
id: String(args.id),
|
|
1061
|
-
parentPageId: args.parentPageId ?? null,
|
|
1062
|
-
sequence: args.sequence == null ? null : n(args.sequence),
|
|
1063
|
-
}),
|
|
1064
|
-
}),
|
|
1065
|
-
'page.reorder': defineFn({
|
|
1066
|
-
input: { id: 'id', direction: 'text' },
|
|
1067
|
-
output: { ok: 'bool', id: 'id?', moved: 'bool?', errors: 'json?' },
|
|
1068
|
-
effects: ['read:flow.Page', 'write:flow.Page', ...membershipEffects],
|
|
1069
|
-
idempotent: true,
|
|
1070
|
-
agent: true,
|
|
1071
|
-
handler: (ctx, args) => reorderPage(ctx, {
|
|
1072
|
-
id: String(args.id),
|
|
1073
|
-
direction: String(args.direction) === 'up' ? 'up' : 'down',
|
|
1074
|
-
}),
|
|
1075
|
-
}),
|
|
1076
|
-
'page.archive': defineFn({
|
|
1077
|
-
input: { id: 'id' },
|
|
1078
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
1079
|
-
effects: ['read:flow.Page', 'write:flow.Page', ...membershipEffects],
|
|
1080
|
-
idempotent: true,
|
|
1081
|
-
agent: true,
|
|
1082
|
-
handler: (ctx, args) => archivePage(ctx, String(args.id)),
|
|
1083
|
-
}),
|
|
1084
|
-
'page.restore': defineFn({
|
|
1085
|
-
input: { id: 'id' },
|
|
1086
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
1087
|
-
effects: ['read:flow.Page', 'write:flow.Page', ...membershipEffects],
|
|
1088
|
-
idempotent: true,
|
|
1089
|
-
agent: true,
|
|
1090
|
-
handler: (ctx, args) => restorePage(ctx, String(args.id)),
|
|
1091
|
-
}),
|
|
1092
|
-
/**
|
|
1093
|
-
* Rewriting a page's document, as a permission key of its own.
|
|
1094
|
-
*
|
|
1095
|
-
* It grants nothing by itself — Live Doc calls it only to ask whether this
|
|
1096
|
-
* caller may write, and hands back the row it returns (documents.ts). It is
|
|
1097
|
-
* separate from `page.save` because writing prose and renaming a page are
|
|
1098
|
-
* different rights: a reviewer may hold one without the other.
|
|
1099
|
-
*/
|
|
1100
|
-
'page.editContent': defineFn({
|
|
1101
|
-
input: { id: 'id' },
|
|
1102
|
-
// The fields as they are actually returned, not a `value` wrapper the
|
|
1103
|
-
// handler never builds: output is projected against these keys, so
|
|
1104
|
-
// declaring `value` and answering `{ id, contentAttachmentId }` threw both
|
|
1105
|
-
// away and handed the caller `{}`. Live Doc reads `contentAttachmentId`
|
|
1106
|
-
// off this to find the stored snapshot, so an empty answer read as "never
|
|
1107
|
-
// written" — and the next push started from a blank document and flattened
|
|
1108
|
-
// it over the real one.
|
|
1109
|
-
output: { id: 'id?', contentAttachmentId: 'id?' },
|
|
1110
|
-
effects: ['read:flow.Page', ...membershipEffects],
|
|
1111
|
-
agent: true,
|
|
1112
|
-
handler: async (ctx, args) => {
|
|
1113
|
-
const row = await readableRow(ctx, 'flow.Page', args.id);
|
|
1114
|
-
return row ? { id: row.id, contentAttachmentId: row.contentAttachmentId ?? null } : null;
|
|
1115
|
-
},
|
|
1116
|
-
}),
|
|
1117
|
-
/**
|
|
1118
|
-
* The project's sprints with what each is carrying — see sprintTotals.
|
|
1119
|
-
*
|
|
1120
|
-
* The totals live here rather than behind a key of their own because every
|
|
1121
|
-
* caller that wants a sprint wants to know how big it is, and the screen that
|
|
1122
|
-
* closes one needs the unfinished count before it can offer anywhere to put it.
|
|
1123
|
-
*/
|
|
1124
|
-
'sprint.list': defineFn({
|
|
1125
|
-
input: { projectId: 'id' },
|
|
1126
|
-
output: {
|
|
1127
|
-
id: 'id',
|
|
1128
|
-
projectId: 'id',
|
|
1129
|
-
name: 'text',
|
|
1130
|
-
startDate: 'date?',
|
|
1131
|
-
endDate: 'date?',
|
|
1132
|
-
state: 'text',
|
|
1133
|
-
total: 'int',
|
|
1134
|
-
done: 'int',
|
|
1135
|
-
unfinished: 'int',
|
|
1136
|
-
estimate: 'decimal',
|
|
1137
|
-
estimateDone: 'decimal',
|
|
1138
|
-
},
|
|
1139
|
-
effects: [
|
|
1140
|
-
'read:flow.Sprint',
|
|
1141
|
-
'read:flow.Issue',
|
|
1142
|
-
'read:flow.Column',
|
|
1143
|
-
'read:flow.Project',
|
|
1144
|
-
'read:flow.ProjectMember',
|
|
1145
|
-
'read:flow.ProjectAccessGrant',
|
|
1146
|
-
'read:user.User',
|
|
1147
|
-
...membershipEffects,
|
|
1148
|
-
],
|
|
1149
|
-
agent: true,
|
|
1150
|
-
handler: async (ctx, args) => {
|
|
1151
|
-
if (!(await canReadProject(ctx, args.projectId)))
|
|
1152
|
-
return [];
|
|
1153
|
-
const [rows, totals] = await Promise.all([
|
|
1154
|
-
ctx.db.select('flow.Sprint', { projectId: args.projectId }),
|
|
1155
|
-
sprintTotals(ctx, String(args.projectId)),
|
|
1156
|
-
]);
|
|
1157
|
-
return rows.map((row) => ({
|
|
1158
|
-
...row,
|
|
1159
|
-
...(totals.get(String(row.id)) ?? {
|
|
1160
|
-
total: 0,
|
|
1161
|
-
done: 0,
|
|
1162
|
-
unfinished: 0,
|
|
1163
|
-
estimate: 0,
|
|
1164
|
-
estimateDone: 0,
|
|
1165
|
-
}),
|
|
1166
|
-
}));
|
|
1167
|
-
},
|
|
1168
|
-
}),
|
|
1169
|
-
'sprint.save': defineFn({
|
|
1170
|
-
input: {
|
|
1171
|
-
id: 'id',
|
|
1172
|
-
projectId: 'id',
|
|
1173
|
-
name: 'text',
|
|
1174
|
-
startDate: 'date?',
|
|
1175
|
-
endDate: 'date?',
|
|
1176
|
-
idempotencyKey: 'text',
|
|
1177
|
-
},
|
|
1178
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
1179
|
-
effects: ['read:flow.Sprint', 'write:flow.Sprint', ...membershipEffects],
|
|
1180
|
-
idempotent: true,
|
|
1181
|
-
agent: true,
|
|
1182
|
-
handler: async (ctx, args) => {
|
|
1183
|
-
const error = command(ctx, args.idempotencyKey);
|
|
1184
|
-
if (error)
|
|
1185
|
-
return error;
|
|
1186
|
-
const existing = await readableRow(ctx, 'flow.Sprint', args.id);
|
|
1187
|
-
if (existing && existing.state !== 'planned')
|
|
1188
|
-
return invalid(issue('id', 'flow.error.invalidSprintState'));
|
|
1189
|
-
if (!(await canReadProject(ctx, args.projectId)))
|
|
1190
|
-
return invalid(issue('projectId', 'flow.error.notFound'));
|
|
1191
|
-
const name = String(args.name ?? '').trim();
|
|
1192
|
-
if (!name)
|
|
1193
|
-
return invalid(issue('name', 'flow.error.required'));
|
|
1194
|
-
const values = {
|
|
1195
|
-
projectId: args.projectId,
|
|
1196
|
-
name,
|
|
1197
|
-
startDate: args.startDate ?? null,
|
|
1198
|
-
endDate: args.endDate ?? null,
|
|
1199
|
-
state: 'planned',
|
|
1200
|
-
};
|
|
1201
|
-
if (existing)
|
|
1202
|
-
await ctx.db.update('flow.Sprint', { id: args.id }, values);
|
|
1203
|
-
else
|
|
1204
|
-
await ctx.db.insert('flow.Sprint', { id: args.id, ...values });
|
|
1205
|
-
return { ok: true, id: args.id };
|
|
1206
|
-
},
|
|
1207
|
-
}),
|
|
1208
|
-
'sprint.start': defineFn({
|
|
1209
|
-
input: { id: 'id', idempotencyKey: 'text' },
|
|
1210
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
1211
|
-
effects: [
|
|
1212
|
-
'read:flow.Sprint',
|
|
1213
|
-
'write:flow.Sprint',
|
|
1214
|
-
// The guard row this contends on, so two callers cannot both be told
|
|
1215
|
-
// they started a sprint — see flow.ProjectGuard.
|
|
1216
|
-
'read:flow.ProjectGuard',
|
|
1217
|
-
'write:flow.ProjectGuard',
|
|
1218
|
-
...membershipEffects,
|
|
1219
|
-
],
|
|
1220
|
-
idempotent: true,
|
|
1221
|
-
agent: true,
|
|
1222
|
-
handler: (ctx, args) => startSprint(ctx, { id: String(args.id), idempotencyKey: String(args.idempotencyKey) }),
|
|
1223
|
-
}),
|
|
1224
|
-
/**
|
|
1225
|
-
* Close it, and decide what happens to the work that did not finish.
|
|
1226
|
-
*
|
|
1227
|
-
* `carryTo` is absent by default, which is exactly what closing used to do —
|
|
1228
|
-
* so nothing that calls this today behaves differently.
|
|
1229
|
-
*/
|
|
1230
|
-
/**
|
|
1231
|
-
* Delete a sprint nobody ever started — see deleteSprint for why only those.
|
|
1232
|
-
*/
|
|
1233
|
-
'sprint.delete': defineFn({
|
|
1234
|
-
input: { id: 'id', idempotencyKey: 'text' },
|
|
1235
|
-
output: { ok: 'bool', id: 'id?', released: 'int?', errors: 'json?' },
|
|
1236
|
-
effects: [
|
|
1237
|
-
'read:flow.Sprint',
|
|
1238
|
-
'write:flow.Sprint',
|
|
1239
|
-
'read:flow.Issue',
|
|
1240
|
-
'write:flow.Issue',
|
|
1241
|
-
...membershipEffects,
|
|
1242
|
-
],
|
|
1243
|
-
idempotent: true,
|
|
1244
|
-
agent: true,
|
|
1245
|
-
handler: (ctx, args) => deleteSprint(ctx, { id: String(args.id), idempotencyKey: String(args.idempotencyKey) }),
|
|
1246
|
-
}),
|
|
1247
|
-
'sprint.close': defineFn({
|
|
1248
|
-
input: { id: 'id', carryTo: 'id?', carry: 'bool?', idempotencyKey: 'text' },
|
|
1249
|
-
output: { ok: 'bool', id: 'id?', carried: 'int?', errors: 'json?' },
|
|
1250
|
-
effects: [
|
|
1251
|
-
'read:flow.Sprint',
|
|
1252
|
-
'write:flow.Sprint',
|
|
1253
|
-
'read:flow.Issue',
|
|
1254
|
-
'write:flow.Issue',
|
|
1255
|
-
'read:flow.Column',
|
|
1256
|
-
...timelineEntryEffects,
|
|
1257
|
-
...membershipEffects,
|
|
1258
|
-
],
|
|
1259
|
-
idempotent: true,
|
|
1260
|
-
agent: true,
|
|
1261
|
-
handler: (ctx, args) => closeSprint(ctx, {
|
|
1262
|
-
id: String(args.id),
|
|
1263
|
-
// Three answers, not two: leave it (omit), move it (an id), or take it
|
|
1264
|
-
// out of every sprint (`carry: true` with no target).
|
|
1265
|
-
...(args.carryTo || args.carry === true
|
|
1266
|
-
? { carryTo: args.carryTo ? String(args.carryTo) : null }
|
|
1267
|
-
: {}),
|
|
1268
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1269
|
-
}),
|
|
1270
|
-
}),
|
|
1271
|
-
/**
|
|
1272
|
-
* Every tag, with how much work in the company carries it.
|
|
1273
|
-
*
|
|
1274
|
-
* The count is here rather than on the screen because of what archiving a tag
|
|
1275
|
-
* does: it deletes every `IssueTag` row for it, across every project, and
|
|
1276
|
-
* cannot be undone. The block that offers that button sits inside a *project's*
|
|
1277
|
-
* settings — so the number is the only thing that says how far the button
|
|
1278
|
-
* reaches. One grouped query, not one count per tag.
|
|
1279
|
-
*/
|
|
1280
|
-
'tag.list': defineFn({
|
|
1281
|
-
input: { search: 'text?', limit: 'int?', includeArchived: 'bool?' },
|
|
1282
|
-
output: { id: 'id', name: 'text', color: 'text?', active: 'bool', usage: 'int' },
|
|
1283
|
-
effects: ['read:flow.Tag', 'read:flow.IssueTag'],
|
|
1284
|
-
agent: true,
|
|
1285
|
-
handler: async (ctx, args) => {
|
|
1286
|
-
const rows = await optionRows(ctx, 'flow.Tag', args);
|
|
1287
|
-
if (!rows.length)
|
|
1288
|
-
return rows;
|
|
1289
|
-
const IT = ctx.table('flow.IssueTag');
|
|
1290
|
-
// Every row, archived issues included, because that is what `tag.archive`
|
|
1291
|
-
// deletes. A count that quietly skipped archived work would understate
|
|
1292
|
-
// exactly the thing the reader is about to lose.
|
|
1293
|
-
//
|
|
1294
|
-
// Counted across every project on purpose, membership notwithstanding.
|
|
1295
|
-
// The figure exists to answer "what will archiving this destroy", and
|
|
1296
|
-
// archiving a tag clears it from every project at once (FLW-DEC-006) — a
|
|
1297
|
-
// count narrowed to the reader's own projects would understate the damage
|
|
1298
|
-
// and make the warning a lie. What it gives away is one number about work
|
|
1299
|
-
// the reader cannot otherwise see, which is the price of the warning
|
|
1300
|
-
// being true. See the exception list in flow-membership.test.ts.
|
|
1301
|
-
const groups = await ctx.db.group(from(IT)
|
|
1302
|
-
.where(inArray(IT.tagId, rows.map((row) => String(row.id))))
|
|
1303
|
-
.groupBy({ col: IT.tagId }));
|
|
1304
|
-
const counted = new Map(groups.map((group) => [String(group.key[0] ?? ''), n(group.count)]));
|
|
1305
|
-
return rows.map((row) => ({ ...row, usage: counted.get(String(row.id)) ?? 0 }));
|
|
1306
|
-
},
|
|
1307
|
-
}),
|
|
1308
|
-
'tag.save': defineFn({
|
|
1309
|
-
input: { id: 'id', name: 'text', color: 'text?', active: 'bool?' },
|
|
1310
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
1311
|
-
effects: ['read:flow.Tag', 'write:flow.Tag'],
|
|
1312
|
-
idempotent: true,
|
|
1313
|
-
agent: true,
|
|
1314
|
-
handler: async (ctx, args) => {
|
|
1315
|
-
const name = String(args.name ?? '').trim();
|
|
1316
|
-
if (!name)
|
|
1317
|
-
return invalid(issue('name', 'flow.error.required'));
|
|
1318
|
-
const existing = (await ctx.db.select('flow.Tag', { id: args.id }))[0];
|
|
1319
|
-
const clash = (await ctx.db.select('flow.Tag', { name })).find((row) => row.id !== args.id);
|
|
1320
|
-
if (clash)
|
|
1321
|
-
return invalid(issue('name', 'flow.error.duplicateName'));
|
|
1322
|
-
const values = {
|
|
1323
|
-
name,
|
|
1324
|
-
color: args.color ? String(args.color) : (existing?.color ?? null),
|
|
1325
|
-
active: args.active ?? existing?.active ?? true,
|
|
1326
|
-
};
|
|
1327
|
-
if (existing)
|
|
1328
|
-
await ctx.db.update('flow.Tag', { id: args.id }, values);
|
|
1329
|
-
else
|
|
1330
|
-
await ctx.db.insert('flow.Tag', { id: args.id, ...values });
|
|
1331
|
-
return { ok: true, id: args.id };
|
|
1332
|
-
},
|
|
1333
|
-
}),
|
|
1334
|
-
'tag.archive': defineFn({
|
|
1335
|
-
input: { id: 'id' },
|
|
1336
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
1337
|
-
effects: ['read:flow.Tag', 'write:flow.Tag', 'read:flow.IssueTag', 'write:flow.IssueTag'],
|
|
1338
|
-
idempotent: true,
|
|
1339
|
-
agent: true,
|
|
1340
|
-
handler: async (ctx, args) => {
|
|
1341
|
-
const existing = (await ctx.db.select('flow.Tag', { id: args.id }))[0];
|
|
1342
|
-
if (!existing)
|
|
1343
|
-
return invalid(issue('id', 'flow.error.notFound'));
|
|
1344
|
-
await ctx.db.update('flow.Tag', { id: args.id }, { active: false });
|
|
1345
|
-
const IT = ctx.table('flow.IssueTag');
|
|
1346
|
-
await ctx.db.del(deleteFrom(IT).where(eq(IT.tagId, args.id)));
|
|
1347
|
-
return { ok: true, id: args.id };
|
|
1348
|
-
},
|
|
1349
|
-
}),
|
|
1350
|
-
'issue.list': defineFn({
|
|
1351
|
-
input: {
|
|
1352
|
-
/** A day; narrows to issues due before it that are not finished. */
|
|
1353
|
-
overdueOn: 'text?',
|
|
1354
|
-
projectId: 'id?',
|
|
1355
|
-
columnId: 'id?',
|
|
1356
|
-
epicId: 'id?',
|
|
1357
|
-
sprintId: 'id?',
|
|
1358
|
-
assigneeUserId: 'id?',
|
|
1359
|
-
/** Assigned to whoever is asking; resolved from the actor, not the caller. */
|
|
1360
|
-
mine: 'bool?',
|
|
1361
|
-
includeArchived: 'bool?',
|
|
1362
|
-
cursor: 'text?',
|
|
1363
|
-
limit: 'int?',
|
|
1364
|
-
listState: 'json?',
|
|
1365
|
-
path: 'json?',
|
|
1366
|
-
timezone: 'text?',
|
|
1367
|
-
},
|
|
1368
|
-
output: { rows: 'json', total: 'int', nextCursor: 'text?', fieldFilterTruncated: 'bool?' },
|
|
1369
|
-
effects: [...flowReadEffects, 'read:company.Company'],
|
|
1370
|
-
agent: true,
|
|
1371
|
-
handler: (ctx, args) => listIssues(ctx, args),
|
|
1372
|
-
}),
|
|
1373
|
-
/**
|
|
1374
|
-
* How the issues under the same filter divide up — see `issueBuckets`.
|
|
1375
|
-
*
|
|
1376
|
-
* Four counts, not a page of rows, so the figures beside a list of a
|
|
1377
|
-
* thousand issues cost four queries.
|
|
1378
|
-
*/
|
|
1379
|
-
'issue.buckets': defineFn({
|
|
1380
|
-
input: {
|
|
1381
|
-
projectId: 'id?',
|
|
1382
|
-
epicId: 'id?',
|
|
1383
|
-
sprintId: 'id?',
|
|
1384
|
-
assigneeUserId: 'id?',
|
|
1385
|
-
mine: 'bool?',
|
|
1386
|
-
includeArchived: 'bool?',
|
|
1387
|
-
listState: 'json',
|
|
1388
|
-
/** Optional: the company's own civil date when the caller names none. */
|
|
1389
|
-
today: 'text?',
|
|
1390
|
-
},
|
|
1391
|
-
output: {
|
|
1392
|
-
total: 'int',
|
|
1393
|
-
done: 'int',
|
|
1394
|
-
overdue: 'int',
|
|
1395
|
-
waiting: 'int',
|
|
1396
|
-
working: 'int',
|
|
1397
|
-
today: 'text',
|
|
1398
|
-
},
|
|
1399
|
-
// The same `issueQuery` `issue.list` and `issue.group` run, so the same
|
|
1400
|
-
// `flow.IssueFieldValue` read whenever the state carries a `field:<code>`
|
|
1401
|
-
// rule — see resolveFieldFilters. A capability nobody declares is one
|
|
1402
|
-
// nobody reviewed, and this one was missing while the other two had it.
|
|
1403
|
-
effects: [
|
|
1404
|
-
'read:flow.Issue',
|
|
1405
|
-
'read:flow.Column',
|
|
1406
|
-
'read:flow.FieldDef',
|
|
1407
|
-
'read:flow.IssueFieldValue',
|
|
1408
|
-
// Where the company keeps its calendar — see businessTimezone.
|
|
1409
|
-
'read:company.Company',
|
|
1410
|
-
...membershipEffects,
|
|
1411
|
-
],
|
|
1412
|
-
agent: true,
|
|
1413
|
-
handler: (ctx, args) => issueBuckets(ctx, args, args.today == null ? undefined : String(args.today)),
|
|
1414
|
-
}),
|
|
1415
|
-
'issue.group': defineFn({
|
|
1416
|
-
input: {
|
|
1417
|
-
projectId: 'id?',
|
|
1418
|
-
columnId: 'id?',
|
|
1419
|
-
epicId: 'id?',
|
|
1420
|
-
sprintId: 'id?',
|
|
1421
|
-
assigneeUserId: 'id?',
|
|
1422
|
-
mine: 'bool?',
|
|
1423
|
-
includeArchived: 'bool?',
|
|
1424
|
-
listState: 'json',
|
|
1425
|
-
path: 'json?',
|
|
1426
|
-
timezone: 'text?',
|
|
1427
|
-
limit: 'int?',
|
|
1428
|
-
offset: 'int?',
|
|
1429
|
-
},
|
|
1430
|
-
effects: [...flowReadEffects, 'read:company.Company'],
|
|
1431
|
-
agent: true,
|
|
1432
|
-
handler: (ctx, args) => groupIssues(ctx, args),
|
|
1433
|
-
}),
|
|
1434
|
-
'issue.get': defineFn({
|
|
1435
|
-
input: { id: 'id' },
|
|
1436
|
-
effects: [
|
|
1437
|
-
...flowReadEffects,
|
|
1438
|
-
'read:flow.Project',
|
|
1439
|
-
'read:flow.ProjectMember',
|
|
1440
|
-
'read:flow.ProjectAccessGrant',
|
|
1441
|
-
'read:user.User',
|
|
1442
|
-
],
|
|
1443
|
-
agent: true,
|
|
1444
|
-
// Not found rather than forbidden: that an issue exists in a project this
|
|
1445
|
-
// caller cannot see is itself the half of the answer to withhold.
|
|
1446
|
-
handler: async (ctx, args) => {
|
|
1447
|
-
const held = await issueDetail(ctx, String(args.id));
|
|
1448
|
-
return held && (await canReadProject(ctx, held.projectId)) ? held : null;
|
|
1449
|
-
},
|
|
1450
|
-
}),
|
|
1451
|
-
/**
|
|
1452
|
-
* The permission that guards the collaborative description.
|
|
1453
|
-
*
|
|
1454
|
-
* The description is a Yjs document edited over flow_backend's `/push` and
|
|
1455
|
-
* `/leave` routes, not through `issue.save`, so it needs its own grantable
|
|
1456
|
-
* key — those routes used to authorize with `issue.get`, which made a
|
|
1457
|
-
* read-only role able to rewrite any issue's text. It answers with the
|
|
1458
|
-
* issue so the caller does not read it twice.
|
|
1459
|
-
*/
|
|
1460
|
-
'issue.editDescription': defineFn({
|
|
1461
|
-
input: { id: 'id' },
|
|
1462
|
-
effects: ['read:flow.Issue', ...membershipEffects],
|
|
1463
|
-
agent: true,
|
|
1464
|
-
handler: async (ctx, args) => (await readableRow(ctx, 'flow.Issue', args.id)) ?? null,
|
|
1465
|
-
}),
|
|
1466
|
-
'issue.save': defineFn({
|
|
1467
|
-
input: {
|
|
1468
|
-
id: 'id',
|
|
1469
|
-
projectId: 'id',
|
|
1470
|
-
columnId: 'id',
|
|
1471
|
-
typeId: 'id?',
|
|
1472
|
-
epicId: 'id?',
|
|
1473
|
-
sprintId: 'id?',
|
|
1474
|
-
parentIssueId: 'id?',
|
|
1475
|
-
title: 'text',
|
|
1476
|
-
assigneeUserId: 'id?',
|
|
1477
|
-
priority: 'text?',
|
|
1478
|
-
startDate: 'date?',
|
|
1479
|
-
dueDate: 'date?',
|
|
1480
|
-
estimate: 'decimal?',
|
|
1481
|
-
tagIds: 'json?',
|
|
1482
|
-
/** Custom field values, keyed by field id or by field code. */
|
|
1483
|
-
fields: 'json?',
|
|
1484
|
-
expectedVersion: 'int?',
|
|
1485
|
-
idempotencyKey: 'text',
|
|
1486
|
-
},
|
|
1487
|
-
output: { ok: 'bool', id: 'id?', version: 'int?', errors: 'json?' },
|
|
1488
|
-
effects: [...issueWriteEffects],
|
|
1489
|
-
idempotent: true,
|
|
1490
|
-
agent: true,
|
|
1491
|
-
handler: (ctx, args) => saveIssue(ctx, {
|
|
1492
|
-
...args,
|
|
1493
|
-
id: String(args.id),
|
|
1494
|
-
projectId: String(args.projectId),
|
|
1495
|
-
columnId: String(args.columnId),
|
|
1496
|
-
title: String(args.title),
|
|
1497
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1498
|
-
tagIds: Array.isArray(args.tagIds) ? args.tagIds.map(String) : undefined,
|
|
1499
|
-
fields: args.fields && typeof args.fields === 'object'
|
|
1500
|
-
? args.fields
|
|
1501
|
-
: undefined,
|
|
1502
|
-
}),
|
|
1503
|
-
}),
|
|
1504
|
-
'issue.move': defineFn({
|
|
1505
|
-
input: { id: 'id', columnId: 'id', expectedVersion: 'int', idempotencyKey: 'text' },
|
|
1506
|
-
output: { ok: 'bool', id: 'id?', version: 'int?', errors: 'json?' },
|
|
1507
|
-
// The move leaves a timeline entry now — "who put this in Done, and when"
|
|
1508
|
-
// is the question the cluster could not answer — so it writes to the thread.
|
|
1509
|
-
effects: [
|
|
1510
|
-
'read:flow.Issue',
|
|
1511
|
-
'write:flow.Issue',
|
|
1512
|
-
'read:flow.Column',
|
|
1513
|
-
'read:flow.IssueDependency',
|
|
1514
|
-
...timelineEntryEffects,
|
|
1515
|
-
...membershipEffects,
|
|
1516
|
-
],
|
|
1517
|
-
idempotent: true,
|
|
1518
|
-
agent: true,
|
|
1519
|
-
handler: (ctx, args) => moveIssue(ctx, {
|
|
1520
|
-
id: String(args.id),
|
|
1521
|
-
columnId: String(args.columnId),
|
|
1522
|
-
expectedVersion: Number(args.expectedVersion),
|
|
1523
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1524
|
-
}),
|
|
1525
|
-
}),
|
|
1526
|
-
/**
|
|
1527
|
-
* One action over many issues — see bulkIssues, and read the note there
|
|
1528
|
-
* about what it deliberately does not guarantee.
|
|
1529
|
-
*/
|
|
1530
|
-
'issue.bulk': defineFn({
|
|
1531
|
-
input: { ids: 'json', action: 'text', value: 'id?', idempotencyKey: 'text' },
|
|
1532
|
-
output: { ok: 'bool', applied: 'int?', refused: 'json?', errors: 'json?' },
|
|
1533
|
-
effects: [
|
|
1534
|
-
'read:flow.Issue',
|
|
1535
|
-
'write:flow.Issue',
|
|
1536
|
-
'read:flow.Column',
|
|
1537
|
-
'write:flow.IssueFieldValue',
|
|
1538
|
-
'write:flow.IssueTag',
|
|
1539
|
-
'read:flow.Tag',
|
|
1540
|
-
'read:flow.IssueTag',
|
|
1541
|
-
'read:flow.FieldDef',
|
|
1542
|
-
'read:flow.IssueType',
|
|
1543
|
-
'read:flow.Sprint',
|
|
1544
|
-
'read:mail.Thread',
|
|
1545
|
-
'write:mail.Thread',
|
|
1546
|
-
'read:mail.Message',
|
|
1547
|
-
'write:mail.Message',
|
|
1548
|
-
'read:mail.Follower',
|
|
1549
|
-
'write:mail.Follower',
|
|
1550
|
-
'read:mail.Subtype',
|
|
1551
|
-
'write:mail.Notification',
|
|
1552
|
-
'write:mail.TrackingValue',
|
|
1553
|
-
'read:user.User',
|
|
1554
|
-
...membershipEffects,
|
|
1555
|
-
],
|
|
1556
|
-
idempotent: true,
|
|
1557
|
-
agent: true,
|
|
1558
|
-
handler: (ctx, args) => bulkIssues(ctx, {
|
|
1559
|
-
ids: Array.isArray(args.ids) ? args.ids.map(String) : [],
|
|
1560
|
-
action: String(args.action),
|
|
1561
|
-
value: args.value ? String(args.value) : null,
|
|
1562
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1563
|
-
}),
|
|
1564
|
-
}),
|
|
1565
|
-
/** A copy of an issue in another project — see copyIssue for what it brings. */
|
|
1566
|
-
'issue.copyTo': defineFn({
|
|
1567
|
-
input: { id: 'id', projectId: 'id', columnId: 'id?', title: 'text?', idempotencyKey: 'text' },
|
|
1568
|
-
output: {
|
|
1569
|
-
ok: 'bool',
|
|
1570
|
-
id: 'id?',
|
|
1571
|
-
columnId: 'id?',
|
|
1572
|
-
fieldsCarried: 'int?',
|
|
1573
|
-
fieldsDropped: 'int?',
|
|
1574
|
-
typeCleared: 'bool?',
|
|
1575
|
-
errors: 'json?',
|
|
1576
|
-
},
|
|
1577
|
-
effects: [
|
|
1578
|
-
'read:flow.Issue',
|
|
1579
|
-
'write:flow.Issue',
|
|
1580
|
-
'read:flow.Column',
|
|
1581
|
-
'read:flow.IssueType',
|
|
1582
|
-
'read:flow.FieldDef',
|
|
1583
|
-
'read:flow.IssueFieldValue',
|
|
1584
|
-
'write:flow.IssueFieldValue',
|
|
1585
|
-
'read:flow.Tag',
|
|
1586
|
-
'read:flow.IssueTag',
|
|
1587
|
-
'write:flow.IssueTag',
|
|
1588
|
-
'read:flow.Sprint',
|
|
1589
|
-
'read:mail.Thread',
|
|
1590
|
-
'write:mail.Thread',
|
|
1591
|
-
'read:mail.Message',
|
|
1592
|
-
'write:mail.Message',
|
|
1593
|
-
'read:mail.Follower',
|
|
1594
|
-
'write:mail.Follower',
|
|
1595
|
-
'read:mail.Subtype',
|
|
1596
|
-
'write:mail.Notification',
|
|
1597
|
-
'write:mail.TrackingValue',
|
|
1598
|
-
'read:user.User',
|
|
1599
|
-
...membershipEffects,
|
|
1600
|
-
],
|
|
1601
|
-
idempotent: true,
|
|
1602
|
-
agent: true,
|
|
1603
|
-
handler: (ctx, args) => copyIssue(ctx, {
|
|
1604
|
-
id: String(args.id),
|
|
1605
|
-
projectId: String(args.projectId),
|
|
1606
|
-
columnId: args.columnId ? String(args.columnId) : null,
|
|
1607
|
-
title: args.title ? String(args.title) : null,
|
|
1608
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1609
|
-
}),
|
|
1610
|
-
}),
|
|
1611
|
-
/**
|
|
1612
|
-
* Take an issue to another project — see transferIssue for what travels.
|
|
1613
|
-
*
|
|
1614
|
-
* Its own key rather than a shape of `issue.save`, which refuses to change
|
|
1615
|
-
* a project at all: moving work between projects touches columns, types,
|
|
1616
|
-
* epics, sprints and custom fields, and priced as an edit it would look like
|
|
1617
|
-
* renaming a title.
|
|
1618
|
-
*/
|
|
1619
|
-
'issue.transfer': defineFn({
|
|
1620
|
-
input: {
|
|
1621
|
-
id: 'id',
|
|
1622
|
-
projectId: 'id',
|
|
1623
|
-
columnId: 'id?',
|
|
1624
|
-
expectedVersion: 'int',
|
|
1625
|
-
idempotencyKey: 'text',
|
|
1626
|
-
},
|
|
1627
|
-
output: {
|
|
1628
|
-
ok: 'bool',
|
|
1629
|
-
id: 'id?',
|
|
1630
|
-
version: 'int?',
|
|
1631
|
-
columnId: 'id?',
|
|
1632
|
-
fieldsCarried: 'int?',
|
|
1633
|
-
fieldsDropped: 'int?',
|
|
1634
|
-
typeCleared: 'bool?',
|
|
1635
|
-
epicCleared: 'bool?',
|
|
1636
|
-
sprintCleared: 'bool?',
|
|
1637
|
-
errors: 'json?',
|
|
1638
|
-
},
|
|
1639
|
-
effects: [
|
|
1640
|
-
'read:flow.Issue',
|
|
1641
|
-
'write:flow.Issue',
|
|
1642
|
-
'read:flow.Column',
|
|
1643
|
-
'read:flow.IssueType',
|
|
1644
|
-
'read:flow.FieldDef',
|
|
1645
|
-
'read:flow.IssueFieldValue',
|
|
1646
|
-
'write:flow.IssueFieldValue',
|
|
1647
|
-
...membershipEffects,
|
|
1648
|
-
],
|
|
1649
|
-
idempotent: true,
|
|
1650
|
-
agent: true,
|
|
1651
|
-
handler: (ctx, args) => transferIssue(ctx, {
|
|
1652
|
-
id: String(args.id),
|
|
1653
|
-
projectId: String(args.projectId),
|
|
1654
|
-
columnId: args.columnId ? String(args.columnId) : null,
|
|
1655
|
-
expectedVersion: Number(args.expectedVersion),
|
|
1656
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1657
|
-
}),
|
|
1658
|
-
}),
|
|
1659
|
-
'issue.assignSprint': defineFn({
|
|
1660
|
-
input: { id: 'id', sprintId: 'id?', expectedVersion: 'int', idempotencyKey: 'text' },
|
|
1661
|
-
output: { ok: 'bool', id: 'id?', version: 'int?', errors: 'json?' },
|
|
1662
|
-
effects: [
|
|
1663
|
-
'read:flow.Issue',
|
|
1664
|
-
'write:flow.Issue',
|
|
1665
|
-
'read:flow.Sprint',
|
|
1666
|
-
...timelineEntryEffects,
|
|
1667
|
-
...membershipEffects,
|
|
1668
|
-
],
|
|
1669
|
-
idempotent: true,
|
|
1670
|
-
agent: true,
|
|
1671
|
-
handler: (ctx, args) => assignSprint(ctx, {
|
|
1672
|
-
id: String(args.id),
|
|
1673
|
-
sprintId: args.sprintId ? String(args.sprintId) : null,
|
|
1674
|
-
expectedVersion: Number(args.expectedVersion),
|
|
1675
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1676
|
-
}),
|
|
1677
|
-
}),
|
|
1678
|
-
'issue.comment': defineFn({
|
|
1679
|
-
input: {
|
|
1680
|
-
id: 'id',
|
|
1681
|
-
issueId: 'id',
|
|
1682
|
-
body: 'text',
|
|
1683
|
-
kind: 'text?',
|
|
1684
|
-
/** Users this comment is addressed to, beyond whoever already follows. */
|
|
1685
|
-
mentionUserIds: 'json?',
|
|
1686
|
-
idempotencyKey: 'text',
|
|
1687
|
-
},
|
|
1688
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
1689
|
-
effects: [...commentEffects, 'read:user.User', 'write:mail.Mention', ...membershipEffects],
|
|
1690
|
-
idempotent: true,
|
|
1691
|
-
agent: true,
|
|
1692
|
-
handler: (ctx, args) => addComment(ctx, {
|
|
1693
|
-
id: String(args.id),
|
|
1694
|
-
issueId: String(args.issueId),
|
|
1695
|
-
body: String(args.body),
|
|
1696
|
-
kind: args.kind === 'note' ? 'note' : 'comment',
|
|
1697
|
-
mentionUserIds: Array.isArray(args.mentionUserIds) ? args.mentionUserIds.map(String) : [],
|
|
1698
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1699
|
-
}),
|
|
1700
|
-
}),
|
|
1701
|
-
/**
|
|
1702
|
-
* Off the board, without claiming it was finished — see archiveIssue.
|
|
1703
|
-
*
|
|
1704
|
-
* Its own key rather than a flag on `issue.save`: taking work out of every
|
|
1705
|
-
* figure the project reports is a different act from editing a field, and the
|
|
1706
|
-
* catalogue can price it separately.
|
|
1707
|
-
*/
|
|
1708
|
-
'issue.archive': defineFn({
|
|
1709
|
-
input: { id: 'id', expectedVersion: 'int', idempotencyKey: 'text' },
|
|
1710
|
-
output: { ok: 'bool', id: 'id?', version: 'int?', errors: 'json?' },
|
|
1711
|
-
effects: ['read:flow.Issue', 'write:flow.Issue', ...membershipEffects],
|
|
1712
|
-
idempotent: true,
|
|
1713
|
-
agent: true,
|
|
1714
|
-
handler: (ctx, args) => archiveIssue(ctx, {
|
|
1715
|
-
id: String(args.id),
|
|
1716
|
-
expectedVersion: n(args.expectedVersion),
|
|
1717
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1718
|
-
}),
|
|
1719
|
-
}),
|
|
1720
|
-
'issue.restore': defineFn({
|
|
1721
|
-
input: { id: 'id', expectedVersion: 'int', idempotencyKey: 'text' },
|
|
1722
|
-
output: { ok: 'bool', id: 'id?', version: 'int?', errors: 'json?' },
|
|
1723
|
-
effects: ['read:flow.Issue', 'write:flow.Issue', ...membershipEffects],
|
|
1724
|
-
idempotent: true,
|
|
1725
|
-
agent: true,
|
|
1726
|
-
handler: (ctx, args) => restoreIssue(ctx, {
|
|
1727
|
-
id: String(args.id),
|
|
1728
|
-
expectedVersion: n(args.expectedVersion),
|
|
1729
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1730
|
-
}),
|
|
1731
|
-
}),
|
|
1732
|
-
/** The door `issue.unfollow` never had a pair for — see startFollowing. */
|
|
1733
|
-
'issue.follow': defineFn({
|
|
1734
|
-
input: { issueId: 'id', idempotencyKey: 'text' },
|
|
1735
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
1736
|
-
effects: [
|
|
1737
|
-
'read:flow.Issue',
|
|
1738
|
-
'read:user.User',
|
|
1739
|
-
'read:mail.Thread',
|
|
1740
|
-
'read:partner.Partner',
|
|
1741
|
-
'read:mail.Follower',
|
|
1742
|
-
'write:mail.Follower',
|
|
1743
|
-
...membershipEffects,
|
|
1744
|
-
],
|
|
1745
|
-
idempotent: true,
|
|
1746
|
-
agent: true,
|
|
1747
|
-
handler: (ctx, args) => startFollowing(ctx, {
|
|
1748
|
-
issueId: String(args.issueId),
|
|
1749
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1750
|
-
}),
|
|
1751
|
-
}),
|
|
1752
|
-
/**
|
|
1753
|
-
* Leaves an issue's thread.
|
|
1754
|
-
*
|
|
1755
|
-
* A separate key from commenting, because it is the opposite act: everything
|
|
1756
|
-
* else in this module hands out subscriptions, and this is the only way to
|
|
1757
|
-
* give one back.
|
|
1758
|
-
*/
|
|
1759
|
-
'issue.unfollow': defineFn({
|
|
1760
|
-
input: { issueId: 'id', idempotencyKey: 'text' },
|
|
1761
|
-
output: { ok: 'bool', removed: 'int?', errors: 'json?' },
|
|
1762
|
-
// `unfollowThread` clears the follower's per-subtype rows too, which the
|
|
1763
|
-
// effect system refused until it was said out loud — which is the point of
|
|
1764
|
-
// it: a capability nobody declared is one nobody reviewed.
|
|
1765
|
-
effects: [
|
|
1766
|
-
'read:flow.Issue',
|
|
1767
|
-
'read:user.User',
|
|
1768
|
-
'read:mail.Follower',
|
|
1769
|
-
'write:mail.Follower',
|
|
1770
|
-
'write:mail.FollowerSubtype',
|
|
1771
|
-
...membershipEffects,
|
|
1772
|
-
],
|
|
1773
|
-
idempotent: true,
|
|
1774
|
-
agent: true,
|
|
1775
|
-
handler: (ctx, args) => stopFollowing(ctx, {
|
|
1776
|
-
issueId: String(args.issueId),
|
|
1777
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1778
|
-
}),
|
|
1779
|
-
}),
|
|
1780
|
-
'issue.dependency.add': defineFn({
|
|
1781
|
-
input: { id: 'id', issueId: 'id', dependsOnIssueId: 'id', relation: 'text', idempotencyKey: 'text' },
|
|
1782
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
1783
|
-
effects: [
|
|
1784
|
-
'read:flow.Issue',
|
|
1785
|
-
'read:flow.IssueDependency',
|
|
1786
|
-
'write:flow.IssueDependency',
|
|
1787
|
-
...membershipEffects,
|
|
1788
|
-
],
|
|
1789
|
-
idempotent: true,
|
|
1790
|
-
agent: true,
|
|
1791
|
-
handler: (ctx, args) => addDependency(ctx, {
|
|
1792
|
-
id: String(args.id),
|
|
1793
|
-
issueId: String(args.issueId),
|
|
1794
|
-
dependsOnIssueId: String(args.dependsOnIssueId),
|
|
1795
|
-
relation: String(args.relation),
|
|
1796
|
-
idempotencyKey: String(args.idempotencyKey),
|
|
1797
|
-
}),
|
|
1798
|
-
}),
|
|
1799
|
-
'issue.dependency.remove': defineFn({
|
|
1800
|
-
input: { id: 'id' },
|
|
1801
|
-
output: { ok: 'bool', id: 'id?', errors: 'json?' },
|
|
1802
|
-
effects: [
|
|
1803
|
-
'read:flow.IssueDependency',
|
|
1804
|
-
'write:flow.IssueDependency',
|
|
1805
|
-
'read:flow.Issue',
|
|
1806
|
-
...membershipEffects,
|
|
1807
|
-
],
|
|
1808
|
-
idempotent: true,
|
|
1809
|
-
agent: true,
|
|
1810
|
-
handler: async (ctx, args) => {
|
|
1811
|
-
const existing = (await ctx.db.select('flow.IssueDependency', { id: args.id }))[0];
|
|
1812
|
-
// The edge carries no project of its own, so it is read through the issue
|
|
1813
|
-
// it hangs off — cutting a link between two issues is editing the project
|
|
1814
|
-
// they are in, and needs the same standing as anything else there.
|
|
1815
|
-
if (!existing || !(await readableRow(ctx, 'flow.Issue', existing.issueId)))
|
|
1816
|
-
return invalid(issue('id', 'flow.error.notFound'));
|
|
1817
|
-
const D = ctx.table('flow.IssueDependency');
|
|
1818
|
-
await ctx.db.del(deleteFrom(D).where(eq(D.id, args.id)));
|
|
1819
|
-
return { ok: true, id: args.id };
|
|
1820
|
-
},
|
|
1821
|
-
}),
|
|
1822
|
-
/**
|
|
1823
|
-
* Issues as picker rows, for the one field that points at another issue —
|
|
1824
|
-
* the dependency target. `issue.list` answers a paged envelope the picker
|
|
1825
|
-
* cannot read, and its search only takes a `listState`, so this wraps that
|
|
1826
|
-
* the same way `crm.case.options` wraps `listCases`.
|
|
1827
|
-
*/
|
|
1828
|
-
'issue.options': defineFn({
|
|
1829
|
-
input: { search: 'text?', limit: 'int?', projectId: 'id?', excludeId: 'id?' },
|
|
1830
|
-
output: { id: 'id', title: 'text', columnName: 'text?' },
|
|
1831
|
-
effects: [...flowReadEffects, 'read:company.Company'],
|
|
1832
|
-
agent: true,
|
|
1833
|
-
handler: async (ctx, args) => {
|
|
1834
|
-
const found = await listIssues(ctx, {
|
|
1835
|
-
...(args.projectId ? { projectId: args.projectId } : {}),
|
|
1836
|
-
listState: args.search ? { ...emptyIssueListState(), q: String(args.search) } : undefined,
|
|
1837
|
-
limit: Math.max(1, Math.min(100, n(args.limit ?? 40))),
|
|
1838
|
-
});
|
|
1839
|
-
return found.rows
|
|
1840
|
-
.filter((row) => !args.excludeId || row.id !== args.excludeId)
|
|
1841
|
-
.map((row) => ({ id: row.id, title: row.title, columnName: row.columnName ?? null }));
|
|
1842
|
-
},
|
|
1843
|
-
}),
|
|
1844
|
-
/** Blocking edges among a node set — the map view's one batch read, see dependenciesFor. */
|
|
1845
|
-
'issue.dependencies': defineFn({
|
|
1846
|
-
input: { issueIds: 'json', includeExternalTargets: 'bool?' },
|
|
1847
|
-
output: { issueId: 'id', dependsOnIssueId: 'id', relation: 'text' },
|
|
1848
|
-
effects: ['read:flow.IssueDependency', 'read:flow.Issue', ...membershipEffects],
|
|
1849
|
-
agent: true,
|
|
1850
|
-
// The ids come from the caller, so this answered about any issue anybody
|
|
1851
|
-
// named — including whether an issue in a project they cannot see has
|
|
1852
|
-
// blockers, which is an existence answer about a hidden project. The map
|
|
1853
|
-
// view passes ids it just read, so filtering costs it nothing (FLW-018).
|
|
1854
|
-
handler: (ctx, args) => dependenciesFor(ctx, Array.isArray(args.issueIds) ? args.issueIds.map(String) : [], args.includeExternalTargets === true),
|
|
1855
|
-
}),
|
|
1856
|
-
};
|
|
1857
|
-
//# sourceMappingURL=functions.js.map
|