@cyberismo/data-handler 0.0.7 → 0.0.9
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.
- package/dist/command-handler.d.ts +11 -2
- package/dist/command-handler.js +61 -18
- package/dist/command-handler.js.map +1 -1
- package/dist/command-manager.js +8 -8
- package/dist/command-manager.js.map +1 -1
- package/dist/commands/calculate.d.ts +7 -44
- package/dist/commands/calculate.js +8 -389
- package/dist/commands/calculate.js.map +1 -1
- package/dist/commands/create.d.ts +7 -4
- package/dist/commands/create.js +42 -15
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/edit.d.ts +9 -3
- package/dist/commands/edit.js +33 -9
- package/dist/commands/edit.js.map +1 -1
- package/dist/commands/export.d.ts +13 -11
- package/dist/commands/export.js +80 -28
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/import.d.ts +7 -0
- package/dist/commands/import.js +21 -2
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/move.d.ts +11 -12
- package/dist/commands/move.js +12 -13
- package/dist/commands/move.js.map +1 -1
- package/dist/commands/remove.d.ts +2 -4
- package/dist/commands/remove.js +8 -16
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/rename.d.ts +1 -3
- package/dist/commands/rename.js +3 -6
- package/dist/commands/rename.js.map +1 -1
- package/dist/commands/show.d.ts +37 -5
- package/dist/commands/show.js +85 -7
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/transition.d.ts +1 -3
- package/dist/commands/transition.js +3 -5
- package/dist/commands/transition.js.map +1 -1
- package/dist/commands/update.d.ts +5 -1
- package/dist/commands/update.js +7 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/validate.d.ts +7 -8
- package/dist/commands/validate.js +30 -35
- package/dist/commands/validate.js.map +1 -1
- package/dist/containers/card-container.d.ts +6 -0
- package/dist/containers/card-container.js +61 -0
- package/dist/containers/card-container.js.map +1 -1
- package/dist/containers/project/calculation-engine.d.ts +90 -0
- package/dist/containers/project/calculation-engine.js +402 -0
- package/dist/containers/project/calculation-engine.js.map +1 -0
- package/dist/containers/project/resource-collector.d.ts +2 -1
- package/dist/containers/project/resource-collector.js +41 -33
- package/dist/containers/project/resource-collector.js.map +1 -1
- package/dist/containers/project.d.ts +18 -6
- package/dist/containers/project.js +37 -72
- package/dist/containers/project.js.map +1 -1
- package/dist/containers/template.d.ts +5 -0
- package/dist/containers/template.js +9 -0
- package/dist/containers/template.js.map +1 -1
- package/dist/exceptions/index.d.ts +20 -0
- package/dist/exceptions/index.js +16 -0
- package/dist/exceptions/index.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/macros.d.ts +7 -2
- package/dist/interfaces/project-interfaces.d.ts +21 -0
- package/dist/interfaces/project-interfaces.js +4 -0
- package/dist/interfaces/project-interfaces.js.map +1 -1
- package/dist/interfaces/resource-interfaces.d.ts +1 -1
- package/dist/interfaces/resource-interfaces.js.map +1 -1
- package/dist/macros/base-macro.d.ts +2 -0
- package/dist/macros/base-macro.js +66 -19
- package/dist/macros/base-macro.js.map +1 -1
- package/dist/macros/common.d.ts +16 -9
- package/dist/macros/common.js +22 -9
- package/dist/macros/common.js.map +1 -1
- package/dist/macros/createCards/index.d.ts +1 -2
- package/dist/macros/createCards/index.js.map +1 -1
- package/dist/macros/graph/index.d.ts +1 -3
- package/dist/macros/graph/index.js +11 -9
- package/dist/macros/graph/index.js.map +1 -1
- package/dist/macros/image/index.d.ts +39 -0
- package/dist/macros/image/index.js +78 -0
- package/dist/macros/image/index.js.map +1 -0
- package/dist/macros/image/metadata.d.ts +18 -0
- package/dist/macros/image/metadata.js +22 -0
- package/dist/macros/image/metadata.js.map +1 -0
- package/dist/macros/include/index.d.ts +32 -0
- package/dist/macros/include/index.js +97 -0
- package/dist/macros/include/index.js.map +1 -0
- package/dist/macros/include/metadata.d.ts +15 -0
- package/dist/macros/include/metadata.js +19 -0
- package/dist/macros/include/metadata.js.map +1 -0
- package/dist/macros/index.d.ts +39 -31
- package/dist/macros/index.js +167 -73
- package/dist/macros/index.js.map +1 -1
- package/dist/macros/percentage/index.d.ts +29 -0
- package/dist/macros/percentage/index.js +36 -0
- package/dist/macros/percentage/index.js.map +1 -0
- package/dist/macros/percentage/metadata.d.ts +15 -0
- package/dist/macros/percentage/metadata.js +19 -0
- package/dist/macros/percentage/metadata.js.map +1 -0
- package/dist/macros/report/index.d.ts +2 -5
- package/dist/macros/report/index.js +20 -12
- package/dist/macros/report/index.js.map +1 -1
- package/dist/macros/scoreCard/index.d.ts +15 -15
- package/dist/macros/scoreCard/index.js +16 -17
- package/dist/macros/scoreCard/index.js.map +1 -1
- package/dist/macros/vega/index.d.ts +28 -0
- package/dist/macros/vega/index.js +27 -0
- package/dist/macros/vega/index.js.map +1 -0
- package/dist/macros/vega/metadata.d.ts +15 -0
- package/dist/macros/vega/metadata.js +7 -0
- package/dist/macros/vega/metadata.js.map +1 -0
- package/dist/macros/vegalite/index.d.ts +27 -0
- package/dist/macros/vegalite/index.js +27 -0
- package/dist/macros/vegalite/index.js.map +1 -0
- package/dist/macros/vegalite/metadata.d.ts +15 -0
- package/dist/macros/vegalite/metadata.js +7 -0
- package/dist/macros/vegalite/metadata.js.map +1 -0
- package/dist/macros/xref/index.d.ts +26 -0
- package/dist/macros/xref/index.js +53 -0
- package/dist/macros/xref/index.js.map +1 -0
- package/dist/macros/xref/metadata.d.ts +15 -0
- package/dist/macros/xref/metadata.js +19 -0
- package/dist/macros/xref/metadata.js.map +1 -0
- package/dist/module-manager.d.ts +17 -4
- package/dist/module-manager.js +192 -58
- package/dist/module-manager.js.map +1 -1
- package/dist/permissions/action-guard.d.ts +2 -2
- package/dist/permissions/action-guard.js +1 -1
- package/dist/permissions/action-guard.js.map +1 -1
- package/dist/project-settings.js +2 -8
- package/dist/project-settings.js.map +1 -1
- package/dist/resources/card-type-resource.d.ts +2 -0
- package/dist/resources/card-type-resource.js +63 -0
- package/dist/resources/card-type-resource.js.map +1 -1
- package/dist/resources/file-resource.d.ts +2 -0
- package/dist/resources/file-resource.js +12 -4
- package/dist/resources/file-resource.js.map +1 -1
- package/dist/resources/folder-resource.d.ts +19 -0
- package/dist/resources/folder-resource.js +51 -2
- package/dist/resources/folder-resource.js.map +1 -1
- package/dist/resources/graph-model-resource.js +1 -1
- package/dist/resources/graph-model-resource.js.map +1 -1
- package/dist/resources/graph-view-resource.js +1 -1
- package/dist/resources/graph-view-resource.js.map +1 -1
- package/dist/resources/report-resource.js +1 -1
- package/dist/resources/report-resource.js.map +1 -1
- package/dist/resources/resource-object.d.ts +8 -0
- package/dist/resources/resource-object.js +9 -0
- package/dist/resources/resource-object.js.map +1 -1
- package/dist/resources/template-resource.js +1 -1
- package/dist/resources/template-resource.js.map +1 -1
- package/dist/resources/workflow-resource.d.ts +2 -0
- package/dist/resources/workflow-resource.js +53 -9
- package/dist/resources/workflow-resource.js.map +1 -1
- package/dist/svg/index.d.ts +15 -0
- package/dist/svg/index.js +16 -0
- package/dist/svg/index.js.map +1 -0
- package/dist/svg/lib.d.ts +9 -0
- package/dist/svg/lib.js +26 -0
- package/dist/svg/lib.js.map +1 -0
- package/dist/svg/percentage.d.ts +25 -0
- package/dist/svg/percentage.js +90 -0
- package/dist/svg/percentage.js.map +1 -0
- package/dist/svg/scoreCard.d.ts +19 -0
- package/dist/svg/scoreCard.js +55 -0
- package/dist/svg/scoreCard.js.map +1 -0
- package/dist/types/queries.d.ts +8 -7
- package/dist/types/queries.js.map +1 -1
- package/dist/utils/card-utils.d.ts +6 -0
- package/dist/utils/card-utils.js +12 -0
- package/dist/utils/card-utils.js.map +1 -1
- package/dist/utils/clingo-facts.d.ts +2 -1
- package/dist/utils/clingo-facts.js +19 -2
- package/dist/utils/clingo-facts.js.map +1 -1
- package/dist/utils/clingo-parser.d.ts +1 -0
- package/dist/utils/clingo-parser.js +22 -100
- package/dist/utils/clingo-parser.js.map +1 -1
- package/dist/utils/constants.d.ts +7 -0
- package/dist/utils/constants.js +14 -0
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/csv.js.map +1 -1
- package/dist/utils/report.d.ts +17 -3
- package/dist/utils/report.js +38 -2
- package/dist/utils/report.js.map +1 -1
- package/dist/utils/resource-utils.d.ts +1 -0
- package/dist/utils/resource-utils.js +9 -0
- package/dist/utils/resource-utils.js.map +1 -1
- package/dist/utils/user-preferences.d.ts +1 -11
- package/dist/utils/user-preferences.js +30 -13
- package/dist/utils/user-preferences.js.map +1 -1
- package/dist/utils/validate.d.ts +2 -3
- package/dist/utils/validate.js +2 -2
- package/dist/utils/validate.js.map +1 -1
- package/package.json +8 -6
- package/src/command-handler.ts +96 -17
- package/src/command-manager.ts +8 -8
- package/src/commands/calculate.ts +11 -525
- package/src/commands/create.ts +53 -16
- package/src/commands/edit.ts +53 -11
- package/src/commands/export.ts +108 -34
- package/src/commands/import.ts +31 -2
- package/src/commands/move.ts +12 -15
- package/src/commands/remove.ts +10 -19
- package/src/commands/rename.ts +3 -12
- package/src/commands/show.ts +121 -8
- package/src/commands/transition.ts +3 -7
- package/src/commands/update.ts +6 -0
- package/src/commands/validate.ts +39 -47
- package/src/containers/card-container.ts +74 -0
- package/src/containers/project/calculation-engine.ts +535 -0
- package/src/containers/project/resource-collector.ts +45 -26
- package/src/containers/project.ts +66 -84
- package/src/containers/template.ts +16 -0
- package/src/exceptions/index.ts +36 -0
- package/src/index.ts +13 -2
- package/src/interfaces/macros.ts +7 -1
- package/src/interfaces/project-interfaces.ts +27 -0
- package/src/interfaces/resource-interfaces.ts +1 -0
- package/src/macros/base-macro.ts +89 -25
- package/src/macros/common.ts +22 -9
- package/src/macros/createCards/index.ts +1 -2
- package/src/macros/graph/index.ts +17 -12
- package/src/macros/image/index.ts +121 -0
- package/src/macros/image/metadata.ts +25 -0
- package/src/macros/include/index.ts +147 -0
- package/src/macros/include/metadata.ts +22 -0
- package/src/macros/index.ts +179 -100
- package/src/macros/percentage/index.ts +54 -0
- package/src/macros/percentage/metadata.ts +22 -0
- package/src/macros/report/index.ts +22 -17
- package/src/macros/scoreCard/index.ts +23 -23
- package/src/macros/vega/index.ts +55 -0
- package/src/macros/vega/metadata.ts +21 -0
- package/src/macros/vegalite/index.ts +50 -0
- package/src/macros/vegalite/metadata.ts +21 -0
- package/src/macros/xref/index.ts +73 -0
- package/src/macros/xref/metadata.ts +22 -0
- package/src/module-manager.ts +241 -69
- package/src/permissions/action-guard.ts +3 -3
- package/src/project-settings.ts +2 -11
- package/src/resources/card-type-resource.ts +100 -0
- package/src/resources/file-resource.ts +16 -4
- package/src/resources/folder-resource.ts +59 -2
- package/src/resources/graph-model-resource.ts +1 -1
- package/src/resources/graph-view-resource.ts +1 -1
- package/src/resources/report-resource.ts +1 -1
- package/src/resources/resource-object.ts +14 -0
- package/src/resources/template-resource.ts +1 -1
- package/src/resources/workflow-resource.ts +68 -13
- package/src/svg/index.ts +15 -0
- package/src/svg/lib.ts +31 -0
- package/src/svg/percentage.ts +97 -0
- package/src/svg/scoreCard.ts +88 -0
- package/src/types/queries.ts +8 -7
- package/src/types/string-pixel-width.d.ts +23 -0
- package/src/utils/card-utils.ts +13 -0
- package/src/utils/clingo-facts.ts +65 -3
- package/src/utils/clingo-parser.ts +31 -144
- package/src/utils/constants.ts +16 -0
- package/src/utils/csv.ts +1 -1
- package/src/utils/report.ts +45 -4
- package/src/utils/resource-utils.ts +9 -0
- package/src/utils/user-preferences.ts +32 -14
- package/src/utils/validate.ts +3 -3
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { basename, join } from 'node:path';
|
|
15
|
-
import { mkdir, rename, rm } from 'node:fs/promises';
|
|
14
|
+
import { basename, dirname, join, normalize } from 'node:path';
|
|
15
|
+
import { mkdir, readdir, readFile, rename, rm } from 'node:fs/promises';
|
|
16
|
+
|
|
17
|
+
import { writeFileSafe } from '../utils/file-utils.js';
|
|
16
18
|
|
|
17
19
|
import type { ResourceFolderType } from '../interfaces/project-interfaces.js';
|
|
18
20
|
import {
|
|
@@ -27,6 +29,7 @@ import {
|
|
|
27
29
|
sortCards,
|
|
28
30
|
} from './file-resource.js';
|
|
29
31
|
import type { ResourceContent } from '../interfaces/resource-interfaces.js';
|
|
32
|
+
import { VALID_FOLDER_RESOURCE_FILES } from '../utils/constants.js';
|
|
30
33
|
|
|
31
34
|
export {
|
|
32
35
|
type Card,
|
|
@@ -73,6 +76,15 @@ export class FolderResource extends FileResource {
|
|
|
73
76
|
await rm(this.internalFolder, { recursive: true, force: true });
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
// Get (resource folder) type name
|
|
80
|
+
protected get getType() {
|
|
81
|
+
return super.getType;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
protected get logger() {
|
|
85
|
+
return super.getLogger(this.getType);
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
protected initialize(): void {
|
|
77
89
|
super.initialize();
|
|
78
90
|
|
|
@@ -98,6 +110,51 @@ export class FolderResource extends FileResource {
|
|
|
98
110
|
return super.show();
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Shows the content of a file in the resource.
|
|
115
|
+
* @param fileName Name of the file to show.
|
|
116
|
+
* @returns the content of the file.
|
|
117
|
+
*/
|
|
118
|
+
public async showFile(fileName: string): Promise<string> {
|
|
119
|
+
const filePath = join(this.internalFolder, fileName);
|
|
120
|
+
return readFile(filePath, 'utf8');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Shows all file names in the resource.
|
|
125
|
+
* @returns all file names in the resource.
|
|
126
|
+
*/
|
|
127
|
+
public async showFileNames(): Promise<string[]> {
|
|
128
|
+
const files = await readdir(this.internalFolder);
|
|
129
|
+
return files.filter((file) => VALID_FOLDER_RESOURCE_FILES.includes(file));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Updates a file in the resource.
|
|
134
|
+
* @param fileName The name of the file to update.
|
|
135
|
+
* @param changedContent The new content for the file.
|
|
136
|
+
*/
|
|
137
|
+
public async updateFile(fileName: string, changedContent: string) {
|
|
138
|
+
const filePath = join(this.internalFolder, fileName);
|
|
139
|
+
|
|
140
|
+
// Do not allow updating file in other directories
|
|
141
|
+
const normalizedFilePath = normalize(filePath);
|
|
142
|
+
const normalizedInternalFilePath = normalize(this.internalFolder);
|
|
143
|
+
if (dirname(normalizedFilePath) !== normalizedInternalFilePath) {
|
|
144
|
+
throw new Error(`File '${fileName}' is not in the resource`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// This makes sure that the file is in the resource folder.
|
|
148
|
+
if (basename(normalizedFilePath) !== fileName) {
|
|
149
|
+
throw new Error(`File '${fileName}' is not in the resource`);
|
|
150
|
+
}
|
|
151
|
+
// check if the file is whitelisted
|
|
152
|
+
if (!VALID_FOLDER_RESOURCE_FILES.includes(fileName)) {
|
|
153
|
+
throw new Error(`File '${fileName}' is not whitelisted`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await writeFileSafe(filePath, changedContent, { flag: 'w' });
|
|
157
|
+
}
|
|
101
158
|
/**
|
|
102
159
|
* Updates resource.
|
|
103
160
|
* @param key Key to modify
|
|
@@ -150,7 +150,7 @@ export class GraphModelResource extends FolderResource {
|
|
|
150
150
|
|
|
151
151
|
await super.update(key, op);
|
|
152
152
|
|
|
153
|
-
const content =
|
|
153
|
+
const content = structuredClone(this.content) as GraphModel;
|
|
154
154
|
|
|
155
155
|
if (key === 'name') {
|
|
156
156
|
content.name = super.handleScalar(op) as string;
|
|
@@ -144,7 +144,7 @@ export class GraphViewResource extends FolderResource {
|
|
|
144
144
|
|
|
145
145
|
await super.update(key, op);
|
|
146
146
|
|
|
147
|
-
const content =
|
|
147
|
+
const content = structuredClone(this.content) as GraphView;
|
|
148
148
|
|
|
149
149
|
if (key === 'name') {
|
|
150
150
|
content.name = super.handleScalar(op) as string;
|
|
@@ -179,7 +179,7 @@ export class ReportResource extends FolderResource {
|
|
|
179
179
|
|
|
180
180
|
await super.update(key, op);
|
|
181
181
|
|
|
182
|
-
const content =
|
|
182
|
+
const content = structuredClone(this.content) as ReportMetadata;
|
|
183
183
|
|
|
184
184
|
if (key === 'name') {
|
|
185
185
|
content.name = super.handleScalar(op) as string;
|
|
@@ -21,9 +21,11 @@ import type {
|
|
|
21
21
|
Card,
|
|
22
22
|
ResourceFolderType,
|
|
23
23
|
} from '../interfaces/project-interfaces.js';
|
|
24
|
+
import type { Logger } from 'pino';
|
|
24
25
|
import { type Project, ResourcesFrom } from '../containers/project.js';
|
|
25
26
|
import type { ResourceContent } from '../interfaces/resource-interfaces.js';
|
|
26
27
|
import type { ResourceName } from '../utils/resource-utils.js';
|
|
28
|
+
import { getChildLogger } from '../utils/log-utils.js';
|
|
27
29
|
|
|
28
30
|
// Possible operations to perform when doing "update"
|
|
29
31
|
export type UpdateOperations = 'add' | 'change' | 'rank' | 'remove';
|
|
@@ -44,6 +46,7 @@ export type AddOperation<T> = BaseOperation<T> & {
|
|
|
44
46
|
export type ChangeOperation<T> = BaseOperation<T> & {
|
|
45
47
|
name: 'change';
|
|
46
48
|
to: T;
|
|
49
|
+
mappingTable?: { stateMapping: Record<string, string> }; // Optional state mapping for workflow changes
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
// Move item in an array to new position.
|
|
@@ -81,6 +84,9 @@ export abstract class AbstractResource {
|
|
|
81
84
|
protected abstract usage(cards?: Card[]): Promise<string[]>; // list of card keys or resource names where this resource is used in
|
|
82
85
|
protected abstract validate(content?: object): Promise<void>; // validate the content
|
|
83
86
|
protected abstract write(): Promise<void>; // write content to disk
|
|
87
|
+
// Abstract getters
|
|
88
|
+
protected abstract get getType(): string;
|
|
89
|
+
protected abstract getLogger(loggerName: string): Logger;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/**
|
|
@@ -110,6 +116,14 @@ export class ResourceObject extends AbstractResource {
|
|
|
110
116
|
protected async show(): Promise<ResourceContent> {
|
|
111
117
|
return {} as ResourceContent;
|
|
112
118
|
}
|
|
119
|
+
protected get getType(): string {
|
|
120
|
+
return this.type;
|
|
121
|
+
}
|
|
122
|
+
protected getLogger(loggerName: string): Logger {
|
|
123
|
+
return getChildLogger({
|
|
124
|
+
module: loggerName,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
113
127
|
protected async update<Type>(_key: string, _op: Operation<Type>) {}
|
|
114
128
|
protected async usage(_cards?: Card[]): Promise<string[]> {
|
|
115
129
|
return [];
|
|
@@ -146,7 +146,7 @@ export class TemplateResource extends FolderResource {
|
|
|
146
146
|
|
|
147
147
|
await super.update(key, op);
|
|
148
148
|
|
|
149
|
-
const content =
|
|
149
|
+
const content = structuredClone(this.content) as TemplateMetadata;
|
|
150
150
|
|
|
151
151
|
if (key === 'name') {
|
|
152
152
|
content.name = super.handleScalar(op) as string;
|
|
@@ -82,12 +82,8 @@ export class WorkflowResource extends FileResource {
|
|
|
82
82
|
|
|
83
83
|
// Handle change of workflow state.
|
|
84
84
|
private async handleStateChange(op: ChangeOperation<WorkflowState>) {
|
|
85
|
-
const content =
|
|
86
|
-
const stateName = (
|
|
87
|
-
(op.target as WorkflowState).name
|
|
88
|
-
? (op.target as WorkflowState).name
|
|
89
|
-
: op.target
|
|
90
|
-
) as string;
|
|
85
|
+
const content = structuredClone(this.content) as Workflow;
|
|
86
|
+
const stateName = this.targetName(op) as string;
|
|
91
87
|
// Check that state can be changed to
|
|
92
88
|
content.transitions = content.transitions.filter(
|
|
93
89
|
(t) => t.toState !== stateName,
|
|
@@ -111,12 +107,8 @@ export class WorkflowResource extends FileResource {
|
|
|
111
107
|
// Handle removal of workflow state.
|
|
112
108
|
// State can be removed with or without replacement.
|
|
113
109
|
private async handleStateRemoval(op: RemoveOperation<WorkflowState>) {
|
|
114
|
-
const content =
|
|
115
|
-
const stateName = (
|
|
116
|
-
(op.target as WorkflowState).name
|
|
117
|
-
? (op.target as WorkflowState).name
|
|
118
|
-
: op.target
|
|
119
|
-
) as string;
|
|
110
|
+
const content = structuredClone(this.content) as Workflow;
|
|
111
|
+
const stateName = this.targetName(op) as string;
|
|
120
112
|
|
|
121
113
|
// If there is no replacement value, remove all transitions "to" and "from" this state.
|
|
122
114
|
if (!op.replacementValue) {
|
|
@@ -154,6 +146,42 @@ export class WorkflowResource extends FileResource {
|
|
|
154
146
|
}
|
|
155
147
|
}
|
|
156
148
|
|
|
149
|
+
// Returns target name irregardless of the type
|
|
150
|
+
private targetName(op: Operation<WorkflowState | WorkflowTransition>) {
|
|
151
|
+
const name = op.target.name ? op.target.name : op.target;
|
|
152
|
+
return name;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Potentially updates the changed transition with current properties.
|
|
156
|
+
private async transitionObject(op: ChangeOperation<WorkflowTransition>) {
|
|
157
|
+
const content = structuredClone(this.content) as Workflow;
|
|
158
|
+
const targetTransitionName = this.targetName(op);
|
|
159
|
+
const currentTransition = content.transitions.filter(
|
|
160
|
+
(item) => item.name === targetTransitionName,
|
|
161
|
+
)[0];
|
|
162
|
+
|
|
163
|
+
if (currentTransition) {
|
|
164
|
+
op.to.fromState =
|
|
165
|
+
op.to.fromState.length === 0
|
|
166
|
+
? currentTransition.fromState
|
|
167
|
+
: op.to.fromState;
|
|
168
|
+
op.to.toState = op.to.toState ?? currentTransition.toState;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
op.to.name === undefined ||
|
|
173
|
+
op.to.toState === undefined ||
|
|
174
|
+
op.to.fromState == undefined ||
|
|
175
|
+
op.to.fromState.length === 0
|
|
176
|
+
) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Cannot change transition '${targetTransitionName}' for workflow '${this.content.name}'.
|
|
179
|
+
Updated transition must have 'name', 'toState' and 'fromState' properties.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return op.to;
|
|
183
|
+
}
|
|
184
|
+
|
|
157
185
|
// Check if operation is a string operation.
|
|
158
186
|
private isStringOperation(op: Operation<unknown>): op is Operation<string> {
|
|
159
187
|
return typeof op.target === 'string';
|
|
@@ -250,7 +278,7 @@ export class WorkflowResource extends FileResource {
|
|
|
250
278
|
|
|
251
279
|
await super.update(key, op);
|
|
252
280
|
|
|
253
|
-
const content =
|
|
281
|
+
const content = structuredClone(this.content) as Workflow;
|
|
254
282
|
|
|
255
283
|
if (key === 'name') {
|
|
256
284
|
content.name = super.handleScalar(op) as string;
|
|
@@ -270,6 +298,33 @@ export class WorkflowResource extends FileResource {
|
|
|
270
298
|
throw new Error(`Unknown property '${key}' for Workflow`);
|
|
271
299
|
}
|
|
272
300
|
|
|
301
|
+
// If workflow transition is removed, then above call to 'handleArray' is all that is needed.
|
|
302
|
+
|
|
303
|
+
if (key === 'transitions' && op.name === 'change') {
|
|
304
|
+
// If workflow transition is changed, update to full object and change the content.
|
|
305
|
+
let changeOp: ChangeOperation<WorkflowTransition>;
|
|
306
|
+
if (this.isStringOperation(op)) {
|
|
307
|
+
const targetTransition = (this.content as Workflow).transitions.find(
|
|
308
|
+
(transition) => transition.name === op.target,
|
|
309
|
+
)!;
|
|
310
|
+
changeOp = {
|
|
311
|
+
name: 'change',
|
|
312
|
+
target: targetTransition as WorkflowTransition,
|
|
313
|
+
to: {
|
|
314
|
+
name: op.to,
|
|
315
|
+
toState: targetTransition.toState,
|
|
316
|
+
fromState: targetTransition.fromState,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
} else {
|
|
320
|
+
changeOp = op as ChangeOperation<WorkflowTransition>;
|
|
321
|
+
}
|
|
322
|
+
const newTransition = await this.transitionObject(changeOp);
|
|
323
|
+
content.transitions = content.transitions.map((item) =>
|
|
324
|
+
item.name == newTransition.name ? newTransition : item,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
273
328
|
if (key === 'states' && op.name === 'remove') {
|
|
274
329
|
// If workflow state is removed, remove all transitions "to" and "from" this state.
|
|
275
330
|
let removeOp: RemoveOperation<WorkflowState>;
|
package/src/svg/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Cyberismo
|
|
3
|
+
Copyright © Cyberismo Ltd and contributors 2025
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify it under
|
|
6
|
+
the terms of the GNU Affero General Public License version 3 as published by
|
|
7
|
+
the Free Software Foundation. This program is distributed in the hope that it
|
|
8
|
+
will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
9
|
+
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
10
|
+
See the GNU Affero General Public License for more details.
|
|
11
|
+
You should have received a copy of the GNU Affero General Public
|
|
12
|
+
License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
13
|
+
*/
|
|
14
|
+
export * from './scoreCard.js';
|
|
15
|
+
export * from './percentage.js';
|
package/src/svg/lib.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Cyberismo
|
|
3
|
+
Copyright © Cyberismo Ltd and contributors 2025
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify it under
|
|
6
|
+
the terms of the GNU Affero General Public License version 3 as published by
|
|
7
|
+
the Free Software Foundation. This program is distributed in the hope that it
|
|
8
|
+
will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
9
|
+
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
10
|
+
See the GNU Affero General Public License for more details.
|
|
11
|
+
You should have received a copy of the GNU Affero General Public
|
|
12
|
+
License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
13
|
+
*/
|
|
14
|
+
import pixelWidth from 'string-pixel-width';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Measures the width of a text string
|
|
18
|
+
* @param text - The text to measure
|
|
19
|
+
* @param font - The font to use
|
|
20
|
+
* @param size - The size of the font
|
|
21
|
+
* @param bold - Whether the text is bold
|
|
22
|
+
* @returns The width of the text
|
|
23
|
+
*/
|
|
24
|
+
export function measureTextWidth(
|
|
25
|
+
text: string,
|
|
26
|
+
font: string,
|
|
27
|
+
size: number,
|
|
28
|
+
bold = false,
|
|
29
|
+
): number {
|
|
30
|
+
return pixelWidth(text, { font, size, bold });
|
|
31
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Cyberismo
|
|
3
|
+
Copyright © Cyberismo Ltd and contributors 2025
|
|
4
|
+
This program is free software: you can redistribute it and/or modify it under
|
|
5
|
+
the terms of the GNU Affero General Public License version 3 as published by
|
|
6
|
+
the Free Software Foundation. This program is distributed in the hope that it
|
|
7
|
+
will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
8
|
+
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
9
|
+
See the GNU Affero General Public License for more details.
|
|
10
|
+
You should have received a copy of the GNU Affero General Public
|
|
11
|
+
License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface PercentageOptions {
|
|
15
|
+
title: string;
|
|
16
|
+
value: number;
|
|
17
|
+
legend: string;
|
|
18
|
+
colour?: 'blue' | 'green' | 'yellow' | 'red' | 'orange' | 'purple';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SIZE = 160;
|
|
22
|
+
const STROKE = 18;
|
|
23
|
+
const R = (SIZE - STROKE) / 2;
|
|
24
|
+
const CIRC = 2 * Math.PI * R;
|
|
25
|
+
const TITLE_HEIGHT = 70; // Space for the title above the donut
|
|
26
|
+
const EXTRA_WIDTH = 80; // Extra width for the title area
|
|
27
|
+
const SVG_WIDTH = SIZE + EXTRA_WIDTH;
|
|
28
|
+
// SVG_HEIGHT will be calculated dynamically below
|
|
29
|
+
const TITLE_FONT_SIZE = 22;
|
|
30
|
+
const VALUE_FONT_SIZE = 32;
|
|
31
|
+
const LEGEND_FONT_SIZE = 18;
|
|
32
|
+
const DONUT_COLOR_RED = '#b22217';
|
|
33
|
+
const TITLE_Y = 36;
|
|
34
|
+
const LINE_SPACING = 1.2;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Splits a string into lines of up to maxLen characters, breaking at spaces.
|
|
38
|
+
*/
|
|
39
|
+
function wrapText(text: string, maxLen: number): string[] {
|
|
40
|
+
const words = text.split(' ');
|
|
41
|
+
const lines: string[] = [];
|
|
42
|
+
let current = '';
|
|
43
|
+
for (const word of words) {
|
|
44
|
+
if ((current + (current ? ' ' : '') + word).length > maxLen) {
|
|
45
|
+
if (current) lines.push(current);
|
|
46
|
+
current = word;
|
|
47
|
+
} else {
|
|
48
|
+
current += (current ? ' ' : '') + word;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (current) lines.push(current);
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates a percentage widget as an SVG
|
|
57
|
+
* @param options - The options for the percentage
|
|
58
|
+
* @returns The widget as an SVG
|
|
59
|
+
*/
|
|
60
|
+
export function percentage(options: PercentageOptions): string {
|
|
61
|
+
const { title, value, legend, colour = 'blue' } = options;
|
|
62
|
+
const offset = CIRC * (1 - value / 100);
|
|
63
|
+
const DONUT_COLOR = colour === 'red' ? DONUT_COLOR_RED : colour;
|
|
64
|
+
const titleLines = wrapText(title, 24);
|
|
65
|
+
const donutYOffset =
|
|
66
|
+
TITLE_Y +
|
|
67
|
+
(titleLines.length - 1) * TITLE_FONT_SIZE * LINE_SPACING +
|
|
68
|
+
TITLE_FONT_SIZE +
|
|
69
|
+
24;
|
|
70
|
+
const donutCenterY = donutYOffset + SIZE / 2 - TITLE_HEIGHT / 2;
|
|
71
|
+
const dynamicSVGHeight = donutYOffset + SIZE / 2 + R + 20;
|
|
72
|
+
return `
|
|
73
|
+
<svg width="${SVG_WIDTH}" height="${dynamicSVGHeight}" viewBox="0 0 ${SVG_WIDTH} ${dynamicSVGHeight}" xmlns="http://www.w3.org/2000/svg">
|
|
74
|
+
<title>${title}</title>
|
|
75
|
+
|
|
76
|
+
<!-- Visible Title (wrapped) -->
|
|
77
|
+
<text x="${SVG_WIDTH / 2}" y="${TITLE_Y}" text-anchor="middle" font-size="${TITLE_FONT_SIZE}" font-weight="bold">
|
|
78
|
+
${titleLines.map((line, i) => `<tspan x='${SVG_WIDTH / 2}' dy='${i === 0 ? 0 : LINE_SPACING}em'>${line}</tspan>`).join('')}
|
|
79
|
+
</text>
|
|
80
|
+
|
|
81
|
+
<!-- Background track -->
|
|
82
|
+
<circle cx="${SVG_WIDTH / 2}" cy="${donutCenterY}" r="${R}"
|
|
83
|
+
fill="none" stroke="#eee" stroke-width="${STROKE}" />
|
|
84
|
+
|
|
85
|
+
<!-- Progress arc -->
|
|
86
|
+
<circle cx="${SVG_WIDTH / 2}" cy="${donutCenterY}" r="${R}"
|
|
87
|
+
fill="none" stroke="${DONUT_COLOR}" stroke-width="${STROKE}"
|
|
88
|
+
stroke-dasharray="${CIRC}" stroke-dashoffset="${offset}"
|
|
89
|
+
stroke-linecap="butt"
|
|
90
|
+
transform="rotate(-90 ${SVG_WIDTH / 2} ${donutCenterY})" />
|
|
91
|
+
|
|
92
|
+
<!-- Numbers -->
|
|
93
|
+
<text x="${SVG_WIDTH / 2}" y="${donutCenterY - 8}" text-anchor="middle" font-size="${VALUE_FONT_SIZE}" font-weight="bold">${value}%</text>
|
|
94
|
+
<text x="${SVG_WIDTH / 2}" y="${donutCenterY + 20}" text-anchor="middle" font-size="${LEGEND_FONT_SIZE}">${legend}</text>
|
|
95
|
+
</svg>
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Cyberismo
|
|
3
|
+
Copyright © Cyberismo Ltd and contributors 2025
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify it under
|
|
6
|
+
the terms of the GNU Affero General Public License version 3 as published by
|
|
7
|
+
the Free Software Foundation. This program is distributed in the hope that it
|
|
8
|
+
will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
9
|
+
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
10
|
+
See the GNU Affero General Public License for more details.
|
|
11
|
+
You should have received a copy of the GNU Affero General Public
|
|
12
|
+
License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
13
|
+
*/
|
|
14
|
+
import { measureTextWidth } from './lib.js';
|
|
15
|
+
|
|
16
|
+
// Padding on y-axis
|
|
17
|
+
const PADDING = 24;
|
|
18
|
+
// font used to estimate text width
|
|
19
|
+
const FONT_FAMILY = 'Helvetica';
|
|
20
|
+
const TITLE_SIZE = 16;
|
|
21
|
+
const VALUE_SIZE = 48;
|
|
22
|
+
const UNIT_SIZE = 24;
|
|
23
|
+
const CAPTION_SIZE = 14;
|
|
24
|
+
const UNIT_OFFSET = 3;
|
|
25
|
+
const LINE_GAP_TITLE = 16;
|
|
26
|
+
const LINE_GAP_CAPTION = 16;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Options for the score card
|
|
30
|
+
* @param title - The title of the score card
|
|
31
|
+
* @param value - The value of the score card
|
|
32
|
+
* @param unit - The unit of the score card
|
|
33
|
+
* @param legend - The legend of the score card
|
|
34
|
+
*/
|
|
35
|
+
export interface ScoreCardOptions {
|
|
36
|
+
value: number;
|
|
37
|
+
title?: string;
|
|
38
|
+
legend?: string;
|
|
39
|
+
unit?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates an SVG score card
|
|
44
|
+
* @param options - The options for the score card
|
|
45
|
+
* @returns The SVG score card
|
|
46
|
+
*/
|
|
47
|
+
export function scoreCard(options: ScoreCardOptions): string {
|
|
48
|
+
const { title = '', value, unit = '', legend = '' } = options;
|
|
49
|
+
|
|
50
|
+
const titleWidth = measureTextWidth(title, FONT_FAMILY, TITLE_SIZE, false);
|
|
51
|
+
const valueWidth = measureTextWidth(
|
|
52
|
+
String(value),
|
|
53
|
+
FONT_FAMILY,
|
|
54
|
+
VALUE_SIZE,
|
|
55
|
+
true,
|
|
56
|
+
);
|
|
57
|
+
const unitWidth = measureTextWidth(unit, FONT_FAMILY, UNIT_SIZE, false);
|
|
58
|
+
const captionWidth = measureTextWidth(
|
|
59
|
+
legend,
|
|
60
|
+
FONT_FAMILY,
|
|
61
|
+
CAPTION_SIZE,
|
|
62
|
+
false,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const valueLineWidth = valueWidth + unitWidth + UNIT_OFFSET;
|
|
66
|
+
const maxTextWidth = Math.max(titleWidth, valueLineWidth, captionWidth);
|
|
67
|
+
|
|
68
|
+
const width = Math.ceil(maxTextWidth + PADDING * 2);
|
|
69
|
+
|
|
70
|
+
const titleHeight = title.length > 0 ? TITLE_SIZE + LINE_GAP_TITLE : 0;
|
|
71
|
+
const valueHeight = VALUE_SIZE;
|
|
72
|
+
const captionHeight = legend.length > 0 ? CAPTION_SIZE + LINE_GAP_CAPTION : 0;
|
|
73
|
+
|
|
74
|
+
const textBlockHeight = titleHeight + valueHeight + captionHeight;
|
|
75
|
+
|
|
76
|
+
const height = Math.ceil(textBlockHeight + PADDING * 2);
|
|
77
|
+
|
|
78
|
+
const svgContent = `<svg class="card" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
|
|
79
|
+
<rect rx="8" ry="8" fill="#fff" stroke="#dfe4ea" stroke-width="3" width="${width}" height="${height}"/>
|
|
80
|
+
<g text-anchor="middle">
|
|
81
|
+
<text class="title" x="${width / 2}" y="${TITLE_SIZE / 2 + PADDING}" font-size="${TITLE_SIZE}" font-weight="400" fill="#001829" dominant-baseline="middle">${title}</text>
|
|
82
|
+
<text class="value" x="${width / 2}" y="${titleHeight + VALUE_SIZE / 2 + PADDING}" font-size="${VALUE_SIZE}" font-weight="700" fill="#333" dominant-baseline="middle">${value}<tspan class="unit" font-size="${UNIT_SIZE}" font-weight="400" dx="${UNIT_OFFSET}">${unit}</tspan></text>
|
|
83
|
+
<text class="caption" x="${width / 2}" y="${titleHeight + valueHeight + LINE_GAP_CAPTION + CAPTION_SIZE / 2 + PADDING}" font-size="${CAPTION_SIZE}" font-weight="400" fill="#999" dominant-baseline="middle">${legend}</text>
|
|
84
|
+
</g>
|
|
85
|
+
</svg>`;
|
|
86
|
+
|
|
87
|
+
return svgContent;
|
|
88
|
+
}
|
package/src/types/queries.ts
CHANGED
|
@@ -61,11 +61,6 @@ export interface DeniedOperationCollection {
|
|
|
61
61
|
|
|
62
62
|
export interface BaseResult extends Record<string, unknown> {
|
|
63
63
|
key: string;
|
|
64
|
-
labels: string[];
|
|
65
|
-
links: CalculationLink[];
|
|
66
|
-
notifications: Notification[];
|
|
67
|
-
policyChecks: PolicyCheckCollection;
|
|
68
|
-
deniedOperations: DeniedOperationCollection;
|
|
69
64
|
}
|
|
70
65
|
|
|
71
66
|
export interface ParseResult<T extends BaseResult> {
|
|
@@ -97,9 +92,15 @@ interface CardQueryResult extends BaseResult {
|
|
|
97
92
|
rank: string;
|
|
98
93
|
title: string;
|
|
99
94
|
cardType: string;
|
|
95
|
+
cardTypeDisplayName: string;
|
|
100
96
|
workflowState: string;
|
|
101
97
|
lastUpdated: string;
|
|
102
|
-
fields
|
|
98
|
+
fields: CardQueryField[];
|
|
99
|
+
labels: string[];
|
|
100
|
+
links: CalculationLink[];
|
|
101
|
+
notifications: Notification[];
|
|
102
|
+
policyChecks: PolicyCheckCollection;
|
|
103
|
+
deniedOperations: DeniedOperationCollection;
|
|
103
104
|
}
|
|
104
105
|
interface FieldsToUpdateQueryResult extends BaseResult {
|
|
105
106
|
updateFields: UpdateField[];
|
|
@@ -135,7 +136,7 @@ interface CardQueryField extends BaseResult {
|
|
|
135
136
|
visibility: 'always' | 'optional';
|
|
136
137
|
index: number;
|
|
137
138
|
fieldDisplayName: string;
|
|
138
|
-
|
|
139
|
+
fieldDescription: string;
|
|
139
140
|
dataType: DataType;
|
|
140
141
|
isCalculated: boolean;
|
|
141
142
|
value: string | number | boolean | null | EnumValue | ListValueItem[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Cyberismo
|
|
3
|
+
Copyright © Cyberismo Ltd and contributors 2025
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify it under
|
|
6
|
+
the terms of the GNU Affero General Public License version 3 as published by
|
|
7
|
+
the Free Software Foundation. This program is distributed in the hope that it
|
|
8
|
+
will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
9
|
+
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
10
|
+
See the GNU Affero General Public License for more details.
|
|
11
|
+
You should have received a copy of the GNU Affero General Public
|
|
12
|
+
License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
13
|
+
*/
|
|
14
|
+
declare module 'string-pixel-width' {
|
|
15
|
+
interface PixelWidthOptions {
|
|
16
|
+
font?: string;
|
|
17
|
+
size?: number;
|
|
18
|
+
bold?: boolean;
|
|
19
|
+
italic?: boolean;
|
|
20
|
+
}
|
|
21
|
+
function pixelWidth(text: string, options?: PixelWidthOptions): number;
|
|
22
|
+
export default pixelWidth;
|
|
23
|
+
}
|
package/src/utils/card-utils.ts
CHANGED
|
@@ -81,3 +81,16 @@ export const sortCards = (a: string, b: string) => {
|
|
|
81
81
|
if (aParts[1] < bParts[1]) return -1;
|
|
82
82
|
return 0;
|
|
83
83
|
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns module name from card key
|
|
87
|
+
* @param cardKey card key
|
|
88
|
+
* @returns module name
|
|
89
|
+
*/
|
|
90
|
+
export const moduleNameFromCardKey = (cardKey: string) => {
|
|
91
|
+
const parts = cardKey.split('_');
|
|
92
|
+
if (parts.length !== 2) {
|
|
93
|
+
throw new Error(`Invalid card key: ${cardKey}`);
|
|
94
|
+
}
|
|
95
|
+
return parts[0];
|
|
96
|
+
};
|