@masterteam/work-center 0.0.64 → 0.0.66

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.
@@ -1481,6 +1481,15 @@ class WorkCenterEscalationInstanceType {
1481
1481
  : isFinal
1482
1482
  ? this.transloco.translate('workCenter.preview.final')
1483
1483
  : null,
1484
+ // Semantic color for the badge — every step state renders with the
1485
+ // same pill and is told apart by color alone.
1486
+ badgeTone: isClosed
1487
+ ? 'closed'
1488
+ : isInitial
1489
+ ? 'initial'
1490
+ : isFinal
1491
+ ? 'final'
1492
+ : 'accent',
1484
1493
  style: 'detail',
1485
1494
  current: currentIds.has(String(step.id)),
1486
1495
  };
@@ -1493,6 +1502,7 @@ class WorkCenterEscalationInstanceType {
1493
1502
  color: 'color',
1494
1503
  subtitle: 'subtitle',
1495
1504
  badge: 'badge',
1505
+ badgeTone: 'badgeTone',
1496
1506
  status: 'status',
1497
1507
  style: 'style',
1498
1508
  current: 'current',
@@ -1742,6 +1752,156 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
1742
1752
  ], template: "<div\r\n class=\"mt-modal-content flex h-full min-h-0 flex-col gap-4 p-4 lg:overflow-hidden\"\r\n *transloco=\"let t; prefix: 'workCenter'\"\r\n>\r\n <div\r\n class=\"flex flex-1 min-h-0 flex-col gap-4 lg:grid lg:grid-cols-[13rem_minmax(0,1fr)] lg:gap-6\"\r\n >\r\n @if (tabOptions().length > 1) {\r\n <mt-tabs\r\n [(active)]=\"activeTab\"\r\n [options]=\"tabOptions()\"\r\n mode=\"vertical\"\r\n size=\"small\"\r\n />\r\n }\r\n\r\n <div class=\"min-w-0 flex-1 min-h-0 lg:overflow-hidden\">\r\n <div\r\n class=\"h-full min-h-0 lg:overflow-hidden\"\r\n [hidden]=\"activeTab() !== 'details'\"\r\n >\r\n <div\r\n class=\"min-w-0 h-full min-h-0 lg:overflow-y-auto lg:overscroll-contain lg:pr-2\"\r\n >\r\n @if (canRenderForm()) {\r\n <mt-client-form\r\n [moduleKey]=\"moduleKey()\"\r\n [operationKey]=\"operationKey()\"\r\n [moduleId]=\"formModuleId()\"\r\n [levelId]=\"formLevelId()\"\r\n [levelDataId]=\"formLevelDataId()\"\r\n [moduleDataId]=\"moduleDataId()\"\r\n [readonly]=\"true\"\r\n [lookups]=\"formLookups()\"\r\n />\r\n } @else {\r\n <div\r\n class=\"flex min-h-[22rem] items-center justify-center rounded-lg border border-dashed border-surface-300 bg-surface-50 p-6\"\r\n >\r\n <p class=\"max-w-md text-center text-sm text-surface-500\">\r\n {{ t(\"context.moduleFormUnavailable\") }}\r\n </p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n @if (!readOnly()) {\r\n <div\r\n class=\"h-full min-h-0 overflow-hidden\"\r\n [hidden]=\"activeTab() !== 'discussion'\"\r\n >\r\n @if (canRenderDiscussion()) {\r\n <div\r\n class=\"h-[32rem] min-h-[22rem] overflow-hidden rounded-lg border border-surface-200 bg-surface-50 lg:h-full lg:min-h-0\"\r\n >\r\n <mt-discussion-thread\r\n [moduleType]=\"discussionModuleType()\"\r\n [recordId]=\"resolvedInstanceId()\"\r\n [mentionSearchEndpoint]=\"'Identity/users'\"\r\n [mentionSearchParam]=\"'query'\"\r\n [mentionSearchDataPath]=\"'data'\"\r\n [uploadEndpoint]=\"'uploader'\"\r\n [attachmentDownloadEndpoint]=\"'uploader'\"\r\n [showParticipants]=\"true\"\r\n [autoMarkRead]=\"true\"\r\n [refreshIntervalMs]=\"0\"\r\n [styleClass]=\"'h-full'\"\r\n />\r\n </div>\r\n } @else {\r\n <div\r\n class=\"flex min-h-[22rem] items-center justify-center rounded-lg border border-dashed border-surface-300 bg-surface-50 p-6\"\r\n >\r\n <p class=\"max-w-md text-center text-sm text-surface-500\">\r\n {{ t(\"context.moduleDiscussionUnavailable\") }}\r\n </p>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</div>\r\n" }]
1743
1753
  }], ctorParameters: () => [], propDecorators: { details: [{ type: i0.Input, args: [{ isSignal: true, alias: "details", required: true }] }], lookups: [{ type: i0.Input, args: [{ isSignal: true, alias: "lookups", required: false }] }], readOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readOnly", required: false }] }], clientForm: [{ type: i0.ViewChild, args: [i0.forwardRef(() => ClientForm), { isSignal: true }] }] } });
1744
1754
 
1755
+ /**
1756
+ * Approval-history row projection for the process preview.
1757
+ *
1758
+ * The runtime stores one `step` record per *target user*, so a group step
1759
+ * fans out into one record per member, and every workflow attempt (return →
1760
+ * resubmit) creates a fresh fan-out for the same step schema. The table shows
1761
+ * one row per attempt: sibling member records collapse, but separate attempts
1762
+ * — and the completed history they carry — are preserved.
1763
+ */
1764
+ const PENDING = 'pending';
1765
+ const TERMINATED = 'terminated';
1766
+ /** Statuses whose actor is not the assigned group/role, so the actor wins. */
1767
+ const ACTOR_OVERRIDES_TARGET = new Set([TERMINATED]);
1768
+ function buildApprovalRows(steps, options) {
1769
+ const { resolveDisplayName } = options;
1770
+ const currentIds = new Set((options.currentStepIds ?? []).map((id) => String(id)));
1771
+ const rows = [];
1772
+ for (const run of splitIntoRuns(steps)) {
1773
+ const groupName = resolveGroupName(run[0].target, resolveDisplayName);
1774
+ if (groupName) {
1775
+ rows.push(buildGroupRow(run, groupName, currentIds, resolveDisplayName));
1776
+ continue;
1777
+ }
1778
+ // Non-group targets keep one row per record, minus the pending siblings
1779
+ // that were superseded when another member of the same fan-out acted.
1780
+ const runHasAction = run.some(hasAction);
1781
+ for (const step of run) {
1782
+ if (runHasAction &&
1783
+ run.length > 1 &&
1784
+ isSupersededPending(step, currentIds)) {
1785
+ continue;
1786
+ }
1787
+ rows.push({
1788
+ stepName: resolveStepName(step, resolveDisplayName),
1789
+ status: step.status,
1790
+ user: {
1791
+ kind: 'user',
1792
+ value: step.actionUserInfo ?? step.targetUser ?? null,
1793
+ },
1794
+ createdAt: step.createdAt ?? '',
1795
+ actionDate: step.actionDate ?? '',
1796
+ });
1797
+ }
1798
+ }
1799
+ return rows;
1800
+ }
1801
+ function buildGroupRow(run, groupName, currentIds, resolveDisplayName) {
1802
+ const pendingOnGroup = run.find((step) => isCurrent(step, currentIds));
1803
+ // While the attempt is open the row belongs to the group as a whole; once it
1804
+ // closes, the record that carries the outcome represents it.
1805
+ const repr = pendingOnGroup ?? resolveOutcome(run);
1806
+ const actor = repr.actionUserInfo;
1807
+ const showActor = !pendingOnGroup && !!actor && ACTOR_OVERRIDES_TARGET.has(statusKey(repr));
1808
+ return {
1809
+ stepName: resolveStepName(repr, resolveDisplayName),
1810
+ status: repr.status,
1811
+ user: showActor
1812
+ ? { kind: 'user', value: actor }
1813
+ : { kind: 'text', value: groupName },
1814
+ createdAt: run[0].createdAt ?? '',
1815
+ actionDate: pendingOnGroup ? '' : (repr.actionDate ?? ''),
1816
+ };
1817
+ }
1818
+ /**
1819
+ * Consecutive records sharing a step schema + target belong to the same
1820
+ * attempt. The API orders steps by creation, so a later attempt on the same
1821
+ * step is always separated by the records that sent the request back.
1822
+ */
1823
+ function splitIntoRuns(steps) {
1824
+ const runs = [];
1825
+ let currentKey = null;
1826
+ for (const step of steps) {
1827
+ const key = runKey(step);
1828
+ if (key !== currentKey || runs.length === 0) {
1829
+ runs.push([step]);
1830
+ currentKey = key;
1831
+ continue;
1832
+ }
1833
+ runs[runs.length - 1].push(step);
1834
+ }
1835
+ return runs;
1836
+ }
1837
+ function runKey(step) {
1838
+ return `${step.stepSchemaId ?? ''}|${step.target?.groupKey ?? ''}`;
1839
+ }
1840
+ /** The record holding the attempt's outcome (Returned/Rejected/Terminated…). */
1841
+ function resolveOutcome(run) {
1842
+ for (let index = run.length - 1; index >= 0; index -= 1) {
1843
+ const step = run[index];
1844
+ if (statusKey(step) !== PENDING && statusKey(step) !== '') {
1845
+ return step;
1846
+ }
1847
+ }
1848
+ return findLast(run, hasAction) ?? run[run.length - 1];
1849
+ }
1850
+ function isCurrent(step, currentIds) {
1851
+ if (typeof step.isCurrent === 'boolean') {
1852
+ return step.isCurrent;
1853
+ }
1854
+ if (currentIds.size > 0) {
1855
+ return currentIds.has(String(step.stepId ?? ''));
1856
+ }
1857
+ return step.isActive !== false && statusKey(step) === PENDING;
1858
+ }
1859
+ /** A pending record left behind after a sibling acted — never actionable. */
1860
+ function isSupersededPending(step, currentIds) {
1861
+ return (statusKey(step) === PENDING &&
1862
+ !hasAction(step) &&
1863
+ !isCurrent(step, currentIds));
1864
+ }
1865
+ function hasAction(step) {
1866
+ if (step.actionUserInfo) {
1867
+ return true;
1868
+ }
1869
+ const key = statusKey(step);
1870
+ return key !== '' && key !== PENDING;
1871
+ }
1872
+ /** Status comes off the lookup key, never the localized display text. */
1873
+ function statusKey(step) {
1874
+ const status = step.status;
1875
+ if (typeof status === 'string') {
1876
+ return status.trim().toLowerCase();
1877
+ }
1878
+ if (status && typeof status === 'object') {
1879
+ const key = status['key'];
1880
+ if (typeof key === 'string') {
1881
+ return key.trim().toLowerCase();
1882
+ }
1883
+ }
1884
+ return '';
1885
+ }
1886
+ function resolveStepName(step, resolveDisplayName) {
1887
+ return resolveDisplayName(step.stepName) || String(step.stepId ?? '--');
1888
+ }
1889
+ function resolveGroupName(target, resolveDisplayName) {
1890
+ if (!target || target.type !== 'Group' || !target.group) {
1891
+ return '';
1892
+ }
1893
+ return (resolveDisplayName(target.displayName) ||
1894
+ resolveDisplayName(target.group.name));
1895
+ }
1896
+ function findLast(items, predicate) {
1897
+ for (let index = items.length - 1; index >= 0; index -= 1) {
1898
+ if (predicate(items[index])) {
1899
+ return items[index];
1900
+ }
1901
+ }
1902
+ return undefined;
1903
+ }
1904
+
1745
1905
  class WorkCenterProcessPreview {
1746
1906
  http = inject(HttpClient);
1747
1907
  transloco = inject(TranslocoService);
@@ -1779,42 +1939,20 @@ class WorkCenterProcessPreview {
1779
1939
  preview = signal(null, ...(ngDevMode ? [{ debugName: "preview" }] : /* istanbul ignore next */ []));
1780
1940
  canRenderPreview = computed(() => (this.requestId() ?? 0) > 0, ...(ngDevMode ? [{ debugName: "canRenderPreview" }] : /* istanbul ignore next */ []));
1781
1941
  approvalRows = computed(() => {
1782
- const steps = this.preview()?.steps ?? [];
1783
- const rows = [];
1784
- const seenGroupKeys = new Set();
1785
- for (const step of steps) {
1786
- const groupName = this.resolveGroupName(step.target);
1787
- if (groupName) {
1788
- const dedupKey = `${step.stepSchemaId ?? ''}|${step.target?.groupKey ?? ''}`;
1789
- if (seenGroupKeys.has(dedupKey))
1790
- continue;
1791
- seenGroupKeys.add(dedupKey);
1792
- // Prefer a sibling that has been actioned so we can surface real
1793
- // action date/status; otherwise fall back to the current step.
1794
- const siblings = steps.filter((s) => (s.stepSchemaId ?? '') === (step.stepSchemaId ?? '') &&
1795
- (s.target?.groupKey ?? '') === (step.target?.groupKey ?? ''));
1796
- const repr = siblings.find((s) => s.actionUserInfo) ?? step;
1797
- rows.push({
1798
- stepName: this.resolveDisplayName(repr.stepName) ||
1799
- String(repr.stepId ?? '--'),
1800
- status: buildEntity('Status', 'Status', repr.status),
1801
- user: buildEntity('User', 'Text', groupName),
1802
- createdAt: buildEntity('Initiation Date', 'DateTime', repr.createdAt ?? ''),
1803
- actionDate: buildEntity('Action Date', 'DateTime', repr.actionDate ?? ''),
1804
- });
1805
- }
1806
- else {
1807
- rows.push({
1808
- stepName: this.resolveDisplayName(step.stepName) ||
1809
- String(step.stepId ?? '--'),
1810
- status: buildEntity('Status', 'Status', step.status),
1811
- user: buildEntity('User', 'User', (step.actionUserInfo ?? step.targetUser ?? null)),
1812
- createdAt: buildEntity('Initiation Date', 'DateTime', step.createdAt ?? ''),
1813
- actionDate: buildEntity('Action Date', 'DateTime', step.actionDate ?? ''),
1814
- });
1815
- }
1816
- }
1817
- return rows;
1942
+ const preview = this.preview();
1943
+ const rows = buildApprovalRows(preview?.steps ?? [], {
1944
+ currentStepIds: preview?.currentStepIds,
1945
+ resolveDisplayName: (value) => this.resolveDisplayName(value),
1946
+ });
1947
+ return rows.map((row) => ({
1948
+ stepName: row.stepName,
1949
+ status: buildEntity('Status', 'Status', row.status),
1950
+ user: row.user.kind === 'text'
1951
+ ? buildEntity('User', 'Text', row.user.value)
1952
+ : buildEntity('User', 'User', row.user.value),
1953
+ createdAt: buildEntity('Initiation Date', 'DateTime', row.createdAt),
1954
+ actionDate: buildEntity('Action Date', 'DateTime', row.actionDate),
1955
+ }));
1818
1956
  }, ...(ngDevMode ? [{ debugName: "approvalRows" }] : /* istanbul ignore next */ []));
1819
1957
  hasApprovals = computed(() => this.approvalRows().length > 0, ...(ngDevMode ? [{ debugName: "hasApprovals" }] : /* istanbul ignore next */ []));
1820
1958
  schemaNodes = computed(() => {
@@ -1869,6 +2007,9 @@ class WorkCenterProcessPreview {
1869
2007
  : isFinal
1870
2008
  ? this.transloco.translate('workCenter.preview.final')
1871
2009
  : null,
2010
+ // Semantic color for the badge — every step state renders with the
2011
+ // same pill and is told apart by color alone.
2012
+ badgeTone: step.isInitial ? 'initial' : isFinal ? 'final' : 'accent',
1872
2013
  status: step.status ?? null,
1873
2014
  style: isAppAction || isApprovalCommit ? 'icon' : 'detail',
1874
2015
  current: currentStepIds.has(String(step.id)),
@@ -1889,6 +2030,7 @@ class WorkCenterProcessPreview {
1889
2030
  color: 'color',
1890
2031
  subtitle: 'subtitle',
1891
2032
  badge: 'badge',
2033
+ badgeTone: 'badgeTone',
1892
2034
  status: 'status',
1893
2035
  style: 'style',
1894
2036
  current: 'current',
@@ -1909,13 +2051,6 @@ class WorkCenterProcessPreview {
1909
2051
  ngOnDestroy() {
1910
2052
  this.loadSub?.unsubscribe();
1911
2053
  }
1912
- resolveGroupName(target) {
1913
- if (!target || target.type !== 'Group' || !target.group) {
1914
- return '';
1915
- }
1916
- return (this.resolveDisplayName(target.displayName) ||
1917
- this.resolveDisplayName(target.group.name));
1918
- }
1919
2054
  resolveDisplayName(name) {
1920
2055
  if (typeof name === 'string') {
1921
2056
  return name;