@valtimo/process-management 13.45.1 → 13.47.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.
@@ -15,7 +15,7 @@ import { LoadingModule, DropdownModule, SelectModule, ButtonModule, IconModule,
15
15
  import * as i1 from '@angular/common/http';
16
16
  import { HttpHeaders, HttpErrorResponse } from '@angular/common/http';
17
17
  import * as i2 from '@valtimo/shared';
18
- import { BaseApiService, InterceptorSkipHeader, InterceptorSkip, getCaseManagementRouteParams, getBuildingBlockManagementRouteParams, ROLE_ADMIN } from '@valtimo/shared';
18
+ import { BaseApiService, InterceptorSkipHeader, InterceptorSkip, toKebabCase, getCaseManagementRouteParams, getBuildingBlockManagementRouteParams, ROLE_ADMIN } from '@valtimo/shared';
19
19
  import { useService, BpmnPropertiesPanelModule, BpmnPropertiesProviderModule, CamundaPlatformPropertiesProviderModule } from 'bpmn-js-properties-panel';
20
20
  import Modeler from 'bpmn-js/lib/Modeler';
21
21
  import camundaPlatformBehaviors from 'camunda-bpmn-js-behaviors/lib/camunda-platform';
@@ -25,7 +25,8 @@ import { filter, BehaviorSubject, Subject, Subscription, combineLatest, switchMa
25
25
  import { distinctUntilChanged, map, filter as filter$1 } from 'rxjs/operators';
26
26
  import * as i2$1 from '@valtimo/form';
27
27
  import { toObservable } from '@angular/core/rxjs-interop';
28
- import { is } from 'bpmn-js/lib/util/ModelUtil';
28
+ import { is, getBusinessObject } from 'bpmn-js/lib/util/ModelUtil';
29
+ import { TextFieldEntry } from '@bpmn-io/properties-panel';
29
30
  import { html } from 'htm/preact';
30
31
  import { View16, ViewOff16, Upload16 } from '@carbon/icons';
31
32
  import * as i3 from 'ngx-logger';
@@ -66,6 +67,8 @@ const EMPTY_BPMN = `
66
67
  </bpmndi:BPMNDiagram>
67
68
  </bpmn:definitions>
68
69
  `;
70
+ // process_link.activity_id is varchar(64) — longer ids fail on save
71
+ const MAX_ACTIVITY_ID_LENGTH = 64;
69
72
 
70
73
  /*
71
74
  * Copyright 2015-2026 Ritense BV, the Netherlands.
@@ -273,7 +276,7 @@ class ProcessManagementEditorService {
273
276
  }
274
277
  updateProcessLink(event) {
275
278
  this.setProcessLinksForSelectedDefinition(this.processLinksForSelectedDefinition.map(processLink => {
276
- if (processLink.activityId === event.activityId) {
279
+ if (processLink.id === event.id) {
277
280
  return { ...processLink, ...event };
278
281
  }
279
282
  return processLink;
@@ -897,6 +900,21 @@ const clearBuildingBlockCalledElement = (editor, activityId) => {
897
900
  * See the License for the specific language governing permissions and
898
901
  * limitations under the License.
899
902
  */
903
+ // Mirrors the QName rules of bpmn-js-properties-panel, which does not expose them
904
+ const SPACE_REGEX = /\s/;
905
+ const QNAME_REGEX = /^([a-z][\w-.]*:)?[a-z_][\w-.]*$/i;
906
+ const ID_REGEX = /^[a-z_][\w-.]*$/i;
907
+ const PROCESS_LINKABLE_TYPES = [
908
+ 'bpmn:UserTask',
909
+ 'bpmn:StartEvent',
910
+ 'bpmn:ServiceTask',
911
+ 'bpmn:SendTask',
912
+ 'bpmn:ReceiveTask',
913
+ 'bpmn:IntermediateThrowEvent',
914
+ 'bpmn:IntermediateCatchEvent',
915
+ 'bpmn:CallActivity',
916
+ ];
917
+ const isProcessLinkable = (element) => PROCESS_LINKABLE_TYPES.some(type => is(element, type));
900
918
  class ValtimoPropertiesProvider {
901
919
  static { this.$inject = ['propertiesPanel', 'translate']; }
902
920
  get processManagementEditorService() {
@@ -932,6 +950,14 @@ class ValtimoPropertiesProvider {
932
950
  const generalGroup = groups.find((g) => g.id === 'general');
933
951
  if (generalGroup) {
934
952
  generalGroup.entries = generalGroup.entries.filter((entry) => entry.id !== 'isExecutable');
953
+ // Same scope as the auto-filled id, so typing and generating share one limit
954
+ if (is(element, 'bpmn:FlowNode')) {
955
+ const idEntry = generalGroup.entries.find((entry) => entry.id === 'id');
956
+ if (idEntry) {
957
+ idEntry.component = LengthLimitedIdElement;
958
+ idEntry.translateService = this.translateService;
959
+ }
960
+ }
935
961
  }
936
962
  if (elementErrors.length > 0) {
937
963
  const errorGroup = {
@@ -966,14 +992,7 @@ class ValtimoPropertiesProvider {
966
992
  targetGroup.shouldOpen = true;
967
993
  }
968
994
  }
969
- if (is(element, 'bpmn:UserTask') ||
970
- is(element, 'bpmn:StartEvent') ||
971
- is(element, 'bpmn:ServiceTask') ||
972
- is(element, 'bpmn:SendTask') ||
973
- is(element, 'bpmn:ReceiveTask') ||
974
- is(element, 'bpmn:IntermediateThrowEvent') ||
975
- is(element, 'bpmn:IntermediateCatchEvent') ||
976
- is(element, 'bpmn:CallActivity')) {
995
+ if (isProcessLinkable(element)) {
977
996
  const editingAllowed = this.processManagementEditorService.editingAllowed;
978
997
  if (editingAllowed || processLink) {
979
998
  const customGroup = {
@@ -1148,6 +1167,75 @@ const CustomRootElement = (props) => {
1148
1167
  ${linkedButtons}
1149
1168
  </div>`);
1150
1169
  }
1170
+ const externalActionKey = processLink?.actionKey;
1171
+ if (processLink?.processLinkType === 'external_plugin' && externalActionKey) {
1172
+ return html `<div class="process-link-properties-panel">
1173
+ <div class="process-link-properties-panel__header">
1174
+ <span class="process-link-properties-panel__title-container">
1175
+ <span class="process-link-properties-panel__title">${externalActionKey}</span>
1176
+ </span>
1177
+
1178
+ <cds-tag
1179
+ class="cds--tag cds--tag--purple cds--tag--md cds--layout--size-md cds-tag--no-margin"
1180
+ ><span class="cds--tag__label">
1181
+ ${translateService.instant('processLinkType.plugin')}
1182
+ </span>
1183
+ </cds-tag>
1184
+ </div>
1185
+
1186
+ <div class="process-link-properties-panel__buttons">
1187
+ <button
1188
+ class="cds--btn cds--btn--danger cds--btn--sm cds--layout--side-md"
1189
+ onClick=${handleUnlinkClick}
1190
+ >
1191
+ ${unlinkText}
1192
+ </button>
1193
+
1194
+ <button
1195
+ class="cds--btn cds--btn--primary cds--btn--sm cds--layout--size-md"
1196
+ onClick=${handleEditClick}
1197
+ >
1198
+ ${editProcessLinkText}
1199
+ </button>
1200
+ </div>
1201
+ </div>`;
1202
+ }
1203
+ if (processLink?.processLinkType === 'external_plugin_task_form') {
1204
+ // A task-form's name is its bundle key (the form identifier), mirroring how the external-plugin
1205
+ // action panel shows its action key; fall back to a generic label for a plugin's sole, unkeyed
1206
+ // task-form bundle.
1207
+ const taskFormName = processLink?.bundleKey || translateService.instant('processLink.pluginForm');
1208
+ return html `<div class="process-link-properties-panel">
1209
+ <div class="process-link-properties-panel__header">
1210
+ <span class="process-link-properties-panel__title-container">
1211
+ <span class="process-link-properties-panel__title">${taskFormName}</span>
1212
+ </span>
1213
+
1214
+ <cds-tag
1215
+ class="cds--tag cds--tag--purple cds--tag--md cds--layout--size-md cds-tag--no-margin"
1216
+ ><span class="cds--tag__label">
1217
+ ${translateService.instant('processLinkType.plugin')}
1218
+ </span>
1219
+ </cds-tag>
1220
+ </div>
1221
+
1222
+ <div class="process-link-properties-panel__buttons">
1223
+ <button
1224
+ class="cds--btn cds--btn--danger cds--btn--sm cds--layout--side-md"
1225
+ onClick=${handleUnlinkClick}
1226
+ >
1227
+ ${unlinkText}
1228
+ </button>
1229
+
1230
+ <button
1231
+ class="cds--btn cds--btn--primary cds--btn--sm cds--layout--size-md"
1232
+ onClick=${handleEditClick}
1233
+ >
1234
+ ${editProcessLinkText}
1235
+ </button>
1236
+ </div>
1237
+ </div>`;
1238
+ }
1151
1239
  const pluginActionKey = processLink?.pluginActionDefinitionKey;
1152
1240
  const pluginActionTranslation = pluginTranslationService.instantByPluginActionKey(pluginActionKey);
1153
1241
  const pluginTitleTranslation = pluginTranslationService.instantPluginTitleByPluginActionKey(pluginActionKey);
@@ -1204,6 +1292,56 @@ const CustomRootElement = (props) => {
1204
1292
  </div>`;
1205
1293
  return wrapEntry(processLink ? genericLinkedPanel : genericCreatePanel);
1206
1294
  };
1295
+ const LengthLimitedIdElement = (props) => {
1296
+ const { element, translateService } = props;
1297
+ const modeling = useService('modeling');
1298
+ const debounce = useService('debounceInput');
1299
+ const translate = useService('translate');
1300
+ const getValue = () => getBusinessObject(element).id;
1301
+ const setValue = (value, error) => {
1302
+ if (error)
1303
+ return;
1304
+ modeling.updateProperties(element, { id: value });
1305
+ };
1306
+ const validate = (value) => {
1307
+ const businessObject = getBusinessObject(element);
1308
+ const assigned = businessObject.$model.ids.assigned(value);
1309
+ if (!value)
1310
+ return translate('ID must not be empty.');
1311
+ if (assigned && assigned !== businessObject)
1312
+ return translate('ID must be unique.');
1313
+ if (SPACE_REGEX.test(value))
1314
+ return translate('ID must not contain spaces.');
1315
+ if (!ID_REGEX.test(value)) {
1316
+ return QNAME_REGEX.test(value)
1317
+ ? translate('ID must not contain prefix.')
1318
+ : translate('ID must be a valid QName.');
1319
+ }
1320
+ if (value.length > MAX_ACTIVITY_ID_LENGTH) {
1321
+ return translateService.instant('processManagement.idTooLong', {
1322
+ max: MAX_ACTIVITY_ID_LENGTH,
1323
+ });
1324
+ }
1325
+ return undefined;
1326
+ };
1327
+ // The panel's text field has no maxLength prop, so cap the rendered input directly
1328
+ const capInputLength = (node) => {
1329
+ const input = node?.querySelector('input');
1330
+ if (input)
1331
+ input.maxLength = MAX_ACTIVITY_ID_LENGTH;
1332
+ };
1333
+ return html `<div ref=${capInputLength}>
1334
+ ${TextFieldEntry({
1335
+ element,
1336
+ id: 'id',
1337
+ label: translate('ID'),
1338
+ getValue,
1339
+ setValue,
1340
+ debounce,
1341
+ validate,
1342
+ })}
1343
+ </div>`;
1344
+ };
1207
1345
  const ValidationErrorsElement = (props) => {
1208
1346
  const getErrorMessage = (error) => {
1209
1347
  if (error.errorCode) {
@@ -1947,6 +2085,145 @@ const ExpressionAutocompleteModule = {
1947
2085
  * limitations under the License.
1948
2086
  */
1949
2087
 
2088
+ /*
2089
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
2090
+ *
2091
+ * Licensed under EUPL, Version 1.2 (the "License");
2092
+ * you may not use this file except in compliance with the License.
2093
+ * You may obtain a copy of the License at
2094
+ *
2095
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
2096
+ *
2097
+ * Unless required by applicable law or agreed to in writing, software
2098
+ * distributed under the License is distributed on an "AS IS" basis,
2099
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2100
+ * See the License for the specific language governing permissions and
2101
+ * limitations under the License.
2102
+ */
2103
+ const MAX_UNIQUE_SUFFIX = 100;
2104
+ const TRAILING_SEPARATOR_REGEX = /[-_]+$/;
2105
+ class AutoIdBehavior {
2106
+ static { this.$inject = ['eventBus', 'modeling', 'elementRegistry']; }
2107
+ constructor(eventBus, modeling, elementRegistry) {
2108
+ this.modeling = modeling;
2109
+ this.elementRegistry = elementRegistry;
2110
+ this._ownedElements = new WeakSet();
2111
+ this._applyingId = false;
2112
+ this._replacedElementWasOwned = false;
2113
+ eventBus.on('commandStack.shape.create.postExecuted', ({ context }) => {
2114
+ const element = context?.shape;
2115
+ if (this.isEligible(element))
2116
+ this._ownedElements.add(element);
2117
+ });
2118
+ // Changing the type swaps in a new element, and carries the id over as a property update
2119
+ eventBus.on('commandStack.shape.replace.preExecute', ({ context }) => {
2120
+ this._replacedElementWasOwned = this._ownedElements.has(context?.oldShape);
2121
+ });
2122
+ eventBus.on('commandStack.shape.replace.postExecuted', ({ context }) => {
2123
+ const { oldShape, newShape } = context ?? {};
2124
+ this._ownedElements.delete(oldShape);
2125
+ if (this._replacedElementWasOwned && this.isEligible(newShape)) {
2126
+ this._ownedElements.add(newShape);
2127
+ }
2128
+ else {
2129
+ this._ownedElements.delete(newShape);
2130
+ }
2131
+ this._replacedElementWasOwned = false;
2132
+ });
2133
+ // Read the previous id before the update is applied
2134
+ eventBus.on('commandStack.element.updateProperties.preExecute', ({ context }) => {
2135
+ if (this._applyingId)
2136
+ return;
2137
+ const newId = context?.properties?.id;
2138
+ if (newId && newId !== context.element?.id)
2139
+ this._ownedElements.delete(context.element);
2140
+ });
2141
+ eventBus.on('commandStack.element.updateLabel.postExecuted', ({ context }) => this.syncIdWithName(context));
2142
+ eventBus.on('commandStack.element.updateProperties.postExecuted', ({ context }) => {
2143
+ // Editing an unrelated property must never move the id
2144
+ if (context?.properties?.name === undefined)
2145
+ return;
2146
+ this.syncIdWithName(context);
2147
+ });
2148
+ }
2149
+ // A new process starts from a template, so treat what it brings as drawn here
2150
+ adoptAll() {
2151
+ this.elementRegistry.forEach((element) => {
2152
+ if (this.isEligible(element))
2153
+ this._ownedElements.add(element);
2154
+ });
2155
+ }
2156
+ syncIdWithName(context) {
2157
+ if (this._applyingId)
2158
+ return;
2159
+ const element = context?.element?.labelTarget ?? context?.element;
2160
+ if (!element || !this._ownedElements.has(element))
2161
+ return;
2162
+ const name = getBusinessObject(element)?.name;
2163
+ if (!name)
2164
+ return;
2165
+ const baseId = toKebabCase(name, MAX_ACTIVITY_ID_LENGTH);
2166
+ if (!baseId || baseId === element.id)
2167
+ return;
2168
+ const uniqueId = this.resolveUniqueId(baseId, element);
2169
+ if (!uniqueId || uniqueId === element.id)
2170
+ return;
2171
+ this._applyingId = true;
2172
+ try {
2173
+ this.modeling.updateProperties(element, { id: uniqueId });
2174
+ }
2175
+ finally {
2176
+ this._applyingId = false;
2177
+ }
2178
+ }
2179
+ isEligible(element) {
2180
+ return !!element && !element.labelTarget && is(element, 'bpmn:FlowNode');
2181
+ }
2182
+ isTaken(id, element) {
2183
+ const existingElement = this.elementRegistry.get(id);
2184
+ if (existingElement && existingElement !== element)
2185
+ return true;
2186
+ // Covers moddle elements that are not on the canvas, e.g. the process itself
2187
+ const businessObject = getBusinessObject(element);
2188
+ const assigned = businessObject?.$model?.ids?.assigned(id);
2189
+ return !!assigned && assigned !== businessObject;
2190
+ }
2191
+ resolveUniqueId(baseId, element) {
2192
+ if (!this.isTaken(baseId, element))
2193
+ return baseId;
2194
+ for (let counter = 2; counter <= MAX_UNIQUE_SUFFIX; counter++) {
2195
+ const suffix = `-${counter}`;
2196
+ const trimmedBase = baseId
2197
+ .slice(0, MAX_ACTIVITY_ID_LENGTH - suffix.length)
2198
+ .replace(TRAILING_SEPARATOR_REGEX, '');
2199
+ const candidate = `${trimmedBase}${suffix}`;
2200
+ if (!this.isTaken(candidate, element))
2201
+ return candidate;
2202
+ }
2203
+ return null;
2204
+ }
2205
+ }
2206
+ const AutoIdBehaviorModule = {
2207
+ __init__: ['autoIdBehavior'],
2208
+ autoIdBehavior: ['type', AutoIdBehavior],
2209
+ };
2210
+
2211
+ /*
2212
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
2213
+ *
2214
+ * Licensed under EUPL, Version 1.2 (the "License");
2215
+ * you may not use this file except in compliance with the License.
2216
+ * You may obtain a copy of the License at
2217
+ *
2218
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
2219
+ *
2220
+ * Unless required by applicable law or agreed to in writing, software
2221
+ * distributed under the License is distributed on an "AS IS" basis,
2222
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2223
+ * See the License for the specific language governing permissions and
2224
+ * limitations under the License.
2225
+ */
2226
+
1950
2227
  /*
1951
2228
  * Copyright 2015-2026 Ritense BV, the Netherlands.
1952
2229
  *
@@ -2863,6 +3140,7 @@ class ProcessManagementBuilderComponent {
2863
3140
  camundaPlatformBehaviors,
2864
3141
  ValtimoPropertiesProviderModule,
2865
3142
  ExpressionAutocompleteModule,
3143
+ AutoIdBehaviorModule,
2866
3144
  ],
2867
3145
  moddleExtensions: { camunda: CamundaBpmnModdle },
2868
3146
  propertiesPanel: { parent: this.modelerPanelElementRef.nativeElement },
@@ -3021,7 +3299,9 @@ class ProcessManagementBuilderComponent {
3021
3299
  if (this._selectedProcess$.getValue() !== 'create')
3022
3300
  return;
3023
3301
  this.creatingNewProcess$.next(true);
3024
- this._bpmnModeler?.importXML(EMPTY_BPMN);
3302
+ this._bpmnModeler
3303
+ ?.importXML(EMPTY_BPMN)
3304
+ .then(() => this._bpmnModeler?.get('autoIdBehavior')?.adoptAll());
3025
3305
  this.isReadOnlyProcess$.next(false);
3026
3306
  this.isSystemProcess$.next(false);
3027
3307
  this.loading$.next(false);