@compas-oscd/scl-template-update 0.0.1

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.
@@ -0,0 +1,249 @@
1
+ function hasSelectionPath(selection, path) {
2
+ let current = selection;
3
+ for (const segment of path) {
4
+ if (!current || !Object.hasOwn(current, segment))
5
+ return false;
6
+ current = current[segment];
7
+ }
8
+ return true;
9
+ }
10
+ function collectMissingMandatorySubfields(tree, selection, parentPath = []) {
11
+ const missing = [];
12
+ Object.entries(tree).forEach(([name, rawNode]) => {
13
+ const node = (rawNode ?? {});
14
+ const path = [...parentPath, name];
15
+ const presentInSelection = hasSelectionPath(selection, path);
16
+ if (node.mandatory && !presentInSelection) {
17
+ missing.push({ path, kind: 'subfield' });
18
+ return;
19
+ }
20
+ if (presentInSelection && node.children) {
21
+ missing.push(...collectMissingMandatorySubfields(node.children, selection, path));
22
+ }
23
+ });
24
+ return missing;
25
+ }
26
+ /**
27
+ * Returns an array of the mandatory fields that are missing from the selection.
28
+ * @param tree The tree structure representing the data model
29
+ * @param selection The current selection in the tree
30
+ * @returns An array of MissingMandatoryField objects
31
+ */
32
+ export function getMissingMandatoryFields(tree, selection) {
33
+ const missing = [];
34
+ Object.entries(tree).forEach(([doName, rawNode]) => {
35
+ const node = (rawNode ?? {});
36
+ const doPath = [doName];
37
+ const doPresentInSelection = hasSelectionPath(selection, doPath);
38
+ if (doPresentInSelection && node.children) {
39
+ missing.push(...collectMissingMandatorySubfields(node.children, selection, doPath));
40
+ }
41
+ });
42
+ return missing.sort((a, b) => a.path.join('/').localeCompare(b.path.join('/')));
43
+ }
44
+ function findTypeElementById(dataTypeTemplates, id) {
45
+ return Array.from(dataTypeTemplates.children).find(child => {
46
+ const isKnownTypeTag = child.tagName === 'DOType' ||
47
+ child.tagName === 'DAType' ||
48
+ child.tagName === 'EnumType';
49
+ return isKnownTypeTag && child.getAttribute('id') === id;
50
+ });
51
+ }
52
+ function getInitialReferencedTypes(lNodeType) {
53
+ return Array.from(lNodeType.querySelectorAll(':scope > DO'))
54
+ .map(doElement => {
55
+ const typeId = doElement.getAttribute('type');
56
+ if (!typeId)
57
+ return undefined;
58
+ return {
59
+ typeId,
60
+ referencePath: doElement.getAttribute('name'),
61
+ };
62
+ })
63
+ .filter((entry) => !!entry);
64
+ }
65
+ function toChildReference(element, parentReferencePath) {
66
+ const typeId = element.getAttribute('type');
67
+ const name = element.getAttribute('name');
68
+ if (!typeId || !name)
69
+ return undefined;
70
+ return {
71
+ typeId,
72
+ referencePath: `${parentReferencePath}.${name}`,
73
+ };
74
+ }
75
+ function getChildReferences(children, parentReferencePath) {
76
+ return children
77
+ .map(child => toChildReference(child, parentReferencePath))
78
+ .filter((entry) => !!entry);
79
+ }
80
+ function analyseReferencedType(typeElement, referencePath) {
81
+ if (typeElement.tagName === 'DOType') {
82
+ const directChildren = Array.from(typeElement.querySelectorAll(':scope > DA, :scope > SDO'));
83
+ return {
84
+ emptyTagName: directChildren.length === 0 ? 'DOType' : undefined,
85
+ childReferences: getChildReferences(directChildren, referencePath),
86
+ };
87
+ }
88
+ if (typeElement.tagName === 'DAType') {
89
+ const directChildren = Array.from(typeElement.querySelectorAll(':scope > BDA'));
90
+ return {
91
+ emptyTagName: directChildren.length === 0 ? 'DAType' : undefined,
92
+ childReferences: getChildReferences(directChildren, referencePath),
93
+ };
94
+ }
95
+ if (typeElement.tagName === 'EnumType') {
96
+ const enumValues = Array.from(typeElement.querySelectorAll(':scope > EnumVal'));
97
+ return {
98
+ emptyTagName: enumValues.length === 0 ? 'EnumType' : undefined,
99
+ childReferences: [],
100
+ };
101
+ }
102
+ return { childReferences: [] };
103
+ }
104
+ function hasMandatoryOverlap(referencePath, missingMandatoryFields) {
105
+ const referencePathParts = referencePath
106
+ .split('.')
107
+ .map(part => part.trim())
108
+ .filter(Boolean);
109
+ return missingMandatoryFields.some(missing => {
110
+ if (missing.path.length < referencePathParts.length)
111
+ return false;
112
+ return referencePathParts.every((segment, index) => segment === missing.path[index]);
113
+ });
114
+ }
115
+ /**
116
+ * Finds all referenced elements of an LNodeType that are empty (i.e., have no children).
117
+ * @param lNodeType The LNodeType element to analyse
118
+ * @param missingMandatoryFields An optional array of MissingMandatoryField objects to filter out empty elements that are part of mandatory fields
119
+ * @returns An array of EmptyReferencedElement objects representing the empty referenced elements
120
+ */
121
+ export function getEmptyReferencedElements(lNodeType, missingMandatoryFields = []) {
122
+ const dataTypeTemplates = lNodeType.closest('DataTypeTemplates');
123
+ if (!dataTypeTemplates)
124
+ return [];
125
+ const referenceQueue = getInitialReferencedTypes(lNodeType);
126
+ const processedReferences = new Set();
127
+ const emptyElements = [];
128
+ while (referenceQueue.length > 0) {
129
+ const currentReference = referenceQueue.shift();
130
+ if (!currentReference)
131
+ break;
132
+ const { typeId, referencePath } = currentReference;
133
+ const referenceKey = `${typeId}:${referencePath}`;
134
+ if (!processedReferences.has(referenceKey)) {
135
+ processedReferences.add(referenceKey);
136
+ const typeElement = findTypeElementById(dataTypeTemplates, typeId);
137
+ if (typeElement) {
138
+ const analysis = analyseReferencedType(typeElement, referencePath);
139
+ if (analysis.emptyTagName) {
140
+ emptyElements.push({
141
+ tagName: analysis.emptyTagName,
142
+ id: typeId,
143
+ referencePath,
144
+ });
145
+ }
146
+ referenceQueue.push(...analysis.childReferences);
147
+ }
148
+ }
149
+ }
150
+ return emptyElements
151
+ .filter(element => !hasMandatoryOverlap(element.referencePath, missingMandatoryFields))
152
+ .sort((a, b) => {
153
+ const tagOrder = a.tagName.localeCompare(b.tagName);
154
+ if (tagOrder !== 0)
155
+ return tagOrder;
156
+ const idOrder = a.id.localeCompare(b.id);
157
+ if (idOrder !== 0)
158
+ return idOrder;
159
+ return a.referencePath.localeCompare(b.referencePath);
160
+ });
161
+ }
162
+ export function getLNodeTypes(doc) {
163
+ return Array.from(doc?.querySelectorAll(':root > DataTypeTemplates > LNodeType') ?? []);
164
+ }
165
+ export function getSelectedLNodeType(doc, selected) {
166
+ return (doc?.querySelector(`:root > DataTypeTemplates > LNodeType[id="${selected}"]`) ?? undefined);
167
+ }
168
+ export function isLNodeTypeReferenced(doc, selectedLNodeTypeID) {
169
+ if (!doc || !selectedLNodeTypeID)
170
+ return false;
171
+ return !!doc.querySelector(`:root > Substation LNode[lnType="${selectedLNodeTypeID}"], :root > IED LN[lnType="${selectedLNodeTypeID}"], :root > IED LN0[lnType="${selectedLNodeTypeID}"]`);
172
+ }
173
+ export function filterSelection(tree, selection) {
174
+ const filteredTree = {};
175
+ Object.keys(selection).forEach(key => {
176
+ const isThere = !!tree[key];
177
+ if (isThere)
178
+ filteredTree[key] = selection[key];
179
+ });
180
+ return filteredTree;
181
+ }
182
+ /**
183
+ * Creates a clone of an LNodeType with only the DOs that are present in the selection.
184
+ * DOs not included in the selection are removed from the cloned element.
185
+ * @param lNodeType - The LNodeType element to filter
186
+ * @param selection - The tree selection containing the DO names to keep
187
+ * @returns A cloned LNodeType element containing only the selected DOs
188
+ */
189
+ export function removeDOsNotInSelection(lNodeType, selection) {
190
+ const clonedLNodeType = lNodeType.cloneNode(true);
191
+ const dosToRemove = [];
192
+ Array.from(clonedLNodeType.querySelectorAll(':scope > DO')).forEach(doElement => {
193
+ const doName = doElement.getAttribute('name');
194
+ if (doName && !selection[doName]) {
195
+ dosToRemove.push(doElement);
196
+ }
197
+ });
198
+ dosToRemove.forEach(doElement => {
199
+ clonedLNodeType.removeChild(doElement);
200
+ });
201
+ return clonedLNodeType;
202
+ }
203
+ /**
204
+ * Builds the Remove edits needed when an LNodeType is replaced.
205
+ * Clones the DataTypeTemplates once, adds the incoming new nodes, removes
206
+ * the old LNodeType, then cascades to find which pre-existing sub-types are
207
+ * now unreferenced. Returns the old LNodeType remove plus all orphan removes.
208
+ * @param dataTypeTemplates The live DataTypeTemplates element to base the remove calculations on
209
+ * @param oldLNodeTypeId The ID of the LNodeType being replaced
210
+ * @param newNodes The new nodes being added (cloned before passing in)
211
+ * @returns An array of EditV2 objects representing the nodes to remove
212
+ */
213
+ export function computeOrphanedRemoves(dataTypeTemplates, oldLNodeTypeId, newNodes) {
214
+ const subTypes = Array.from(dataTypeTemplates.querySelectorAll(':scope > DOType, :scope > DAType, :scope > EnumType'));
215
+ const subTypeIds = subTypes
216
+ .map(el => el.getAttribute('id'))
217
+ .filter(id => id !== null);
218
+ const preEditIds = new Set(subTypeIds);
219
+ const dttClone = dataTypeTemplates.cloneNode(true);
220
+ newNodes.forEach(n => dttClone.appendChild(n.cloneNode(true)));
221
+ dttClone
222
+ .querySelector(`:scope > LNodeType[id="${oldLNodeTypeId}"]`)
223
+ ?.remove();
224
+ const remainingIds = new Set(preEditIds);
225
+ const orphanIds = [];
226
+ let changed = true;
227
+ while (changed) {
228
+ changed = false;
229
+ for (const id of remainingIds) {
230
+ if (!dttClone.querySelector(`:scope *[type="${id}"]`)) {
231
+ orphanIds.push(id);
232
+ remainingIds.delete(id);
233
+ dttClone.querySelector(`:scope > *[id="${id}"]`)?.remove();
234
+ changed = true;
235
+ }
236
+ }
237
+ }
238
+ const results = [];
239
+ const oldLN = dataTypeTemplates.querySelector(`:scope > LNodeType[id="${oldLNodeTypeId}"]`);
240
+ if (oldLN)
241
+ results.push({ node: oldLN });
242
+ orphanIds.forEach(id => {
243
+ const node = dataTypeTemplates.querySelector(`:scope > *[id="${id}"]`);
244
+ if (node)
245
+ results.push({ node });
246
+ });
247
+ return results;
248
+ }
249
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../foundation/utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { Tree, TreeSelection } from '@openenergytools/tree-grid';\nimport { EditV2 } from '@openscd/oscd-api';\n\nexport type MissingMandatoryField = {\n path: string[];\n kind: 'subfield';\n};\n\nexport type EmptyReferencedElement = {\n tagName: 'DOType' | 'DAType' | 'EnumType';\n id: string;\n referencePath: string;\n};\n\ntype ReferencedType = {\n typeId: string;\n referencePath: string;\n};\n\ntype EmptyReferenceAnalysis = {\n emptyTagName?: EmptyReferencedElement['tagName'];\n childReferences: ReferencedType[];\n};\n\ntype TreeNodeWithMetadata = {\n children?: Tree;\n mandatory?: boolean;\n presCond?: string;\n};\n\nfunction hasSelectionPath(selection: TreeSelection, path: string[]): boolean {\n let current: TreeSelection | undefined = selection;\n for (const segment of path) {\n if (!current || !Object.hasOwn(current, segment)) return false;\n current = current[segment];\n }\n return true;\n}\n\nfunction collectMissingMandatorySubfields(\n tree: Tree,\n selection: TreeSelection,\n parentPath: string[] = []\n): MissingMandatoryField[] {\n const missing: MissingMandatoryField[] = [];\n\n Object.entries(tree).forEach(([name, rawNode]) => {\n const node = (rawNode ?? {}) as TreeNodeWithMetadata;\n const path = [...parentPath, name];\n const presentInSelection = hasSelectionPath(selection, path);\n\n if (node.mandatory && !presentInSelection) {\n missing.push({ path, kind: 'subfield' });\n return;\n }\n\n if (presentInSelection && node.children) {\n missing.push(\n ...collectMissingMandatorySubfields(node.children, selection, path)\n );\n }\n });\n\n return missing;\n}\n\n/**\n * Returns an array of the mandatory fields that are missing from the selection.\n * @param tree The tree structure representing the data model\n * @param selection The current selection in the tree\n * @returns An array of MissingMandatoryField objects\n */\nexport function getMissingMandatoryFields(\n tree: Tree,\n selection: TreeSelection\n): MissingMandatoryField[] {\n const missing: MissingMandatoryField[] = [];\n\n Object.entries(tree).forEach(([doName, rawNode]) => {\n const node = (rawNode ?? {}) as TreeNodeWithMetadata;\n const doPath = [doName];\n const doPresentInSelection = hasSelectionPath(selection, doPath);\n if (doPresentInSelection && node.children) {\n missing.push(\n ...collectMissingMandatorySubfields(node.children, selection, doPath)\n );\n }\n });\n\n return missing.sort((a, b) =>\n a.path.join('/').localeCompare(b.path.join('/'))\n );\n}\n\nfunction findTypeElementById(\n dataTypeTemplates: Element,\n id: string\n): Element | undefined {\n return Array.from(dataTypeTemplates.children).find(child => {\n const isKnownTypeTag =\n child.tagName === 'DOType' ||\n child.tagName === 'DAType' ||\n child.tagName === 'EnumType';\n\n return isKnownTypeTag && child.getAttribute('id') === id;\n });\n}\n\nfunction getInitialReferencedTypes(lNodeType: Element): ReferencedType[] {\n return Array.from(lNodeType.querySelectorAll(':scope > DO'))\n .map(doElement => {\n const typeId = doElement.getAttribute('type');\n if (!typeId) return undefined;\n\n return {\n typeId,\n referencePath: doElement.getAttribute('name'),\n };\n })\n .filter((entry): entry is ReferencedType => !!entry);\n}\n\nfunction toChildReference(\n element: Element,\n parentReferencePath: string\n): ReferencedType | undefined {\n const typeId = element.getAttribute('type');\n const name = element.getAttribute('name');\n if (!typeId || !name) return undefined;\n\n return {\n typeId,\n referencePath: `${parentReferencePath}.${name}`,\n };\n}\n\nfunction getChildReferences(\n children: Element[],\n parentReferencePath: string\n): ReferencedType[] {\n return children\n .map(child => toChildReference(child, parentReferencePath))\n .filter((entry): entry is ReferencedType => !!entry);\n}\n\nfunction analyseReferencedType(\n typeElement: Element,\n referencePath: string\n): EmptyReferenceAnalysis {\n if (typeElement.tagName === 'DOType') {\n const directChildren = Array.from(\n typeElement.querySelectorAll(':scope > DA, :scope > SDO')\n );\n\n return {\n emptyTagName: directChildren.length === 0 ? 'DOType' : undefined,\n childReferences: getChildReferences(directChildren, referencePath),\n };\n }\n\n if (typeElement.tagName === 'DAType') {\n const directChildren = Array.from(\n typeElement.querySelectorAll(':scope > BDA')\n );\n\n return {\n emptyTagName: directChildren.length === 0 ? 'DAType' : undefined,\n childReferences: getChildReferences(directChildren, referencePath),\n };\n }\n\n if (typeElement.tagName === 'EnumType') {\n const enumValues = Array.from(\n typeElement.querySelectorAll(':scope > EnumVal')\n );\n\n return {\n emptyTagName: enumValues.length === 0 ? 'EnumType' : undefined,\n childReferences: [],\n };\n }\n\n return { childReferences: [] };\n}\n\nfunction hasMandatoryOverlap(\n referencePath: string,\n missingMandatoryFields: MissingMandatoryField[]\n): boolean {\n const referencePathParts = referencePath\n .split('.')\n .map(part => part.trim())\n .filter(Boolean);\n\n return missingMandatoryFields.some(missing => {\n if (missing.path.length < referencePathParts.length) return false;\n return referencePathParts.every(\n (segment, index) => segment === missing.path[index]\n );\n });\n}\n\n/**\n * Finds all referenced elements of an LNodeType that are empty (i.e., have no children).\n * @param lNodeType The LNodeType element to analyse\n * @param missingMandatoryFields An optional array of MissingMandatoryField objects to filter out empty elements that are part of mandatory fields\n * @returns An array of EmptyReferencedElement objects representing the empty referenced elements\n */\nexport function getEmptyReferencedElements(\n lNodeType: Element,\n missingMandatoryFields: MissingMandatoryField[] = []\n): EmptyReferencedElement[] {\n const dataTypeTemplates = lNodeType.closest('DataTypeTemplates');\n if (!dataTypeTemplates) return [];\n\n const referenceQueue: ReferencedType[] = getInitialReferencedTypes(lNodeType);\n\n const processedReferences = new Set<string>();\n const emptyElements: EmptyReferencedElement[] = [];\n\n while (referenceQueue.length > 0) {\n const currentReference = referenceQueue.shift();\n if (!currentReference) break;\n\n const { typeId, referencePath } = currentReference;\n const referenceKey = `${typeId}:${referencePath}`;\n\n if (!processedReferences.has(referenceKey)) {\n processedReferences.add(referenceKey);\n\n const typeElement = findTypeElementById(dataTypeTemplates, typeId);\n if (typeElement) {\n const analysis = analyseReferencedType(typeElement, referencePath);\n if (analysis.emptyTagName) {\n emptyElements.push({\n tagName: analysis.emptyTagName,\n id: typeId,\n referencePath,\n });\n }\n\n referenceQueue.push(...analysis.childReferences);\n }\n }\n }\n\n return emptyElements\n .filter(\n element =>\n !hasMandatoryOverlap(element.referencePath, missingMandatoryFields)\n )\n .sort((a, b) => {\n const tagOrder = a.tagName.localeCompare(b.tagName);\n if (tagOrder !== 0) return tagOrder;\n\n const idOrder = a.id.localeCompare(b.id);\n if (idOrder !== 0) return idOrder;\n\n return a.referencePath.localeCompare(b.referencePath);\n });\n}\n\nexport function getLNodeTypes(doc: XMLDocument | undefined): Element[] {\n return Array.from(\n doc?.querySelectorAll(':root > DataTypeTemplates > LNodeType') ?? []\n );\n}\n\nexport function getSelectedLNodeType(\n doc: XMLDocument,\n selected: string\n): Element | undefined {\n return (\n doc?.querySelector(\n `:root > DataTypeTemplates > LNodeType[id=\"${selected}\"]`\n ) ?? undefined\n );\n}\n\nexport function isLNodeTypeReferenced(\n doc: XMLDocument,\n selectedLNodeTypeID: string | null\n): boolean {\n if (!doc || !selectedLNodeTypeID) return false;\n return !!doc.querySelector(\n `:root > Substation LNode[lnType=\"${selectedLNodeTypeID}\"], :root > IED LN[lnType=\"${selectedLNodeTypeID}\"], :root > IED LN0[lnType=\"${selectedLNodeTypeID}\"]`\n );\n}\n\nexport function filterSelection(\n tree: Tree,\n selection: TreeSelection\n): TreeSelection {\n const filteredTree: TreeSelection = {};\n Object.keys(selection).forEach(key => {\n const isThere = !!tree[key];\n if (isThere) filteredTree[key] = selection[key];\n });\n\n return filteredTree;\n}\n\n/**\n * Creates a clone of an LNodeType with only the DOs that are present in the selection.\n * DOs not included in the selection are removed from the cloned element.\n * @param lNodeType - The LNodeType element to filter\n * @param selection - The tree selection containing the DO names to keep\n * @returns A cloned LNodeType element containing only the selected DOs\n */\nexport function removeDOsNotInSelection(\n lNodeType: Element,\n selection: TreeSelection\n): Element {\n const clonedLNodeType = lNodeType.cloneNode(true) as Element;\n\n const dosToRemove: Element[] = [];\n Array.from(clonedLNodeType.querySelectorAll(':scope > DO')).forEach(\n doElement => {\n const doName = doElement.getAttribute('name');\n if (doName && !selection[doName]) {\n dosToRemove.push(doElement);\n }\n }\n );\n\n dosToRemove.forEach(doElement => {\n clonedLNodeType.removeChild(doElement);\n });\n\n return clonedLNodeType;\n}\n\n/**\n * Builds the Remove edits needed when an LNodeType is replaced.\n * Clones the DataTypeTemplates once, adds the incoming new nodes, removes\n * the old LNodeType, then cascades to find which pre-existing sub-types are\n * now unreferenced. Returns the old LNodeType remove plus all orphan removes.\n * @param dataTypeTemplates The live DataTypeTemplates element to base the remove calculations on\n * @param oldLNodeTypeId The ID of the LNodeType being replaced\n * @param newNodes The new nodes being added (cloned before passing in)\n * @returns An array of EditV2 objects representing the nodes to remove\n */\nexport function computeOrphanedRemoves(\n dataTypeTemplates: Element,\n oldLNodeTypeId: string,\n newNodes: Element[]\n): EditV2[] {\n const subTypes = Array.from(\n dataTypeTemplates.querySelectorAll(\n ':scope > DOType, :scope > DAType, :scope > EnumType'\n )\n );\n const subTypeIds = subTypes\n .map(el => el.getAttribute('id'))\n .filter(id => id !== null);\n const preEditIds = new Set(subTypeIds);\n\n const dttClone = dataTypeTemplates.cloneNode(true) as Element;\n newNodes.forEach(n => dttClone.appendChild(n.cloneNode(true)));\n dttClone\n .querySelector(`:scope > LNodeType[id=\"${oldLNodeTypeId}\"]`)\n ?.remove();\n\n const remainingIds = new Set(preEditIds);\n const orphanIds: string[] = [];\n let changed = true;\n\n while (changed) {\n changed = false;\n for (const id of remainingIds) {\n if (!dttClone.querySelector(`:scope *[type=\"${id}\"]`)) {\n orphanIds.push(id);\n remainingIds.delete(id);\n dttClone.querySelector(`:scope > *[id=\"${id}\"]`)?.remove();\n changed = true;\n }\n }\n }\n\n const results: EditV2[] = [];\n const oldLN = dataTypeTemplates.querySelector(\n `:scope > LNodeType[id=\"${oldLNodeTypeId}\"]`\n );\n if (oldLN) results.push({ node: oldLN as Node });\n orphanIds.forEach(id => {\n const node = dataTypeTemplates.querySelector(\n `:scope > *[id=\"${id}\"]`\n ) as Node | null;\n if (node) results.push({ node });\n });\n return results;\n}\n"]}
@@ -0,0 +1,88 @@
1
+ import { LitElement, TemplateResult } from 'lit';
2
+ import { TreeGrid, TreeSelection } from '@openenergytools/tree-grid';
3
+ import { OscdOutlinedButton } from '@omicronenergy/oscd-ui/button/OscdOutlinedButton.js';
4
+ import { OscdDialog } from '@omicronenergy/oscd-ui/dialog/OscdDialog.js';
5
+ import { OscdFab } from '@omicronenergy/oscd-ui/fab/OscdFab.js';
6
+ import { OscdIcon } from '@omicronenergy/oscd-ui/icon/OscdIcon.js';
7
+ import { OscdCircularProgress } from '@omicronenergy/oscd-ui/progress/OscdCircularProgress.js';
8
+ import { OscdOutlinedTextField } from '@omicronenergy/oscd-ui/textfield/OscdOutlinedTextField.js';
9
+ import { OscdIconButton } from '@omicronenergy/oscd-ui/iconbutton/OscdIconButton.js';
10
+ import { OscdAssistChip } from '@omicronenergy/oscd-ui/chips/OscdAssistChip.js';
11
+ import { AddDataObjectDialog } from './components/add-data-object-dialog.js';
12
+ import { DeleteDialog } from './components/delete-lnodetype-dialog.js';
13
+ import { LNodeTypeSidebar } from './components/lnodetype-sidebar.js';
14
+ import { SettingsDialog } from './components/settings-dialog.js';
15
+ import { EmptyReferencedElement, type MissingMandatoryField } from './foundation/utils.js';
16
+ declare const NsdTemplateUpdated_base: typeof LitElement & import("@open-wc/scoped-elements/lit-element.js").ScopedElementsHostConstructor;
17
+ export default class NsdTemplateUpdated extends NsdTemplateUpdated_base {
18
+ static scopedElements: {
19
+ 'tree-grid': typeof TreeGrid;
20
+ 'oscd-fab': typeof OscdFab;
21
+ 'oscd-icon': typeof OscdIcon;
22
+ 'oscd-dialog': typeof OscdDialog;
23
+ 'oscd-outlined-button': typeof OscdOutlinedButton;
24
+ 'oscd-circular-progress': typeof OscdCircularProgress;
25
+ 'oscd-outlined-text-field': typeof OscdOutlinedTextField;
26
+ 'oscd-icon-button': typeof OscdIconButton;
27
+ 'oscd-assist-chip': typeof OscdAssistChip;
28
+ 'add-data-object-dialog': typeof AddDataObjectDialog;
29
+ 'delete-dialog': typeof DeleteDialog;
30
+ 'lnodetype-sidebar': typeof LNodeTypeSidebar;
31
+ 'settings-dialog': typeof SettingsDialog;
32
+ };
33
+ doc?: XMLDocument;
34
+ editCount: number;
35
+ treeUI: TreeGrid;
36
+ warningDialog?: OscdDialog;
37
+ choiceDialog?: OscdDialog;
38
+ deleteDialog: DeleteDialog;
39
+ addDataObjectDialog: HTMLElement & {
40
+ show: () => void;
41
+ validateForm: () => boolean;
42
+ };
43
+ settingsDialog: SettingsDialog;
44
+ lnodeTypeDesc: OscdOutlinedTextField;
45
+ lNodeTypes: Element[];
46
+ selectedLNodeType?: Element;
47
+ lNodeTypeSelection?: TreeSelection;
48
+ nsdSelection?: TreeSelection;
49
+ warningMsg: string;
50
+ loading: boolean;
51
+ fabLabel: string;
52
+ disableAddDataObjectButton: boolean;
53
+ lNodeTypeDescription: string;
54
+ missingMandatoryFields: MissingMandatoryField[];
55
+ emptyReferencedElements: EmptyReferencedElement[];
56
+ missingFieldsCollapsed: boolean;
57
+ private toggleMissingFieldsPanel;
58
+ updated(changedProperties: Map<string, unknown>): void;
59
+ private refreshSelectedLNodeType;
60
+ private cloneSelection;
61
+ private resetUI;
62
+ private openAddDataObjectDialog;
63
+ private showWarning;
64
+ private focusTreePath;
65
+ private getHeaderOffset;
66
+ private warningPathFromEntry;
67
+ private closeWarningDialog;
68
+ private closeChoiceDialog;
69
+ private applyDescriptionUpdate;
70
+ private saveTemplates;
71
+ private showSuccessFeedback;
72
+ private buildUpdateEdits;
73
+ private updateLNodeTypeDescription;
74
+ private proceedWithDataLoss;
75
+ private confirmDelete;
76
+ private handleUpdateTemplate;
77
+ onLNodeTypeSelect(e: CustomEvent): Promise<void>;
78
+ private addDataObjectToTree;
79
+ private handleAddDOConfirm;
80
+ renderWarning(): TemplateResult;
81
+ renderChoice(): TemplateResult;
82
+ renderFab(): TemplateResult;
83
+ renderLNodeTypeControls(): TemplateResult;
84
+ renderMissingMandatoryFields(): TemplateResult;
85
+ render(): TemplateResult<1>;
86
+ static styles: import("lit").CSSResult;
87
+ }
88
+ export {};