@valtimo/process-management 13.45.1 → 13.46.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.
@@ -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 = {
@@ -1204,6 +1223,56 @@ const CustomRootElement = (props) => {
1204
1223
  </div>`;
1205
1224
  return wrapEntry(processLink ? genericLinkedPanel : genericCreatePanel);
1206
1225
  };
1226
+ const LengthLimitedIdElement = (props) => {
1227
+ const { element, translateService } = props;
1228
+ const modeling = useService('modeling');
1229
+ const debounce = useService('debounceInput');
1230
+ const translate = useService('translate');
1231
+ const getValue = () => getBusinessObject(element).id;
1232
+ const setValue = (value, error) => {
1233
+ if (error)
1234
+ return;
1235
+ modeling.updateProperties(element, { id: value });
1236
+ };
1237
+ const validate = (value) => {
1238
+ const businessObject = getBusinessObject(element);
1239
+ const assigned = businessObject.$model.ids.assigned(value);
1240
+ if (!value)
1241
+ return translate('ID must not be empty.');
1242
+ if (assigned && assigned !== businessObject)
1243
+ return translate('ID must be unique.');
1244
+ if (SPACE_REGEX.test(value))
1245
+ return translate('ID must not contain spaces.');
1246
+ if (!ID_REGEX.test(value)) {
1247
+ return QNAME_REGEX.test(value)
1248
+ ? translate('ID must not contain prefix.')
1249
+ : translate('ID must be a valid QName.');
1250
+ }
1251
+ if (value.length > MAX_ACTIVITY_ID_LENGTH) {
1252
+ return translateService.instant('processManagement.idTooLong', {
1253
+ max: MAX_ACTIVITY_ID_LENGTH,
1254
+ });
1255
+ }
1256
+ return undefined;
1257
+ };
1258
+ // The panel's text field has no maxLength prop, so cap the rendered input directly
1259
+ const capInputLength = (node) => {
1260
+ const input = node?.querySelector('input');
1261
+ if (input)
1262
+ input.maxLength = MAX_ACTIVITY_ID_LENGTH;
1263
+ };
1264
+ return html `<div ref=${capInputLength}>
1265
+ ${TextFieldEntry({
1266
+ element,
1267
+ id: 'id',
1268
+ label: translate('ID'),
1269
+ getValue,
1270
+ setValue,
1271
+ debounce,
1272
+ validate,
1273
+ })}
1274
+ </div>`;
1275
+ };
1207
1276
  const ValidationErrorsElement = (props) => {
1208
1277
  const getErrorMessage = (error) => {
1209
1278
  if (error.errorCode) {
@@ -1947,6 +2016,145 @@ const ExpressionAutocompleteModule = {
1947
2016
  * limitations under the License.
1948
2017
  */
1949
2018
 
2019
+ /*
2020
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
2021
+ *
2022
+ * Licensed under EUPL, Version 1.2 (the "License");
2023
+ * you may not use this file except in compliance with the License.
2024
+ * You may obtain a copy of the License at
2025
+ *
2026
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
2027
+ *
2028
+ * Unless required by applicable law or agreed to in writing, software
2029
+ * distributed under the License is distributed on an "AS IS" basis,
2030
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2031
+ * See the License for the specific language governing permissions and
2032
+ * limitations under the License.
2033
+ */
2034
+ const MAX_UNIQUE_SUFFIX = 100;
2035
+ const TRAILING_SEPARATOR_REGEX = /[-_]+$/;
2036
+ class AutoIdBehavior {
2037
+ static { this.$inject = ['eventBus', 'modeling', 'elementRegistry']; }
2038
+ constructor(eventBus, modeling, elementRegistry) {
2039
+ this.modeling = modeling;
2040
+ this.elementRegistry = elementRegistry;
2041
+ this._ownedElements = new WeakSet();
2042
+ this._applyingId = false;
2043
+ this._replacedElementWasOwned = false;
2044
+ eventBus.on('commandStack.shape.create.postExecuted', ({ context }) => {
2045
+ const element = context?.shape;
2046
+ if (this.isEligible(element))
2047
+ this._ownedElements.add(element);
2048
+ });
2049
+ // Changing the type swaps in a new element, and carries the id over as a property update
2050
+ eventBus.on('commandStack.shape.replace.preExecute', ({ context }) => {
2051
+ this._replacedElementWasOwned = this._ownedElements.has(context?.oldShape);
2052
+ });
2053
+ eventBus.on('commandStack.shape.replace.postExecuted', ({ context }) => {
2054
+ const { oldShape, newShape } = context ?? {};
2055
+ this._ownedElements.delete(oldShape);
2056
+ if (this._replacedElementWasOwned && this.isEligible(newShape)) {
2057
+ this._ownedElements.add(newShape);
2058
+ }
2059
+ else {
2060
+ this._ownedElements.delete(newShape);
2061
+ }
2062
+ this._replacedElementWasOwned = false;
2063
+ });
2064
+ // Read the previous id before the update is applied
2065
+ eventBus.on('commandStack.element.updateProperties.preExecute', ({ context }) => {
2066
+ if (this._applyingId)
2067
+ return;
2068
+ const newId = context?.properties?.id;
2069
+ if (newId && newId !== context.element?.id)
2070
+ this._ownedElements.delete(context.element);
2071
+ });
2072
+ eventBus.on('commandStack.element.updateLabel.postExecuted', ({ context }) => this.syncIdWithName(context));
2073
+ eventBus.on('commandStack.element.updateProperties.postExecuted', ({ context }) => {
2074
+ // Editing an unrelated property must never move the id
2075
+ if (context?.properties?.name === undefined)
2076
+ return;
2077
+ this.syncIdWithName(context);
2078
+ });
2079
+ }
2080
+ // A new process starts from a template, so treat what it brings as drawn here
2081
+ adoptAll() {
2082
+ this.elementRegistry.forEach((element) => {
2083
+ if (this.isEligible(element))
2084
+ this._ownedElements.add(element);
2085
+ });
2086
+ }
2087
+ syncIdWithName(context) {
2088
+ if (this._applyingId)
2089
+ return;
2090
+ const element = context?.element?.labelTarget ?? context?.element;
2091
+ if (!element || !this._ownedElements.has(element))
2092
+ return;
2093
+ const name = getBusinessObject(element)?.name;
2094
+ if (!name)
2095
+ return;
2096
+ const baseId = toKebabCase(name, MAX_ACTIVITY_ID_LENGTH);
2097
+ if (!baseId || baseId === element.id)
2098
+ return;
2099
+ const uniqueId = this.resolveUniqueId(baseId, element);
2100
+ if (!uniqueId || uniqueId === element.id)
2101
+ return;
2102
+ this._applyingId = true;
2103
+ try {
2104
+ this.modeling.updateProperties(element, { id: uniqueId });
2105
+ }
2106
+ finally {
2107
+ this._applyingId = false;
2108
+ }
2109
+ }
2110
+ isEligible(element) {
2111
+ return !!element && !element.labelTarget && is(element, 'bpmn:FlowNode');
2112
+ }
2113
+ isTaken(id, element) {
2114
+ const existingElement = this.elementRegistry.get(id);
2115
+ if (existingElement && existingElement !== element)
2116
+ return true;
2117
+ // Covers moddle elements that are not on the canvas, e.g. the process itself
2118
+ const businessObject = getBusinessObject(element);
2119
+ const assigned = businessObject?.$model?.ids?.assigned(id);
2120
+ return !!assigned && assigned !== businessObject;
2121
+ }
2122
+ resolveUniqueId(baseId, element) {
2123
+ if (!this.isTaken(baseId, element))
2124
+ return baseId;
2125
+ for (let counter = 2; counter <= MAX_UNIQUE_SUFFIX; counter++) {
2126
+ const suffix = `-${counter}`;
2127
+ const trimmedBase = baseId
2128
+ .slice(0, MAX_ACTIVITY_ID_LENGTH - suffix.length)
2129
+ .replace(TRAILING_SEPARATOR_REGEX, '');
2130
+ const candidate = `${trimmedBase}${suffix}`;
2131
+ if (!this.isTaken(candidate, element))
2132
+ return candidate;
2133
+ }
2134
+ return null;
2135
+ }
2136
+ }
2137
+ const AutoIdBehaviorModule = {
2138
+ __init__: ['autoIdBehavior'],
2139
+ autoIdBehavior: ['type', AutoIdBehavior],
2140
+ };
2141
+
2142
+ /*
2143
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
2144
+ *
2145
+ * Licensed under EUPL, Version 1.2 (the "License");
2146
+ * you may not use this file except in compliance with the License.
2147
+ * You may obtain a copy of the License at
2148
+ *
2149
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
2150
+ *
2151
+ * Unless required by applicable law or agreed to in writing, software
2152
+ * distributed under the License is distributed on an "AS IS" basis,
2153
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2154
+ * See the License for the specific language governing permissions and
2155
+ * limitations under the License.
2156
+ */
2157
+
1950
2158
  /*
1951
2159
  * Copyright 2015-2026 Ritense BV, the Netherlands.
1952
2160
  *
@@ -2863,6 +3071,7 @@ class ProcessManagementBuilderComponent {
2863
3071
  camundaPlatformBehaviors,
2864
3072
  ValtimoPropertiesProviderModule,
2865
3073
  ExpressionAutocompleteModule,
3074
+ AutoIdBehaviorModule,
2866
3075
  ],
2867
3076
  moddleExtensions: { camunda: CamundaBpmnModdle },
2868
3077
  propertiesPanel: { parent: this.modelerPanelElementRef.nativeElement },
@@ -3021,7 +3230,9 @@ class ProcessManagementBuilderComponent {
3021
3230
  if (this._selectedProcess$.getValue() !== 'create')
3022
3231
  return;
3023
3232
  this.creatingNewProcess$.next(true);
3024
- this._bpmnModeler?.importXML(EMPTY_BPMN);
3233
+ this._bpmnModeler
3234
+ ?.importXML(EMPTY_BPMN)
3235
+ .then(() => this._bpmnModeler?.get('autoIdBehavior')?.adoptAll());
3025
3236
  this.isReadOnlyProcess$.next(false);
3026
3237
  this.isSystemProcess$.next(false);
3027
3238
  this.loading$.next(false);