@conduction/nextcloud-vue 2.119.0 → 2.121.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js +9 -0
  2. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js.map +1 -0
  3. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js +226 -0
  4. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js +211 -0
  6. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js +9 -0
  8. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js.map +1 -0
  9. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js +64 -0
  10. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js.map +1 -0
  11. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js +89 -0
  12. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js.map +1 -0
  13. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  14. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  15. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +53 -1
  16. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  17. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +546 -523
  18. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  19. package/dist/esm/components/CnIndexPage/useNamedSource.js +9 -0
  20. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  21. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js +9 -0
  22. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js.map +1 -0
  23. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js +257 -0
  24. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js.map +1 -0
  25. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js +266 -0
  26. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js.map +1 -0
  27. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue.js +1 -1
  28. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js +14 -7
  29. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js.map +1 -1
  30. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue3.js.map +1 -1
  31. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js +9 -0
  32. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js.map +1 -0
  33. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js +278 -0
  34. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js.map +1 -0
  35. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js +128 -0
  36. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js.map +1 -0
  37. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js +9 -0
  38. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js.map +1 -0
  39. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js +214 -0
  40. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js.map +1 -0
  41. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js +125 -0
  42. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js.map +1 -0
  43. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js +9 -0
  44. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js.map +1 -0
  45. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js +163 -0
  46. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js.map +1 -0
  47. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js +131 -0
  48. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js.map +1 -0
  49. package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js +127 -0
  50. package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js.map +1 -0
  51. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js +9 -0
  52. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js.map +1 -0
  53. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js +82 -0
  54. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js.map +1 -0
  55. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js +61 -0
  56. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js.map +1 -0
  57. package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js +60 -0
  58. package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js.map +1 -0
  59. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js +10 -0
  60. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js.map +1 -0
  61. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js +226 -0
  62. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js.map +1 -0
  63. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js +126 -0
  64. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js.map +1 -0
  65. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js +10 -0
  66. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js.map +1 -0
  67. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js +944 -0
  68. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js.map +1 -0
  69. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js +276 -0
  70. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js.map +1 -0
  71. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js +9 -0
  72. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js.map +1 -0
  73. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js +354 -0
  74. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js.map +1 -0
  75. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js +392 -0
  76. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js.map +1 -0
  77. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js +9 -0
  78. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js.map +1 -0
  79. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js +180 -0
  80. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js.map +1 -0
  81. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js +208 -0
  82. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js.map +1 -0
  83. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js +9 -0
  84. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js.map +1 -0
  85. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js +244 -0
  86. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js.map +1 -0
  87. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js +76 -0
  88. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js.map +1 -0
  89. package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js +290 -0
  90. package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js.map +1 -0
  91. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +9 -0
  92. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js.map +1 -0
  93. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +448 -0
  94. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -0
  95. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +303 -0
  96. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -0
  97. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +1 -1
  98. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +19 -2
  99. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
  100. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +73 -62
  101. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
  102. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +6 -0
  103. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  104. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +42 -32
  105. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  106. package/dist/esm/composables/indexSources.js +116 -16
  107. package/dist/esm/composables/indexSources.js.map +1 -1
  108. package/dist/esm/index.js +9 -0
  109. package/dist/esm/index.js.map +1 -1
  110. package/dist/esm/l10n/en.json.js +207 -1
  111. package/dist/esm/l10n/en.json.js.map +1 -1
  112. package/dist/esm/l10n/nl.json.js +207 -1
  113. package/dist/esm/l10n/nl.json.js.map +1 -1
  114. package/dist/esm/library-style.js +1 -1
  115. package/dist/esm/nextcloud-vue.css +951 -16
  116. package/dist/esm/utils/aiFeatures.js +245 -0
  117. package/dist/esm/utils/aiFeatures.js.map +1 -0
  118. package/dist/esm/utils/aiSuggestionTracker.js +182 -0
  119. package/dist/esm/utils/aiSuggestionTracker.js.map +1 -0
  120. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  121. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  122. package/dist/esm/utils/objectPermissions.js +552 -0
  123. package/dist/esm/utils/objectPermissions.js.map +1 -0
  124. package/dist/esm/utils/objectTranslations.js +180 -0
  125. package/dist/esm/utils/objectTranslations.js.map +1 -0
  126. package/dist/esm/utils/stateMachineValidation.js +254 -0
  127. package/dist/esm/utils/stateMachineValidation.js.map +1 -0
  128. package/dist/esm/utils/widgetDispatch.js +3 -1
  129. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  130. package/dist/nextcloud-vue.cjs.js +23038 -14567
  131. package/dist/nextcloud-vue.cjs.js.map +2 -2
  132. package/dist/nextcloud-vue.css +951 -16
  133. package/l10n/en.json +207 -1
  134. package/l10n/nl.json +207 -1
  135. package/package.json +1 -1
  136. package/src/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue +416 -0
  137. package/src/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue +136 -0
  138. package/src/components/CnAiFeaturesWidget/__tests__/CnAiFeaturesWidget.spec.js +213 -0
  139. package/src/components/CnAiFeaturesWidget/index.js +14 -0
  140. package/src/components/CnIndexPage/CnIndexPage.vue +74 -3
  141. package/src/components/CnIndexPage/useNamedSource.js +9 -0
  142. package/src/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue +499 -0
  143. package/src/components/CnObjectPermissionsTab/__tests__/CnObjectPermissionsTab.spec.js +186 -0
  144. package/src/components/CnObjectPermissionsTab/index.js +13 -0
  145. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +8 -1
  146. package/src/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue +424 -0
  147. package/src/components/CnObjectTranslationsWidget/__tests__/CnObjectTranslationsWidget.spec.js +189 -0
  148. package/src/components/CnObjectTranslationsWidget/index.js +13 -0
  149. package/src/components/CnReadinessChecklist/CnReadinessChecklist.vue +335 -0
  150. package/src/components/CnReadinessChecklist/index.js +9 -0
  151. package/src/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue +300 -0
  152. package/src/components/CnSchemaRulesWidget/index.js +10 -0
  153. package/src/components/CnSchemaRulesWidget/schemaRules.js +123 -0
  154. package/src/components/CnSearchIndexStatus/CnSearchIndexStatus.vue +160 -0
  155. package/src/components/CnSearchIndexStatus/index.js +10 -0
  156. package/src/components/CnSearchIndexStatus/searchIndexStatus.js +56 -0
  157. package/src/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue +328 -0
  158. package/src/components/CnShippedConfigurationSection/index.js +9 -0
  159. package/src/components/CnStateMachineEditor/CnStateMachineEditor.vue +1332 -0
  160. package/src/components/CnStateMachineEditor/CnStateMachineFields.vue +791 -0
  161. package/src/components/CnStateMachineEditor/CnStateMachineNode.vue +442 -0
  162. package/src/components/CnStateMachineEditor/CnStateMachinePanel.vue +326 -0
  163. package/src/components/CnStateMachineEditor/index.js +8 -0
  164. package/src/components/CnStateMachineEditor/stateMachineFields.js +287 -0
  165. package/src/components/CnTimelineWidget/CnTimelineManage.vue +827 -0
  166. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +29 -3
  167. package/src/components/CnWidgetGrid/builtInWidgets.js +6 -0
  168. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +10 -0
  169. package/src/components/index.js +8 -0
  170. package/src/composables/indexSources.js +116 -16
  171. package/src/css/index-page.css +7 -0
  172. package/src/index.js +9 -0
  173. package/src/utils/aiFeatures.js +241 -0
  174. package/src/utils/aiSuggestionTracker.js +178 -0
  175. package/src/utils/libraryWidgetKeys.js +2 -0
  176. package/src/utils/objectPermissions.js +548 -0
  177. package/src/utils/objectTranslations.js +176 -0
  178. package/src/utils/stateMachineValidation.js +250 -0
  179. package/src/utils/widgetDispatch.js +3 -1
@@ -0,0 +1,1332 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div class="cn-state-machine-editor" :style="rootStyle">
7
+ <!-- Issues found by validate(), and refusals of an edit. Text says the
8
+ severity, so it does not rest on colour. -->
9
+ <div
10
+ v-if="issues.length > 0"
11
+ class="cn-state-machine-editor__issues"
12
+ :class="hasErrors ? 'cn-state-machine-editor__issues--error' : 'cn-state-machine-editor__issues--warning'"
13
+ role="alert"
14
+ data-testid="cn-state-machine-issues">
15
+ <ul class="cn-state-machine-editor__issue-list">
16
+ <li
17
+ v-for="(issue, index) in issues"
18
+ :key="index"
19
+ class="cn-state-machine-editor__issue"
20
+ :class="`cn-state-machine-editor__issue--${issue.type}`">
21
+ <AlertCircle v-if="issue.type === 'error'" :size="18" />
22
+ <Alert v-else :size="18" />
23
+ <span class="cn-state-machine-editor__sr">{{ issue.type === 'error' ? t('nextcloud-vue', 'Error:') : t('nextcloud-vue', 'Warning:') }}</span>
24
+ <span>{{ issue.message }}</span>
25
+ </li>
26
+ </ul>
27
+ <NcButton variant="tertiary" @click="issues = []">
28
+ {{ say('dismiss') }}
29
+ </NcButton>
30
+ </div>
31
+
32
+ <div class="cn-state-machine-editor__layout">
33
+ <!-- Palette: drag a state onto the canvas, or add one with the button. -->
34
+ <section
35
+ v-if="!readOnly"
36
+ class="cn-state-machine-editor__palette"
37
+ :aria-labelledby="`${uid}-palette`">
38
+ <h3 :id="`${uid}-palette`" class="cn-state-machine-editor__palette-title">
39
+ {{ say('paletteTitle') }}
40
+ </h3>
41
+ <div
42
+ class="cn-state-machine-editor__palette-item"
43
+ draggable="true"
44
+ data-testid="cn-state-machine-palette-state"
45
+ @dragstart="onPaletteDragStart">
46
+ <SquareOutline :size="18" />
47
+ <span>{{ say('paletteItem') }}</span>
48
+ </div>
49
+ <NcButton
50
+ variant="secondary"
51
+ wide
52
+ data-testid="cn-state-machine-add-state"
53
+ @click="addStateFromKeyboard">
54
+ <template #icon>
55
+ <Plus :size="20" />
56
+ </template>
57
+ {{ say('addState') }}
58
+ </NcButton>
59
+ <div class="cn-state-machine-editor__help">
60
+ <p v-for="(line, index) in paletteHelp" :key="index">
61
+ {{ line }}
62
+ </p>
63
+ </div>
64
+ <h3 class="cn-state-machine-editor__palette-title">
65
+ {{ say('controlsTitle') }}
66
+ </h3>
67
+ <dl class="cn-state-machine-editor__controls">
68
+ <template v-for="(control, index) in controlsHelp" :key="index">
69
+ <dt>{{ control.term }}</dt>
70
+ <dd>{{ control.text }}</dd>
71
+ </template>
72
+ </dl>
73
+ </section>
74
+
75
+ <section
76
+ class="cn-state-machine-editor__canvas"
77
+ :aria-label="say('canvasLabel')"
78
+ data-testid="cn-state-machine-canvas">
79
+ <CnGraphCanvas
80
+ :nodes="canvasNodes"
81
+ :edges="canvasEdges"
82
+ :readOnly="readOnly"
83
+ :snapToGrid="false"
84
+ :fitView="fitView"
85
+ @nodesChange="onNodesChange"
86
+ @edgesChange="onEdgesChange"
87
+ @connect="onConnect"
88
+ @nodeSelect="onNodeSelect"
89
+ @nodeActivate="selectState"
90
+ @nodeRemove="requestStateRemoval"
91
+ @nodeMenu="menuOpenFor = $event.id"
92
+ @edgeSelect="onEdgeSelect"
93
+ @edgeLabelClick="selectTransition"
94
+ @edgeLabelMove="onEdgeLabelMove"
95
+ @canvasClick="clearSelection"
96
+ @canvasDrop="onCanvasDrop">
97
+ <template #node="{ node }">
98
+ <CnStateMachineNode
99
+ :label="stateLabel(node.id)"
100
+ :final="isFinal(node.id)"
101
+ :steps="stepsOf(node.id)"
102
+ :showSteps="showSteps"
103
+ :stepKeys="stepKeys"
104
+ :selectedStepId="selection.kind === 'step' ? selection.id : null"
105
+ :connectTargets="connectTargetsOf(node.id)"
106
+ :outgoing="outgoingOf(node.id)"
107
+ :menuOpen="menuOpenFor === node.id"
108
+ :readOnly="readOnly"
109
+ :say="say"
110
+ @update:menuOpen="onMenuToggle(node.id, $event)"
111
+ @connect="connect(node.id, $event)"
112
+ @disconnect="removeTransition"
113
+ @editTransition="selectTransition"
114
+ @addStep="addStep(node.id)"
115
+ @deleteState="requestStateRemoval(node.id)"
116
+ @stepClick="selectStep"
117
+ @stepMove="moveStep(node.id, $event)" />
118
+ </template>
119
+ <template #edge-label="{ edge }">
120
+ <span class="cn-state-machine-editor__edge-label">{{ transitionText(edge.id) }}</span>
121
+ </template>
122
+ </CnGraphCanvas>
123
+ </section>
124
+
125
+ <aside
126
+ class="cn-state-machine-editor__properties"
127
+ :aria-label="say('propertiesLabel')"
128
+ data-testid="cn-state-machine-properties">
129
+ <CnStateMachinePanel
130
+ v-if="selectedStep"
131
+ :key="`step-${selectedStep.id}`"
132
+ :record="selectedStep"
133
+ :fields="stepFields"
134
+ :title="say('stepPanelTitle')"
135
+ :subtitle="stateLabel(selectedStep[keys.stepState])"
136
+ :closeLabel="say('closeStepPanel')"
137
+ :deleteLabel="say('deleteStep')"
138
+ :canDelete="!locked"
139
+ :locked="locked"
140
+ :readOnly="readOnly"
141
+ :optionSets="optionSets"
142
+ :translate="tr"
143
+ :uid="`${uid}-step`"
144
+ @update="updateStep"
145
+ @delete="removeStep(selectedStep.id)"
146
+ @close="clearSelection" />
147
+
148
+ <CnStateMachinePanel
149
+ v-else-if="selectedTransition"
150
+ :key="`transition-${selectedTransition.id}`"
151
+ :record="selectedTransition"
152
+ :fields="transitionFields"
153
+ :title="say('transitionPanelTitle')"
154
+ :subtitle="transitionRoute(selectedTransition)"
155
+ :closeLabel="say('closeTransitionPanel')"
156
+ :deleteLabel="say('deleteTransition')"
157
+ canDelete
158
+ :locked="locked"
159
+ :readOnly="readOnly"
160
+ :optionSets="optionSets"
161
+ :translate="tr"
162
+ :uid="`${uid}-transition`"
163
+ @update="updateTransition"
164
+ @delete="requestTransitionRemoval(selectedTransition.id)"
165
+ @close="clearSelection" />
166
+
167
+ <CnStateMachinePanel
168
+ v-else-if="selectedState"
169
+ :key="`state-${selectedState.id}`"
170
+ :record="selectedState"
171
+ :fields="stateFields"
172
+ :title="stateLabel(selectedState.id)"
173
+ :subtitle="say('statePanelTitle')"
174
+ :closeLabel="say('closeStatePanel')"
175
+ :deleteLabel="say('deleteState')"
176
+ canDelete
177
+ :locked="locked"
178
+ :readOnly="readOnly"
179
+ :optionSets="optionSets"
180
+ :translate="tr"
181
+ :uid="`${uid}-state`"
182
+ @update="updateState"
183
+ @delete="requestStateRemoval(selectedState.id)"
184
+ @close="clearSelection">
185
+ <NcButton v-if="showSteps && !readOnly" variant="secondary" @click="addStep(selectedState.id)">
186
+ <template #icon>
187
+ <Plus :size="20" />
188
+ </template>
189
+ {{ say('addStep') }}
190
+ </NcButton>
191
+ </CnStateMachinePanel>
192
+
193
+ <p v-else class="cn-state-machine-editor__empty">
194
+ {{ say('emptyPanel') }}
195
+ </p>
196
+ </aside>
197
+ </div>
198
+
199
+ <CnDeleteDialog
200
+ v-if="pendingRemoval"
201
+ ref="removeDialog"
202
+ :item="pendingRemoval"
203
+ nameField="name"
204
+ :dialogTitle="pendingRemoval.title"
205
+ :warningText="pendingRemoval.warning"
206
+ :irreversible="false"
207
+ @confirm="confirmRemoval"
208
+ @close="cancelRemoval" />
209
+ </div>
210
+ </template>
211
+
212
+ <script>
213
+ import { translatePlural as n, translate as t } from '@nextcloud/l10n'
214
+ import { NcButton } from '@nextcloud/vue'
215
+ import Alert from 'vue-material-design-icons/Alert.vue'
216
+ import AlertCircle from 'vue-material-design-icons/AlertCircle.vue'
217
+ import Plus from 'vue-material-design-icons/Plus.vue'
218
+ import SquareOutline from 'vue-material-design-icons/SquareOutline.vue'
219
+ import CnDeleteDialog from '../CnDeleteDialog/CnDeleteDialog.vue'
220
+ import CnGraphCanvas from '../CnGraphCanvas/CnGraphCanvas.vue'
221
+ import CnStateMachineNode from './CnStateMachineNode.vue'
222
+ import CnStateMachinePanel from './CnStateMachinePanel.vue'
223
+ import { validateStateMachine } from '../../utils/stateMachineValidation.js'
224
+
225
+ /** The drag type a palette state carries, so other drops are ignored. */
226
+ const PALETTE_TYPE = 'application/x-cn-state-machine-state'
227
+
228
+ /** Record keys by role. A host passes its own names through `config.keys`. */
229
+ export const DEFAULT_STATE_MACHINE_EDITOR_KEYS = Object.freeze({
230
+ stateLabel: 'label',
231
+ stateFinal: 'final',
232
+ transitionFrom: 'from',
233
+ transitionTo: 'to',
234
+ transitionLabel: 'label',
235
+ stepState: 'state',
236
+ stepTitle: 'title',
237
+ stepOrder: 'order',
238
+ stepRequired: 'required',
239
+ stepBadge: '',
240
+ })
241
+
242
+ let editorCount = 0
243
+
244
+ /**
245
+ * Fill `{name}` placeholders.
246
+ *
247
+ * @param {string} text The text.
248
+ * @param {object} [vars] Values by placeholder.
249
+ * @return {string} The filled text.
250
+ */
251
+ function fill(text, vars) {
252
+ if (!vars) {
253
+ return String(text ?? '')
254
+ }
255
+ return String(text ?? '').replace(/\{(\w+)\}/g, (match, name) => (Object.hasOwn(vars, name) ? String(vars[name]) : match))
256
+ }
257
+
258
+ /**
259
+ * An editor for a state machine: states drawn on a canvas, the transitions
260
+ * between them, the steps inside each state, and a property panel for whichever
261
+ * of those is selected. What a state, step or transition carries is described
262
+ * by field descriptors in `config`, so the component has no domain words of its
263
+ * own.
264
+ *
265
+ * The editor never changes the records it is given. Every edit is an event;
266
+ * the host stores the change and passes the records back down, the same rule
267
+ * CnGraphCanvas holds for positions.
268
+ *
269
+ * Every pointer gesture has a keyboard route: the palette's add button instead
270
+ * of a drag, a state's actions menu (or `c` on two states) instead of drawing a
271
+ * line, arrow keys instead of dragging a state, Alt with an arrow or the panel's
272
+ * move buttons instead of dragging a step or list item.
273
+ */
274
+ export default {
275
+ name: 'CnStateMachineEditor',
276
+
277
+ components: {
278
+ NcButton,
279
+ Alert,
280
+ AlertCircle,
281
+ Plus,
282
+ SquareOutline,
283
+ CnDeleteDialog,
284
+ CnGraphCanvas,
285
+ CnStateMachineNode,
286
+ CnStateMachinePanel,
287
+ },
288
+
289
+ inject: {
290
+ cnTranslate: { default: null },
291
+ },
292
+
293
+ props: {
294
+ /** The states. Each has an `id`; its name and final flag sit under `config.keys`. */
295
+ states: {
296
+ type: Array,
297
+ required: true,
298
+ },
299
+
300
+ /** The transitions, each `{ id }` plus a from and to state id under `config.keys`. */
301
+ transitions: {
302
+ type: Array,
303
+ default: () => [],
304
+ },
305
+
306
+ /** The steps inside states, each `{ id }` plus its state id under `config.keys.stepState`. */
307
+ steps: {
308
+ type: Array,
309
+ default: () => [],
310
+ },
311
+
312
+ /** Canvas positions by state id: `{ [id]: { x, y } }`. A state without one is placed on a grid. */
313
+ positions: {
314
+ type: Object,
315
+ default: () => ({}),
316
+ },
317
+
318
+ /**
319
+ * What the machine's records carry and what the editor says.
320
+ *
321
+ * - `keys`: record keys by role, see DEFAULT_STATE_MACHINE_EDITOR_KEYS.
322
+ * - `stateFields`, `stepFields`, `transitionFields`: field descriptors for
323
+ * each property panel (see the docs page for the descriptor types).
324
+ * - `steps`: false to leave steps out of the machine. On by default when
325
+ * `stepFields` is given.
326
+ * - `labels`: the editor's own texts by name, overriding the defaults.
327
+ * - `paletteHelp`, `controlsHelp`: the help under the palette.
328
+ * - `messages`: validation messages by rule code.
329
+ * - `requireFinal`: refuse to delete the last final state (default true).
330
+ * - `blockOnWarnings`: let warnings fail validate() too (default false).
331
+ * - `canvasHeight`: the canvas height in px (default 540).
332
+ *
333
+ * Texts are translated through `translate`, or the app's `cnTranslate`.
334
+ */
335
+ config: {
336
+ type: Object,
337
+ default: () => ({}),
338
+ },
339
+
340
+ /** Named option lists a select or checkboxes field points at with `optionSet`. */
341
+ optionSets: {
342
+ type: Object,
343
+ default: () => ({}),
344
+ },
345
+
346
+ /** Disable the panel fields marked `lockable`, and step deletion. */
347
+ locked: {
348
+ type: Boolean,
349
+ default: false,
350
+ },
351
+
352
+ /** Show the machine without any editing control. */
353
+ readOnly: {
354
+ type: Boolean,
355
+ default: false,
356
+ },
357
+
358
+ /** Frame the whole machine when the canvas first renders. */
359
+ fitView: {
360
+ type: Boolean,
361
+ default: true,
362
+ },
363
+
364
+ /** Translates configured texts. Falls back to the injected `cnTranslate`. */
365
+ translate: {
366
+ type: Function,
367
+ default: null,
368
+ },
369
+ },
370
+
371
+ emits: [
372
+ 'state-add',
373
+ 'state-move',
374
+ 'state-update',
375
+ 'state-remove',
376
+ 'transition-add',
377
+ 'transition-update',
378
+ 'transition-remove',
379
+ 'step-add',
380
+ 'step-update',
381
+ 'step-remove',
382
+ 'step-reorder',
383
+ 'validated',
384
+ ],
385
+
386
+ data() {
387
+ editorCount += 1
388
+ return {
389
+ uid: `cn-state-machine-${editorCount}`,
390
+ selection: { kind: null, id: null },
391
+ issues: [],
392
+ menuOpenFor: null,
393
+ pendingRemoval: null,
394
+ labelPositions: {},
395
+ syncTick: 0,
396
+ }
397
+ },
398
+
399
+ computed: {
400
+ /**
401
+ * @return {object} Record keys by role.
402
+ */
403
+ keys() {
404
+ return { ...DEFAULT_STATE_MACHINE_EDITOR_KEYS, ...(this.config.keys || {}) }
405
+ },
406
+
407
+ /**
408
+ * @return {object} The step keys the node reads.
409
+ */
410
+ stepKeys() {
411
+ return { title: this.keys.stepTitle, required: this.keys.stepRequired, badge: this.keys.stepBadge }
412
+ },
413
+
414
+ /**
415
+ * @return {Array<object>} The step panel's fields.
416
+ */
417
+ stepFields() {
418
+ return Array.isArray(this.config.stepFields) ? this.config.stepFields : []
419
+ },
420
+
421
+ /**
422
+ * @return {Array<object>} The transition panel's fields.
423
+ */
424
+ transitionFields() {
425
+ return Array.isArray(this.config.transitionFields) ? this.config.transitionFields : []
426
+ },
427
+
428
+ /**
429
+ * @return {Array<object>} The state panel's fields.
430
+ */
431
+ stateFields() {
432
+ return Array.isArray(this.config.stateFields) ? this.config.stateFields : []
433
+ },
434
+
435
+ /**
436
+ * @return {boolean} Whether states hold steps.
437
+ */
438
+ showSteps() {
439
+ if (this.config.steps === false) {
440
+ return false
441
+ }
442
+ return this.config.steps === true || this.stepFields.length > 0
443
+ },
444
+
445
+ /**
446
+ * @return {object} The root's style.
447
+ */
448
+ rootStyle() {
449
+ const height = Number(this.config.canvasHeight)
450
+ return { '--cn-state-machine-canvas-height': `${Number.isFinite(height) && height > 0 ? height : 540}px` }
451
+ },
452
+
453
+ /**
454
+ * @return {boolean} Whether any issue is an error.
455
+ */
456
+ hasErrors() {
457
+ return this.issues.some((issue) => issue.type === 'error')
458
+ },
459
+
460
+ /**
461
+ * @return {Array<string>} The palette's help lines.
462
+ */
463
+ paletteHelp() {
464
+ if (Array.isArray(this.config.paletteHelp)) {
465
+ return this.config.paletteHelp.map((line) => this.tr(line))
466
+ }
467
+ return [
468
+ t('nextcloud-vue', 'Drag a state onto the canvas to add it, or use the add button.'),
469
+ t('nextcloud-vue', 'Connect states by dragging from one port to another, by pressing c on one state and then on another, or through a state\'s actions menu.'),
470
+ t('nextcloud-vue', 'Click a state, a step or a transition to see its properties.'),
471
+ ]
472
+ },
473
+
474
+ /**
475
+ * @return {Array<{term: string, text: string}>} The canvas controls help.
476
+ */
477
+ controlsHelp() {
478
+ if (Array.isArray(this.config.controlsHelp)) {
479
+ return this.config.controlsHelp.map((control) => ({ term: this.tr(control.term), text: this.tr(control.text) }))
480
+ }
481
+ return [
482
+ { term: t('nextcloud-vue', 'Pan'), text: t('nextcloud-vue', 'Drag the empty canvas') },
483
+ { term: t('nextcloud-vue', 'Zoom'), text: t('nextcloud-vue', 'Scroll wheel or the zoom buttons') },
484
+ { term: t('nextcloud-vue', 'Move'), text: t('nextcloud-vue', 'Arrow keys on a focused state') },
485
+ ]
486
+ },
487
+
488
+ /**
489
+ * @return {Map<string, object>} States by id.
490
+ */
491
+ stateById() {
492
+ return new Map(this.states.map((state) => [state.id, state]))
493
+ },
494
+
495
+ /**
496
+ * @return {Array<object>} The canvas nodes.
497
+ */
498
+ canvasNodes() {
499
+ // `sync` changes when a refused removal must re-send the graph Vue Flow
500
+ // already dropped, so the nodes are rebuilt.
501
+ const sync = this.syncTick
502
+ return this.states.map((state, index) => ({
503
+ id: state.id,
504
+ type: 'default',
505
+ position: this.positionOf(state.id, index),
506
+ selected: this.selection.kind === 'state' && this.selection.id === state.id,
507
+ style: { width: '200px' },
508
+ data: { label: this.say('stateAria', { name: this.stateLabel(state.id) }), sync },
509
+ }))
510
+ },
511
+
512
+ /**
513
+ * @return {Array<object>} The canvas edges.
514
+ */
515
+ canvasEdges() {
516
+ const sync = this.syncTick
517
+ return this.transitions
518
+ .filter((transition) => this.stateById.has(this.fromOf(transition)) && this.stateById.has(this.toOf(transition)))
519
+ .map((transition) => ({
520
+ id: transition.id,
521
+ source: this.fromOf(transition),
522
+ target: this.toOf(transition),
523
+ selected: this.selection.kind === 'transition' && this.selection.id === transition.id,
524
+ markerEnd: { type: 'arrowclosed', width: 22, height: 22 },
525
+ data: {
526
+ animated: false,
527
+ sync,
528
+ labelT: this.labelPositions[transition.id],
529
+ labelAriaLabel: `${this.say('transitionAria', { from: this.stateLabel(this.fromOf(transition)), to: this.stateLabel(this.toOf(transition)) })}: ${this.transitionText(transition.id)}`,
530
+ },
531
+ }))
532
+ },
533
+
534
+ /**
535
+ * @return {object|null} The selected step.
536
+ */
537
+ selectedStep() {
538
+ return this.selection.kind === 'step' ? (this.steps.find((step) => step.id === this.selection.id) || null) : null
539
+ },
540
+
541
+ /**
542
+ * @return {object|null} The selected transition.
543
+ */
544
+ selectedTransition() {
545
+ return this.selection.kind === 'transition' ? (this.transitions.find((transition) => transition.id === this.selection.id) || null) : null
546
+ },
547
+
548
+ /**
549
+ * @return {object|null} The selected state.
550
+ */
551
+ selectedState() {
552
+ return this.selection.kind === 'state' ? (this.stateById.get(this.selection.id) || null) : null
553
+ },
554
+ },
555
+
556
+ methods: {
557
+ t,
558
+
559
+ /**
560
+ * Hand the panel's edited step to the host.
561
+ *
562
+ * @param {object} record The edited step, complete.
563
+ */
564
+ updateStep(record) {
565
+ /** @event step-update The edited step, complete, as the panel returns it. */
566
+ this.$emit('step-update', record)
567
+ },
568
+
569
+ /**
570
+ * Hand the panel's edited transition to the host.
571
+ *
572
+ * @param {object} record The edited transition, complete.
573
+ */
574
+ updateTransition(record) {
575
+ /** @event transition-update The edited transition, complete, as the panel returns it. */
576
+ this.$emit('transition-update', record)
577
+ },
578
+
579
+ /**
580
+ * Hand the panel's edited state to the host.
581
+ *
582
+ * @param {object} record The edited state, complete.
583
+ */
584
+ updateState(record) {
585
+ /** @event state-update The edited state, complete, as the panel returns it. */
586
+ this.$emit('state-update', record)
587
+ },
588
+
589
+ /**
590
+ * Translate a configured text.
591
+ *
592
+ * @param {string} text The text.
593
+ * @return {string} The translation, or the text.
594
+ */
595
+ tr(text) {
596
+ if (typeof text !== 'string' || text === '') {
597
+ return text || ''
598
+ }
599
+ const translate = this.translate || this.cnTranslate
600
+ const answer = typeof translate === 'function' ? translate(text) : text
601
+ return typeof answer === 'string' && answer !== '' ? answer : text
602
+ },
603
+
604
+ /**
605
+ * One of the editor's own texts: the config's, translated, or the default.
606
+ *
607
+ * @param {string} name The text's name.
608
+ * @param {object} [vars] Placeholder values.
609
+ * @return {string} The text.
610
+ */
611
+ say(name, vars) {
612
+ const own = this.config.labels && this.config.labels[name]
613
+ if (typeof own === 'string' && own !== '') {
614
+ return fill(this.tr(own), vars)
615
+ }
616
+ if (name === 'stepCount') {
617
+ return n('nextcloud-vue', '%n step', '%n steps', vars?.count ?? 0)
618
+ }
619
+ return fill(this.defaultText(name), vars)
620
+ },
621
+
622
+ /**
623
+ * @param {string} name A text's name.
624
+ * @return {string} Its default, in the library's domain.
625
+ */
626
+ defaultText(name) {
627
+ const texts = {
628
+ paletteTitle: t('nextcloud-vue', 'Elements'),
629
+ paletteItem: t('nextcloud-vue', 'State'),
630
+ addState: t('nextcloud-vue', 'Add state'),
631
+ controlsTitle: t('nextcloud-vue', 'Controls'),
632
+ canvasLabel: t('nextcloud-vue', 'States and transitions'),
633
+ propertiesLabel: t('nextcloud-vue', 'Properties'),
634
+ emptyPanel: t('nextcloud-vue', 'Select a state, a step or a transition to see its properties.'),
635
+ final: t('nextcloud-vue', 'Final'),
636
+ stateAria: t('nextcloud-vue', 'State: {name}'),
637
+ required: t('nextcloud-vue', 'required'),
638
+ stateActions: t('nextcloud-vue', 'Actions for state {name}'),
639
+ connectTo: t('nextcloud-vue', 'Connect to {name}'),
640
+ disconnectFrom: t('nextcloud-vue', 'Disconnect from {name}'),
641
+ editTransitionTo: t('nextcloud-vue', 'Edit transition to {name}'),
642
+ addStep: t('nextcloud-vue', 'Add step'),
643
+ deleteState: t('nextcloud-vue', 'Delete state'),
644
+ statePanelTitle: t('nextcloud-vue', 'State'),
645
+ closeStatePanel: t('nextcloud-vue', 'Close state properties'),
646
+ stepPanelTitle: t('nextcloud-vue', 'Step'),
647
+ closeStepPanel: t('nextcloud-vue', 'Close step properties'),
648
+ deleteStep: t('nextcloud-vue', 'Delete step'),
649
+ transitionPanelTitle: t('nextcloud-vue', 'Transition'),
650
+ closeTransitionPanel: t('nextcloud-vue', 'Close transition properties'),
651
+ deleteTransition: t('nextcloud-vue', 'Delete transition'),
652
+ deleteTransitionConfirm: t('nextcloud-vue', 'Delete the transition {name}?'),
653
+ deleteStateConfirm: t('nextcloud-vue', 'Delete state "{name}"? Its steps and transitions go with it.'),
654
+ needsFinal: t('nextcloud-vue', 'At least one state must stay final'),
655
+ deleteStateFailed: t('nextcloud-vue', 'The state could not be deleted'),
656
+ unnamedTransition: t('nextcloud-vue', 'Transition'),
657
+ transitionAria: t('nextcloud-vue', 'Transition from {from} to {to}'),
658
+ dismiss: t('nextcloud-vue', 'Dismiss'),
659
+ }
660
+ return texts[name] ?? name
661
+ },
662
+
663
+ /**
664
+ * @param {object} transition A transition.
665
+ * @return {string} Its source state id.
666
+ */
667
+ fromOf(transition) {
668
+ return transition[this.keys.transitionFrom]
669
+ },
670
+
671
+ /**
672
+ * @param {object} transition A transition.
673
+ * @return {string} Its target state id.
674
+ */
675
+ toOf(transition) {
676
+ return transition[this.keys.transitionTo]
677
+ },
678
+
679
+ /**
680
+ * @param {string} id A state id.
681
+ * @return {string} Its name, or the id.
682
+ */
683
+ stateLabel(id) {
684
+ const state = this.stateById.get(id)
685
+ return (state && state[this.keys.stateLabel]) || id || ''
686
+ },
687
+
688
+ /**
689
+ * @param {string} id A state id.
690
+ * @return {boolean} Whether it is final.
691
+ */
692
+ isFinal(id) {
693
+ const state = this.stateById.get(id)
694
+ return !!state && state[this.keys.stateFinal] === true
695
+ },
696
+
697
+ /**
698
+ * @param {string} id A transition id.
699
+ * @return {string} The text its label shows.
700
+ */
701
+ transitionText(id) {
702
+ const transition = this.transitions.find((candidate) => candidate.id === id)
703
+ const label = transition ? transition[this.keys.transitionLabel] : ''
704
+ return label || this.say('unnamedTransition')
705
+ },
706
+
707
+ /**
708
+ * @param {object} transition A transition.
709
+ * @return {string} `From → To`.
710
+ */
711
+ transitionRoute(transition) {
712
+ return `${this.stateLabel(this.fromOf(transition))} → ${this.stateLabel(this.toOf(transition))}`
713
+ },
714
+
715
+ /**
716
+ * @param {string} id A state id.
717
+ * @param {number} index Its position in `states`.
718
+ * @return {{x: number, y: number}} Its stored position, or its grid slot.
719
+ */
720
+ positionOf(id, index) {
721
+ const stored = this.positions && this.positions[id]
722
+ if (stored && Number.isFinite(stored.x) && Number.isFinite(stored.y)) {
723
+ return { x: stored.x, y: stored.y }
724
+ }
725
+ return this.gridSlot(index)
726
+ },
727
+
728
+ /**
729
+ * @param {number} index A slot number.
730
+ * @return {{x: number, y: number}} Four states to a row, 250 by 200 apart.
731
+ */
732
+ gridSlot(index) {
733
+ return { x: 100 + (index % 4) * 250, y: 100 + Math.floor(index / 4) * 200 }
734
+ },
735
+
736
+ /**
737
+ * @param {string} id A state id.
738
+ * @return {Array<object>} Its steps in order.
739
+ */
740
+ stepsOf(id) {
741
+ const order = this.keys.stepOrder
742
+ return this.steps
743
+ .filter((step) => step[this.keys.stepState] === id)
744
+ .sort((a, b) => (Number(a[order]) || 0) - (Number(b[order]) || 0))
745
+ },
746
+
747
+ /**
748
+ * @param {string} id A state id.
749
+ * @return {Array<{id: string, label: string}>} States it has no transition to yet.
750
+ */
751
+ connectTargetsOf(id) {
752
+ const connected = new Set(this.transitions.filter((transition) => this.fromOf(transition) === id).map(this.toOf))
753
+ return this.states
754
+ .filter((state) => state.id !== id && !connected.has(state.id))
755
+ .map((state) => ({ id: state.id, label: this.stateLabel(state.id) }))
756
+ },
757
+
758
+ /**
759
+ * @param {string} id A state id.
760
+ * @return {Array<{id: string, targetLabel: string}>} Its outgoing transitions.
761
+ */
762
+ outgoingOf(id) {
763
+ return this.transitions
764
+ .filter((transition) => this.fromOf(transition) === id)
765
+ .map((transition) => ({ id: transition.id, targetLabel: this.stateLabel(this.toOf(transition)) }))
766
+ },
767
+
768
+ /**
769
+ * @param {string} id A state id.
770
+ * @return {void}
771
+ */
772
+ selectState(id) {
773
+ this.selection = { kind: 'state', id }
774
+ },
775
+
776
+ /**
777
+ * @param {string} id A transition id.
778
+ * @return {void}
779
+ */
780
+ selectTransition(id) {
781
+ this.selection = { kind: 'transition', id }
782
+ },
783
+
784
+ /**
785
+ * @param {string} id A step id.
786
+ * @return {void}
787
+ */
788
+ selectStep(id) {
789
+ this.selection = { kind: 'step', id }
790
+ },
791
+
792
+ /**
793
+ * @return {void}
794
+ */
795
+ clearSelection() {
796
+ this.selection = { kind: null, id: null }
797
+ },
798
+
799
+ /**
800
+ * @param {object} event CnGraphCanvas's node event.
801
+ * @return {void}
802
+ */
803
+ onNodeSelect(event) {
804
+ if (event && event.node) {
805
+ this.selectState(event.node.id)
806
+ }
807
+ },
808
+
809
+ /**
810
+ * @param {object} event CnGraphCanvas's edge event.
811
+ * @return {void}
812
+ */
813
+ onEdgeSelect(event) {
814
+ if (event && event.edge) {
815
+ this.selectTransition(event.edge.id)
816
+ }
817
+ },
818
+
819
+ /**
820
+ * @param {string} id A state id.
821
+ * @param {boolean} open Whether its menu opened.
822
+ * @return {void}
823
+ */
824
+ onMenuToggle(id, open) {
825
+ if (open) {
826
+ this.menuOpenFor = id
827
+ } else if (this.menuOpenFor === id) {
828
+ this.menuOpenFor = null
829
+ }
830
+ },
831
+
832
+ /**
833
+ * Report moves; Vue Flow reports a drag once per frame and a keyboard
834
+ * move once per key.
835
+ *
836
+ * @param {Array<object>} changes Vue Flow's node changes.
837
+ * @return {void}
838
+ */
839
+ onNodesChange(changes) {
840
+ for (const change of changes || []) {
841
+ if (change.type === 'position' && change.position) {
842
+ /**
843
+ * @event state-move A state was moved: `{ id, x, y }`.
844
+ */
845
+ this.$emit('state-move', { id: change.id, x: Math.round(change.position.x), y: Math.round(change.position.y) })
846
+ } else if (change.type === 'remove') {
847
+ this.requestStateRemoval(change.id)
848
+ }
849
+ }
850
+ },
851
+
852
+ /**
853
+ * @param {Array<object>} changes Vue Flow's edge changes.
854
+ * @return {void}
855
+ */
856
+ onEdgesChange(changes) {
857
+ for (const change of changes || []) {
858
+ if (change.type === 'remove') {
859
+ this.requestTransitionRemoval(change.id)
860
+ }
861
+ }
862
+ },
863
+
864
+ /**
865
+ * @param {{source: string, target: string}} connection A new connection.
866
+ * @return {void}
867
+ */
868
+ onConnect(connection) {
869
+ if (connection && connection.source && connection.target) {
870
+ this.connect(connection.source, connection.target)
871
+ }
872
+ },
873
+
874
+ /**
875
+ * @param {string} from The source state.
876
+ * @param {string} to The target state.
877
+ * @return {void}
878
+ */
879
+ connect(from, to) {
880
+ if (this.readOnly || from === to) {
881
+ return
882
+ }
883
+ /**
884
+ * @event transition-add A transition should be added: `{ from, to }`.
885
+ */
886
+ this.$emit('transition-add', { from, to })
887
+ },
888
+
889
+ /**
890
+ * @param {{id: string, labelT: number}} payload The label and its place on the line.
891
+ * @return {void}
892
+ */
893
+ onEdgeLabelMove(payload) {
894
+ this.labelPositions = { ...this.labelPositions, [payload.id]: payload.labelT }
895
+ },
896
+
897
+ /**
898
+ * @param {DragEvent} event The palette drag.
899
+ * @return {void}
900
+ */
901
+ onPaletteDragStart(event) {
902
+ if (event.dataTransfer) {
903
+ event.dataTransfer.setData(PALETTE_TYPE, 'state')
904
+ event.dataTransfer.setData('text/plain', 'state')
905
+ event.dataTransfer.effectAllowed = 'copy'
906
+ }
907
+ },
908
+
909
+ /**
910
+ * @param {{position: object, event: DragEvent}} payload The drop.
911
+ * @return {void}
912
+ */
913
+ onCanvasDrop({ position, event }) {
914
+ const types = event && event.dataTransfer ? Array.from(event.dataTransfer.types || []) : []
915
+ if (!types.includes(PALETTE_TYPE)) {
916
+ return
917
+ }
918
+ this.addState({ x: Math.round(position.x), y: Math.round(position.y) }, false)
919
+ },
920
+
921
+ /**
922
+ * The keyboard route to a new state: the next free grid slot, and the
923
+ * new state selected so its panel opens.
924
+ *
925
+ * @return {void}
926
+ */
927
+ addStateFromKeyboard() {
928
+ this.addState(this.gridSlot(this.states.length), true)
929
+ },
930
+
931
+ /**
932
+ * @param {{x: number, y: number}} position Where the state goes.
933
+ * @param {boolean} select Whether to select it once added.
934
+ * @return {void}
935
+ */
936
+ addState(position, select) {
937
+ if (this.readOnly) {
938
+ return
939
+ }
940
+ /**
941
+ * @event state-add A state should be added at `position`. The host
942
+ * creates it and calls `done(id)`; the editor then selects it when
943
+ * it was added from the keyboard.
944
+ */
945
+ this.$emit('state-add', {
946
+ position,
947
+ done: (id) => {
948
+ if (select && id) {
949
+ this.selectState(id)
950
+ }
951
+ },
952
+ })
953
+ },
954
+
955
+ /**
956
+ * @param {string} stateId The state.
957
+ * @return {void}
958
+ */
959
+ addStep(stateId) {
960
+ if (this.readOnly) {
961
+ return
962
+ }
963
+ /**
964
+ * @event step-add A step should be added to a state:
965
+ * `{ state, done }`. The host calls `done(id)` and the editor opens
966
+ * the new step.
967
+ */
968
+ this.$emit('step-add', {
969
+ state: stateId,
970
+ done: (id) => {
971
+ if (id) {
972
+ this.selectStep(id)
973
+ }
974
+ },
975
+ })
976
+ },
977
+
978
+ /**
979
+ * @param {string} id A step.
980
+ * @return {void}
981
+ */
982
+ removeStep(id) {
983
+ if (this.selection.kind === 'step' && this.selection.id === id) {
984
+ this.clearSelection()
985
+ }
986
+ /**
987
+ * @event step-remove A step should be removed, by id.
988
+ */
989
+ this.$emit('step-remove', id)
990
+ },
991
+
992
+ /**
993
+ * @param {string} stateId The state the step is in.
994
+ * @param {{from: number, to: number}} move The step's old and new place.
995
+ * @return {void}
996
+ */
997
+ moveStep(stateId, { from, to }) {
998
+ const ids = this.stepsOf(stateId).map((step) => step.id)
999
+ if (from < 0 || to < 0 || from >= ids.length || to >= ids.length) {
1000
+ return
1001
+ }
1002
+ const [moved] = ids.splice(from, 1)
1003
+ ids.splice(to, 0, moved)
1004
+ /**
1005
+ * @event step-reorder The steps of a state in their new order:
1006
+ * `{ state, ids }`.
1007
+ */
1008
+ this.$emit('step-reorder', { state: stateId, ids })
1009
+ },
1010
+
1011
+ /**
1012
+ * @param {string} id A transition.
1013
+ * @return {void}
1014
+ */
1015
+ removeTransition(id) {
1016
+ if (this.selection.kind === 'transition' && this.selection.id === id) {
1017
+ this.clearSelection()
1018
+ }
1019
+ /**
1020
+ * @event transition-remove A transition should be removed, by id.
1021
+ */
1022
+ this.$emit('transition-remove', id)
1023
+ },
1024
+
1025
+ /**
1026
+ * Ask before removing a transition.
1027
+ *
1028
+ * @param {string} id A transition.
1029
+ * @return {void}
1030
+ */
1031
+ requestTransitionRemoval(id) {
1032
+ const transition = this.transitions.find((candidate) => candidate.id === id)
1033
+ if (!transition || this.readOnly || this.pendingRemoval) {
1034
+ this.syncTick += 1
1035
+ return
1036
+ }
1037
+ this.pendingRemoval = {
1038
+ kind: 'transition',
1039
+ id,
1040
+ name: this.transitionRoute(transition),
1041
+ title: this.say('deleteTransition'),
1042
+ warning: this.say('deleteTransitionConfirm', { name: this.transitionRoute(transition) }),
1043
+ }
1044
+ },
1045
+
1046
+ /**
1047
+ * Ask before removing a state, unless it is the last final one.
1048
+ *
1049
+ * @param {string} id A state.
1050
+ * @return {void}
1051
+ */
1052
+ requestStateRemoval(id) {
1053
+ const state = this.stateById.get(id)
1054
+ if (!state || this.readOnly || (this.pendingRemoval && this.pendingRemoval.id === id)) {
1055
+ this.syncTick += 1
1056
+ return
1057
+ }
1058
+ this.menuOpenFor = null
1059
+ if (this.config.requireFinal !== false && this.isFinal(id)
1060
+ && !this.states.some((other) => other.id !== id && this.isFinal(other.id))) {
1061
+ this.issues = [{ type: 'error', code: 'LAST_FINAL_STATE', message: this.say('needsFinal') }]
1062
+ this.syncTick += 1
1063
+ return
1064
+ }
1065
+ this.pendingRemoval = {
1066
+ kind: 'state',
1067
+ id,
1068
+ name: this.stateLabel(id),
1069
+ title: this.say('deleteState'),
1070
+ warning: this.say('deleteStateConfirm'),
1071
+ }
1072
+ },
1073
+
1074
+ /**
1075
+ * The removal was confirmed: hand it to the host.
1076
+ *
1077
+ * @return {void}
1078
+ */
1079
+ confirmRemoval() {
1080
+ const pending = this.pendingRemoval
1081
+ if (!pending) {
1082
+ return
1083
+ }
1084
+ if (pending.kind === 'transition') {
1085
+ this.removeTransition(pending.id)
1086
+ this.pendingRemoval = null
1087
+ return
1088
+ }
1089
+ /**
1090
+ * @event state-remove A state should be removed: `{ id, done }`. The
1091
+ * host removes the state with its steps and transitions and calls
1092
+ * `done(true)`, or `done(false)` when it could not.
1093
+ */
1094
+ this.$emit('state-remove', {
1095
+ id: pending.id,
1096
+ done: (ok) => {
1097
+ if (ok) {
1098
+ if (this.selection.id === pending.id) {
1099
+ this.clearSelection()
1100
+ }
1101
+ this.pendingRemoval = null
1102
+ return
1103
+ }
1104
+ if (this.$refs.removeDialog) {
1105
+ this.$refs.removeDialog.setResult({ error: this.say('deleteStateFailed') })
1106
+ }
1107
+ },
1108
+ })
1109
+ },
1110
+
1111
+ /**
1112
+ * The removal dialog closed without a removal.
1113
+ *
1114
+ * @return {void}
1115
+ */
1116
+ cancelRemoval() {
1117
+ this.pendingRemoval = null
1118
+ this.syncTick += 1
1119
+ },
1120
+
1121
+ /**
1122
+ * Check the machine and show what is wrong with it.
1123
+ *
1124
+ * @return {boolean} True when nothing blocks: no errors, and no warnings
1125
+ * either when `config.blockOnWarnings` is set.
1126
+ * @public
1127
+ */
1128
+ validate() {
1129
+ this.issues = validateStateMachine(
1130
+ { states: this.states, transitions: this.transitions },
1131
+ {
1132
+ keys: {
1133
+ label: this.keys.stateLabel,
1134
+ final: this.keys.stateFinal,
1135
+ from: this.keys.transitionFrom,
1136
+ to: this.keys.transitionTo,
1137
+ transitionLabel: this.keys.transitionLabel,
1138
+ },
1139
+ messages: this.config.messages,
1140
+ translate: (text) => this.tr(text),
1141
+ },
1142
+ )
1143
+ /**
1144
+ * @event validated The issues validate() found.
1145
+ */
1146
+ this.$emit('validated', this.issues)
1147
+ const blocking = this.config.blockOnWarnings === true
1148
+ ? this.issues
1149
+ : this.issues.filter((issue) => issue.type === 'error')
1150
+ return blocking.length === 0
1151
+ },
1152
+ },
1153
+ }
1154
+ </script>
1155
+
1156
+ <style scoped>
1157
+ .cn-state-machine-editor {
1158
+ display: flex;
1159
+ flex-direction: column;
1160
+ gap: 12px;
1161
+ min-width: 0;
1162
+ }
1163
+
1164
+ .cn-state-machine-editor__issues {
1165
+ display: flex;
1166
+ flex-wrap: wrap;
1167
+ align-items: flex-start;
1168
+ justify-content: space-between;
1169
+ gap: 8px;
1170
+ padding: 8px 12px;
1171
+ border: 1px solid var(--color-warning, #c28900);
1172
+ border-inline-start-width: 4px;
1173
+ border-radius: var(--border-radius-large, 10px);
1174
+ background: var(--color-main-background, #fff);
1175
+ }
1176
+
1177
+ .cn-state-machine-editor__issues--error {
1178
+ border-color: var(--color-error, #d91812);
1179
+ }
1180
+
1181
+ .cn-state-machine-editor__issue-list {
1182
+ display: flex;
1183
+ flex-direction: column;
1184
+ gap: 4px;
1185
+ margin: 0;
1186
+ padding: 0;
1187
+ list-style: none;
1188
+ }
1189
+
1190
+ .cn-state-machine-editor__issue {
1191
+ display: flex;
1192
+ align-items: center;
1193
+ gap: 6px;
1194
+ font-size: 14px;
1195
+ }
1196
+
1197
+ .cn-state-machine-editor__issue--error {
1198
+ color: var(--color-error-text, #c9302c);
1199
+ }
1200
+
1201
+ .cn-state-machine-editor__issue--warning {
1202
+ color: var(--color-warning-text, #7a5600);
1203
+ }
1204
+
1205
+ .cn-state-machine-editor__layout {
1206
+ display: grid;
1207
+ grid-template-columns: 220px minmax(0, 1fr) 280px;
1208
+ gap: 16px;
1209
+ align-items: start;
1210
+ }
1211
+
1212
+ .cn-state-machine-editor__palette {
1213
+ display: flex;
1214
+ flex-direction: column;
1215
+ gap: 10px;
1216
+ padding: 14px;
1217
+ border: 1px solid var(--color-border, #e4e6ea);
1218
+ border-radius: 12px;
1219
+ background: var(--color-background-hover, #f5f6f8);
1220
+ }
1221
+
1222
+ .cn-state-machine-editor__palette-title {
1223
+ margin: 0;
1224
+ font-size: 12px;
1225
+ font-weight: 600;
1226
+ letter-spacing: 0.06em;
1227
+ text-transform: uppercase;
1228
+ color: var(--color-text-maxcontrast, #5e6168);
1229
+ }
1230
+
1231
+ .cn-state-machine-editor__palette-item {
1232
+ display: flex;
1233
+ align-items: center;
1234
+ gap: 8px;
1235
+ padding: 8px 10px;
1236
+ border: 1px solid var(--color-border, #e4e6ea);
1237
+ border-radius: 8px;
1238
+ background: var(--color-main-background, #fff);
1239
+ font-size: 14px;
1240
+ cursor: grab;
1241
+ }
1242
+
1243
+ .cn-state-machine-editor__palette-item:hover {
1244
+ border-color: var(--color-primary-element, #0082c9);
1245
+ }
1246
+
1247
+ .cn-state-machine-editor__help {
1248
+ font-size: 13px;
1249
+ line-height: 1.5;
1250
+ color: var(--color-text-maxcontrast, #5e6168);
1251
+ }
1252
+
1253
+ .cn-state-machine-editor__help p {
1254
+ margin: 0 0 6px;
1255
+ }
1256
+
1257
+ .cn-state-machine-editor__controls {
1258
+ display: grid;
1259
+ grid-template-columns: max-content 1fr;
1260
+ gap: 4px 8px;
1261
+ margin: 0;
1262
+ font-size: 13px;
1263
+ color: var(--color-text-maxcontrast, #5e6168);
1264
+ }
1265
+
1266
+ .cn-state-machine-editor__controls dt {
1267
+ font-weight: 600;
1268
+ }
1269
+
1270
+ .cn-state-machine-editor__controls dd {
1271
+ margin: 0;
1272
+ }
1273
+
1274
+ .cn-state-machine-editor__canvas {
1275
+ position: relative;
1276
+ height: var(--cn-state-machine-canvas-height, 540px);
1277
+ overflow: hidden;
1278
+ border: 1px solid var(--color-border, #e4e6ea);
1279
+ border-radius: 12px;
1280
+ background: var(--color-main-background, #fff);
1281
+ }
1282
+
1283
+ .cn-state-machine-editor__properties {
1284
+ display: flex;
1285
+ flex-direction: column;
1286
+ gap: 12px;
1287
+ padding: 16px;
1288
+ border: 1px solid var(--color-border, #e4e6ea);
1289
+ border-radius: 12px;
1290
+ background: var(--color-main-background, #fff);
1291
+ }
1292
+
1293
+ .cn-state-machine-editor__empty {
1294
+ margin: 0;
1295
+ font-size: 14px;
1296
+ color: var(--color-text-maxcontrast, #5e6168);
1297
+ }
1298
+
1299
+ .cn-state-machine-editor__edge-label {
1300
+ font-size: 12px;
1301
+ font-weight: 500;
1302
+ }
1303
+
1304
+ .cn-state-machine-editor__sr {
1305
+ position: absolute;
1306
+ width: 1px;
1307
+ height: 1px;
1308
+ overflow: hidden;
1309
+ clip-path: inset(50%);
1310
+ white-space: nowrap;
1311
+ }
1312
+
1313
+ /* The board's state card: a 1px outline, a 10px radius and a soft shadow; a
1314
+ selected state gets the 2px primary outline. */
1315
+ .cn-state-machine-editor__canvas :deep(.cn-flow-node) {
1316
+ padding: 10px 12px;
1317
+ border: 1px solid var(--color-border-maxcontrast, #828a95);
1318
+ border-radius: 10px;
1319
+ box-shadow: 0 1px 2px rgba(27, 28, 29, 0.08);
1320
+ }
1321
+
1322
+ .cn-state-machine-editor__canvas :deep(.cn-flow-node--selected) {
1323
+ padding: 9px 11px;
1324
+ border: 2px solid var(--color-primary-element, #0082c9);
1325
+ }
1326
+
1327
+ @media (max-width: 1100px) {
1328
+ .cn-state-machine-editor__layout {
1329
+ grid-template-columns: minmax(0, 1fr);
1330
+ }
1331
+ }
1332
+ </style>