Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"extends @nextcloud/browserslist-config"
],
"dependencies": {
"@conduction/nextcloud-vue": "^2.31.1",
"@conduction/nextcloud-vue": "^2.34.0",
"@nextcloud/auth": "^2.6.0",
"@nextcloud/axios": "~2.5.2",
"@nextcloud/capabilities": "^1.2.1",
Expand Down
43 changes: 39 additions & 4 deletions src/dialogs/DecisionFormDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
Dialog: schema-driven create/edit form for Decision objects, with the
decisionType picker fed from the registry.

This is a manifest `form-dialog` slot replacement for the Decisions and
Motions index pages (wired via each page's `slots` map in
src/manifest.json). The built-in dialog those pages otherwise render
This is a manifest `form-dialog` slot replacement for every decidiq
surface that renders the Decision schema in a form: the Decisions and
Motions INDEX pages, and the Decision, Motion, Amendment and Decision
integrations DETAIL pages (wired via each page's `slots` map in
src/manifest.json). CnIndexPage and CnDetailPage deliberately name the
slot the same and scope it the same, so one component serves both.
The built-in dialog those pages otherwise render
builds its type picker from `properties.decisionType.enum` in the stored
schema — and decision-types-as-configuration (#1099) deliberately
emptied that enum, making the `decision_types` app config the only
Expand All @@ -30,10 +34,11 @@
<template>
<CnFormDialog
v-if="show"
ref="dialog"
:schema="typedSchema"
:item="item"
register="decidiq"
@confirm="confirm"
@confirm="onConfirm"
@close="close" />
</template>

Expand All @@ -43,6 +48,7 @@ import {
listDecisionTypes,
withDecisionTypeVocabulary,
} from '../integrations/decisionLink.js'
import { settleFormDialogResult } from './formDialogResult.js'

export default {
name: 'DecisionFormDialog',
Expand Down Expand Up @@ -104,5 +110,34 @@ export default {
async mounted() {
this.decisionTypes = await listDecisionTypes()
},

methods: {
/**
* Save through the page's own persistence path, then hand the
* outcome back to the dialog that submitted it.
*
* The result matters because this dialog is ours, not the page's:
* CnFormDialog raises `loading` on submit and only `setResult()`
* lowers it, with `no-close` bound to `loading`. On CnDetailPage a
* failed edit leaves the form open, so dropping the result would
* strand the user in a modal that can neither retry nor close.
* CnIndexPage's `confirm` resolves to nothing and closes the dialog
* by flipping `show` instead, which settleFormDialogResult() treats
* as a normal outcome rather than a fault, so this one component
* still serves both pages.
*
* @param {object} formData The submitted form data.
* @param {?object} extra CnFormDialog's second confirm argument
* (extension answers), passed through untouched.
*
* @return {Promise<void>}
*
* @spec openspec/changes/decision-types-as-configuration/specs/decidesk-contract-decision-hub/spec.md
*/
async onConfirm(formData, extra) {
const result = await this.confirm(formData, extra)
settleFormDialogResult(this.$refs.dialog, result)
},
},
}
</script>
51 changes: 51 additions & 0 deletions src/dialogs/formDialogResult.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* SPDX-FileCopyrightText: 2026 Conduction B.V.
* SPDX-License-Identifier: EUPL-1.2
*
* Result plumbing for a `form-dialog` slot replacement.
*
* A page that replaces its built-in form dialog through the `form-dialog`
* slot renders its own `CnFormDialog`, and the host holds no ref to it. That
* makes the save result the only channel back: `CnFormDialog` raises its
* `loading` flag on submit and ONLY `setResult()` lowers it again, while
* `no-close` is bound to `loading`. A replacement that drops the result
* therefore locks its own modal open, on a successful save as well as on a
* failed one, with no error and nothing in the console.
*
* `CnDetailPage`'s slot `confirm` resolves to `{ success: true, data }` or
* `{ error }` for exactly this reason. `CnIndexPage`'s does not resolve to
* anything yet (nextcloud-vue#944 left that half alone deliberately), and it
* closes its dialog by flipping the slot's `show` instead — so this helper
* has to accept "no result" as a normal outcome rather than a fault, and one
* replacement component can serve both pages.
*
* Kept in a plain .js module because this repo's vitest runs on plain Vite
* with no @vitejs/plugin-vue, so a `.vue` file cannot be imported by a spec.
* The logic that must be tested lives here and the SFC calls it.
*
* @spec openspec/changes/decision-types-as-configuration/specs/decidesk-contract-decision-hub/spec.md
*/

/**
* Hand a slot `confirm`'s resolved result back to the dialog that submitted it.
*
* @param {?object} dialog The CnFormDialog instance (a `$refs` entry), or null
* when the dialog has already unmounted because `show` went false.
* @param {?object} result What the slot's `confirm` resolved to:
* `{ success: true, data }` / `{ error }` on CnDetailPage, `undefined` on a
* page whose confirm returns nothing.
*
* @return {boolean} True when the dialog was settled, false when there was
* nothing to settle (no dialog, or no result to settle it with).
*
* @spec openspec/changes/decision-types-as-configuration/specs/decidesk-contract-decision-hub/spec.md
*/
export function settleFormDialogResult(dialog, result) {
// `setResult` reads `resultData.success`, so a null / undefined result
// would throw rather than close anything. A page whose confirm resolves
// to nothing closes its dialog itself; leave that path alone.
if (!result || typeof result !== 'object') return false
if (!dialog || typeof dialog.setResult !== 'function') return false
dialog.setResult(result)
return true
}
9 changes: 9 additions & 0 deletions src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,9 @@
"route": "/decisions/:id/integrations",
"type": "detail",
"title": "Decision integrations",
"slots": {
"form-dialog": "DecisionFormDialog"
},
"config": {
"register": "decidiq",
"schema": "decision",
Expand Down Expand Up @@ -876,7 +879,9 @@
"id": "MotionDetail",
"route": "/motions/:id",
"type": "detail",
"_note": "decision-types-as-configuration: same wiring and same reason as DecisionDetail. This page renders the Decision schema too, so its built-in Edit dialog showed an empty decisionType picker until the form-dialog slot (nextcloud-vue#944) let DecisionFormDialog splice the registry vocabulary in.",
"slots": {
"form-dialog": "DecisionFormDialog",
"widget-motion-amendments": "MotionAmendmentsTab",
"widget-motion-votes": "MotionVotesTab",
"widget-motion-amendment-order": "MotionAmendmentOrderTab",
Expand Down Expand Up @@ -960,7 +965,9 @@
},
"documentationUrl": "https://decidiq.conduction.nl"
},
"_note": "decision-types-as-configuration: same wiring and same reason as DecisionDetail. This page renders the Decision schema too, so its built-in Edit dialog showed an empty decisionType picker until the form-dialog slot (nextcloud-vue#944) let DecisionFormDialog splice the registry vocabulary in.",
"slots": {
"form-dialog": "DecisionFormDialog",
"widget-amend-diff": "AmendmentDiffTab",
"widget-amend-parent": "AmendmentParentMotionTab"
}
Expand Down Expand Up @@ -1131,7 +1138,9 @@
"id": "DecisionDetail",
"route": "/decisions/:id",
"type": "detail",
"_note": "decision-types-as-configuration: this detail page's built-in Edit dialog renders the same Decision schema the index pages do, and #1099 deliberately emptied properties.decisionType.enum (the decision_types app config is the only authority), so its type picker offered nothing and the required field blocked the save. #1109 fixed the two index pages through CnIndexPage's form-dialog slot; CnDetailPage grew the same slot with the same scope in nextcloud-vue#944, so the same DecisionFormDialog is wired here rather than a second copy of the vocabulary logic. The note stays a SIBLING of slots: CnPageRenderer resolves every slots entry as a registry name and warns on one it cannot find.",
"slots": {
"form-dialog": "DecisionFormDialog",
"widget-decision-lifecycle": "DecisionLifecycleTab",
"widget-decision-actions": "ActionItemsSurface",
"widget-decision-related": "RelatedDecisionsTab",
Expand Down
Loading
Loading