@finos/legend-lego 2.0.148 → 2.0.150

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,1139 @@
1
+ /**
2
+ * Copyright (c) 2025-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import {
18
+ AnchorLinkIcon,
19
+ BasePopover,
20
+ ChevronDownIcon,
21
+ ChevronRightIcon,
22
+ ClockIcon,
23
+ CogIcon,
24
+ ControlledDropdownMenu,
25
+ FilterIcon,
26
+ InfoCircleIcon,
27
+ MenuContent,
28
+ MenuContentItem,
29
+ MoreVerticalIcon,
30
+ SearchIcon,
31
+ TimesIcon,
32
+ Tooltip,
33
+ clsx,
34
+ type TreeNodeContainerProps,
35
+ TreeView,
36
+ PackageIcon,
37
+ CheckSquareIcon,
38
+ MinusSquareIcon,
39
+ EmptySquareIcon,
40
+ CaretRightIcon,
41
+ CaretLeftIcon,
42
+ } from '@finos/legend-art';
43
+ import {
44
+ CORE_PURE_PATH,
45
+ ELEMENT_PATH_DELIMITER,
46
+ getMultiplicityDescription,
47
+ MILESTONING_STEREOTYPE,
48
+ PROPERTY_ACCESSOR,
49
+ } from '@finos/legend-graph';
50
+ import {
51
+ type NormalizedDocumentationEntry,
52
+ AssociationDocumentationEntry,
53
+ BasicDocumentationEntry,
54
+ ClassDocumentationEntry,
55
+ EnumerationDocumentationEntry,
56
+ ModelDocumentationEntry,
57
+ PropertyDocumentationEntry,
58
+ } from './ModelDocumentationAnalysis.js';
59
+ import { debounce, isNonNullable, prettyCONSTName } from '@finos/legend-shared';
60
+ import {
61
+ DataGrid,
62
+ type DataGridCellRendererParams,
63
+ } from '../data-grid/DataGrid.js';
64
+ import {
65
+ useApplicationStore,
66
+ useCommands,
67
+ type GenericLegendApplicationStore,
68
+ } from '@finos/legend-application';
69
+ import { observer } from 'mobx-react-lite';
70
+ import {
71
+ type ModelsDocumentationFilterTreeNodeData,
72
+ type ViewerModelsDocumentationState,
73
+ checkFilterTreeNode,
74
+ ModelsDocumentationFilterTreeElementNodeData,
75
+ ModelsDocumentationFilterTreeNodeCheckType,
76
+ ModelsDocumentationFilterTreePackageNodeData,
77
+ ModelsDocumentationFilterTreeRootNodeData,
78
+ ModelsDocumentationFilterTreeTypeNodeData,
79
+ uncheckAllFilterTree,
80
+ uncheckFilterTreeNode,
81
+ } from './ModelDocumentationState.js';
82
+ import { FuzzySearchAdvancedConfigMenu } from '../application/FuzzySearchAdvancedConfigMenu.js';
83
+ import { useEffect, useMemo, useRef } from 'react';
84
+
85
+ export const getMilestoningLabel = (
86
+ val: string | undefined,
87
+ ): string | undefined => {
88
+ switch (val) {
89
+ case MILESTONING_STEREOTYPE.BITEMPORAL:
90
+ return 'Bi-temporal';
91
+ case MILESTONING_STEREOTYPE.BUSINESS_TEMPORAL:
92
+ return 'Business Temporal';
93
+ case MILESTONING_STEREOTYPE.PROCESSING_TEMPORAL:
94
+ return 'Processing Temporal';
95
+ default:
96
+ return undefined;
97
+ }
98
+ };
99
+
100
+ const ElementInfoTooltip: React.FC<{
101
+ entry: ModelDocumentationEntry;
102
+ children: React.ReactElement;
103
+ }> = (props) => {
104
+ const { entry, children } = props;
105
+
106
+ return (
107
+ <Tooltip
108
+ arrow={false}
109
+ placement="bottom-end"
110
+ disableInteractive={true}
111
+ classes={{
112
+ tooltip: 'models-documentation__tooltip',
113
+ tooltipPlacementRight: 'models-documentation__tooltip--right',
114
+ }}
115
+ slotProps={{
116
+ transition: {
117
+ // disable transition
118
+ // NOTE: somehow, this is the only workaround we have, if for example
119
+ // we set `appear = true`, the tooltip will jump out of position
120
+ timeout: 0,
121
+ },
122
+ }}
123
+ title={
124
+ <div className="models-documentation__tooltip__content">
125
+ <div className="models-documentation__tooltip__item">
126
+ <div className="models-documentation__tooltip__item__label">
127
+ Name
128
+ </div>
129
+ <div className="models-documentation__tooltip__item__value">
130
+ {entry.name}
131
+ </div>
132
+ </div>
133
+ <div className="models-documentation__tooltip__item">
134
+ <div className="models-documentation__tooltip__item__label">
135
+ Path
136
+ </div>
137
+ <div className="models-documentation__tooltip__item__value">
138
+ {entry.path}
139
+ </div>
140
+ </div>
141
+ {entry instanceof ClassDocumentationEntry &&
142
+ entry.milestoning !== undefined && (
143
+ <div className="models-documentation__tooltip__item">
144
+ <div className="models-documentation__tooltip__item__label">
145
+ Milestoning
146
+ </div>
147
+ <div className="models-documentation__tooltip__item__value">
148
+ {getMilestoningLabel(entry.milestoning)}
149
+ </div>
150
+ </div>
151
+ )}
152
+ </div>
153
+ }
154
+ >
155
+ {children}
156
+ </Tooltip>
157
+ );
158
+ };
159
+
160
+ const PropertyInfoTooltip: React.FC<{
161
+ entry: PropertyDocumentationEntry;
162
+ elementEntry: ModelDocumentationEntry;
163
+ children: React.ReactElement;
164
+ }> = (props) => {
165
+ const { entry, elementEntry, children } = props;
166
+
167
+ return (
168
+ <Tooltip
169
+ arrow={false}
170
+ placement="bottom-end"
171
+ disableInteractive={true}
172
+ classes={{
173
+ tooltip: 'models-documentation__tooltip',
174
+ tooltipPlacementRight: 'models-documentation__tooltip--right',
175
+ }}
176
+ slotProps={{
177
+ transition: {
178
+ // disable transition
179
+ // NOTE: somehow, this is the only workaround we have, if for example
180
+ // we set `appear = true`, the tooltip will jump out of position
181
+ timeout: 0,
182
+ },
183
+ }}
184
+ title={
185
+ <div className="models-documentation__tooltip__content">
186
+ <div className="models-documentation__tooltip__item">
187
+ <div className="models-documentation__tooltip__item__label">
188
+ Name
189
+ </div>
190
+ <div className="models-documentation__tooltip__item__value">
191
+ {entry.name}
192
+ </div>
193
+ </div>
194
+ <div className="models-documentation__tooltip__item">
195
+ <div className="models-documentation__tooltip__item__label">
196
+ Owner
197
+ </div>
198
+ <div className="models-documentation__tooltip__item__value">
199
+ {elementEntry.path}
200
+ </div>
201
+ </div>
202
+ {entry.type && (
203
+ <div className="models-documentation__tooltip__item">
204
+ <div className="models-documentation__tooltip__item__label">
205
+ Type
206
+ </div>
207
+ <div className="models-documentation__tooltip__item__value">
208
+ {entry.type}
209
+ </div>
210
+ </div>
211
+ )}
212
+ {entry.multiplicity && (
213
+ <div className="models-documentation__tooltip__item">
214
+ <div className="models-documentation__tooltip__item__label">
215
+ Multiplicity
216
+ </div>
217
+ <div className="models-documentation__tooltip__item__value">
218
+ {getMultiplicityDescription(entry.multiplicity)}
219
+ </div>
220
+ </div>
221
+ )}
222
+ {entry.milestoning && (
223
+ <div className="models-documentation__tooltip__item">
224
+ <div className="models-documentation__tooltip__item__label">
225
+ Milestoning
226
+ </div>
227
+ <div className="models-documentation__tooltip__item__value">
228
+ {getMilestoningLabel(entry.milestoning)}
229
+ </div>
230
+ </div>
231
+ )}
232
+ </div>
233
+ }
234
+ >
235
+ {children}
236
+ </Tooltip>
237
+ );
238
+ };
239
+
240
+ export const ElementContentCellRenderer = observer(
241
+ (
242
+ params: DataGridCellRendererParams<NormalizedDocumentationEntry> & {
243
+ modelsDocumentationState: ViewerModelsDocumentationState;
244
+ },
245
+ ) => {
246
+ const { data, modelsDocumentationState } = params;
247
+ const applicationStore = useApplicationStore();
248
+ const showHumanizedForm = modelsDocumentationState.showHumanizedForm;
249
+
250
+ if (!data) {
251
+ return null;
252
+ }
253
+
254
+ const copyPath = (): void => {
255
+ applicationStore.clipboardService
256
+ .copyTextToClipboard(data.elementEntry.path)
257
+ .catch(applicationStore.alertUnhandledError);
258
+ };
259
+ const label = showHumanizedForm
260
+ ? prettyCONSTName(data.elementEntry.name)
261
+ : data.elementEntry.name;
262
+
263
+ if (data.elementEntry instanceof ClassDocumentationEntry) {
264
+ return (
265
+ <div
266
+ className="models-documentation__grid__cell"
267
+ title={`Class: ${data.elementEntry.path}`}
268
+ >
269
+ <div className="models-documentation__grid__cell__label">
270
+ <div className="models-documentation__grid__cell__label__icon models-documentation__grid__cell__label__icon--class">
271
+ C
272
+ </div>
273
+ <div className="models-documentation__grid__cell__label__text">
274
+ {label}
275
+ </div>
276
+ {data.elementEntry.milestoning && (
277
+ <div
278
+ className="models-documentation__grid__cell__label__milestoning-badge"
279
+ title={`Milestoning: ${getMilestoningLabel(
280
+ data.elementEntry.milestoning,
281
+ )}`}
282
+ >
283
+ <ClockIcon />
284
+ </div>
285
+ )}
286
+ </div>
287
+ <div className="models-documentation__grid__cell__actions">
288
+ <ElementInfoTooltip entry={data.elementEntry}>
289
+ <div className="models-documentation__grid__cell__action">
290
+ <InfoCircleIcon className="models-documentation__grid__cell__action__info" />
291
+ </div>
292
+ </ElementInfoTooltip>
293
+ <ControlledDropdownMenu
294
+ className="models-documentation__grid__cell__action"
295
+ menuProps={{
296
+ anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
297
+ transformOrigin: { vertical: 'top', horizontal: 'right' },
298
+ elevation: 7,
299
+ }}
300
+ content={
301
+ <MenuContent>
302
+ <MenuContentItem onClick={copyPath}>
303
+ Copy Path
304
+ </MenuContentItem>
305
+ <MenuContentItem disabled={true}>
306
+ Preview Data
307
+ </MenuContentItem>
308
+ </MenuContent>
309
+ }
310
+ >
311
+ <MoreVerticalIcon />
312
+ </ControlledDropdownMenu>
313
+ </div>
314
+ </div>
315
+ );
316
+ } else if (data.elementEntry instanceof EnumerationDocumentationEntry) {
317
+ return (
318
+ <div
319
+ className="models-documentation__grid__cell"
320
+ title={`Enumeration: ${data.elementEntry.path}`}
321
+ >
322
+ <div className="models-documentation__grid__cell__label">
323
+ <div className="models-documentation__grid__cell__label__icon models-documentation__grid__cell__label__icon--enumeration">
324
+ E
325
+ </div>
326
+ <div className="models-documentation__grid__cell__label__text">
327
+ {label}
328
+ </div>
329
+ </div>
330
+ <div className="models-documentation__grid__cell__actions">
331
+ <ElementInfoTooltip entry={data.elementEntry}>
332
+ <div className="models-documentation__grid__cell__action">
333
+ <InfoCircleIcon className="models-documentation__grid__cell__action__info" />
334
+ </div>
335
+ </ElementInfoTooltip>
336
+ <ControlledDropdownMenu
337
+ className="models-documentation__grid__cell__action"
338
+ menuProps={{
339
+ anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
340
+ transformOrigin: { vertical: 'top', horizontal: 'right' },
341
+ elevation: 7,
342
+ }}
343
+ content={
344
+ <MenuContent>
345
+ <MenuContentItem onClick={copyPath}>
346
+ Copy Path
347
+ </MenuContentItem>
348
+ </MenuContent>
349
+ }
350
+ >
351
+ <MoreVerticalIcon />
352
+ </ControlledDropdownMenu>
353
+ </div>
354
+ </div>
355
+ );
356
+ } else if (data.elementEntry instanceof AssociationDocumentationEntry) {
357
+ return (
358
+ <div
359
+ className="models-documentation__grid__cell"
360
+ title={`Association: ${data.elementEntry.path}`}
361
+ >
362
+ <div className="models-documentation__grid__cell__label">
363
+ <div className="models-documentation__grid__cell__label__icon models-documentation__grid__cell__label__icon--association">
364
+ A
365
+ </div>
366
+ <div className="models-documentation__grid__cell__label__text">
367
+ {label}
368
+ </div>
369
+ </div>
370
+ <div className="models-documentation__grid__cell__actions">
371
+ <ElementInfoTooltip entry={data.elementEntry}>
372
+ <div className="models-documentation__grid__cell__action">
373
+ <InfoCircleIcon className="models-documentation__grid__cell__action__info" />
374
+ </div>
375
+ </ElementInfoTooltip>
376
+ <ControlledDropdownMenu
377
+ className="models-documentation__grid__cell__action"
378
+ menuProps={{
379
+ anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
380
+ transformOrigin: { vertical: 'top', horizontal: 'right' },
381
+ elevation: 7,
382
+ }}
383
+ content={
384
+ <MenuContent>
385
+ <MenuContentItem onClick={copyPath}>
386
+ Copy Path
387
+ </MenuContentItem>
388
+ </MenuContent>
389
+ }
390
+ >
391
+ <MoreVerticalIcon />
392
+ </ControlledDropdownMenu>
393
+ </div>
394
+ </div>
395
+ );
396
+ }
397
+ return null;
398
+ },
399
+ );
400
+
401
+ export const SubElementDocContentCellRenderer = observer(
402
+ (
403
+ params: DataGridCellRendererParams<NormalizedDocumentationEntry> & {
404
+ modelsDocumentationState: ViewerModelsDocumentationState;
405
+ },
406
+ ) => {
407
+ const { data, modelsDocumentationState } = params;
408
+ const applicationStore = useApplicationStore();
409
+ const showHumanizedForm = modelsDocumentationState.showHumanizedForm;
410
+
411
+ if (!data) {
412
+ return null;
413
+ }
414
+
415
+ let label = showHumanizedForm ? prettyCONSTName(data.text) : data.text;
416
+ const isDerivedProperty = label.endsWith('()');
417
+ label = isDerivedProperty ? label.slice(0, -2) : label;
418
+
419
+ if (data.entry instanceof ModelDocumentationEntry) {
420
+ return null;
421
+ } else if (data.entry instanceof PropertyDocumentationEntry) {
422
+ return (
423
+ <div
424
+ className="models-documentation__grid__cell"
425
+ title={`${isDerivedProperty ? 'Derived property' : 'Property'}: ${
426
+ data.elementEntry.path
427
+ }${PROPERTY_ACCESSOR}${data.entry.name}`}
428
+ >
429
+ <div className="models-documentation__grid__cell__label">
430
+ <div className="models-documentation__grid__cell__label__icon models-documentation__grid__cell__label__icon--property">
431
+ P
432
+ </div>
433
+ <div className="models-documentation__grid__cell__label__text">
434
+ {label}
435
+ </div>
436
+ {isDerivedProperty && (
437
+ <div className="models-documentation__grid__cell__label__derived-property-badge">
438
+ ()
439
+ </div>
440
+ )}
441
+ {data.entry.milestoning && (
442
+ <div
443
+ className="models-documentation__grid__cell__label__milestoning-badge"
444
+ title={`Milestoning: ${getMilestoningLabel(
445
+ data.entry.milestoning,
446
+ )}`}
447
+ >
448
+ <ClockIcon />
449
+ </div>
450
+ )}
451
+ </div>
452
+ <div className="models-documentation__grid__cell__actions">
453
+ <PropertyInfoTooltip
454
+ entry={data.entry}
455
+ elementEntry={data.elementEntry}
456
+ >
457
+ <div className="models-documentation__grid__cell__action">
458
+ <InfoCircleIcon className="models-documentation__grid__cell__action__info" />
459
+ </div>
460
+ </PropertyInfoTooltip>
461
+ <ControlledDropdownMenu
462
+ className="models-documentation__grid__cell__action"
463
+ menuProps={{
464
+ anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
465
+ transformOrigin: { vertical: 'top', horizontal: 'right' },
466
+ elevation: 7,
467
+ }}
468
+ content={
469
+ <MenuContent>
470
+ <MenuContentItem disabled={true}>
471
+ Preview Data
472
+ </MenuContentItem>
473
+ </MenuContent>
474
+ }
475
+ >
476
+ <MoreVerticalIcon />
477
+ </ControlledDropdownMenu>
478
+ </div>
479
+ </div>
480
+ );
481
+ } else if (data.entry instanceof BasicDocumentationEntry) {
482
+ const copyValue = (): void => {
483
+ applicationStore.clipboardService
484
+ .copyTextToClipboard(
485
+ data.elementEntry.path + PROPERTY_ACCESSOR + data.entry.name,
486
+ )
487
+ .catch(applicationStore.alertUnhandledError);
488
+ };
489
+ return (
490
+ <div
491
+ className="models-documentation__grid__cell"
492
+ title={`Enum: ${data.elementEntry.path}${PROPERTY_ACCESSOR}${data.entry.name}`}
493
+ >
494
+ <div className="models-documentation__grid__cell__label">
495
+ <div className="models-documentation__grid__cell__label__icon models-documentation__grid__cell__label__icon--enum">
496
+ e
497
+ </div>
498
+ <div className="models-documentation__grid__cell__label__text">
499
+ {label}
500
+ </div>
501
+ </div>
502
+ <div className="models-documentation__grid__cell__actions">
503
+ <ControlledDropdownMenu
504
+ className="models-documentation__grid__cell__action"
505
+ menuProps={{
506
+ anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
507
+ transformOrigin: { vertical: 'top', horizontal: 'right' },
508
+ elevation: 7,
509
+ }}
510
+ content={
511
+ <MenuContent>
512
+ <MenuContentItem onClick={copyValue}>
513
+ Copy Value
514
+ </MenuContentItem>
515
+ </MenuContent>
516
+ }
517
+ >
518
+ <MoreVerticalIcon />
519
+ </ControlledDropdownMenu>
520
+ </div>
521
+ </div>
522
+ );
523
+ }
524
+ return null;
525
+ },
526
+ );
527
+
528
+ export const ElementDocumentationCellRenderer = (
529
+ params: DataGridCellRendererParams<NormalizedDocumentationEntry> & {},
530
+ ): React.ReactNode => {
531
+ const data = params.data;
532
+ if (!data) {
533
+ return null;
534
+ }
535
+ return data.documentation.trim() ? (
536
+ data.documentation
537
+ ) : (
538
+ <div className="models-documentation__grid__empty-cell">
539
+ No documentation provided
540
+ </div>
541
+ );
542
+ };
543
+
544
+ export const ModelsDocumentationGridPanel = observer(
545
+ (props: {
546
+ modelsDocumentationState: ViewerModelsDocumentationState;
547
+ applicationStore: GenericLegendApplicationStore;
548
+ }) => {
549
+ const { modelsDocumentationState, applicationStore } = props;
550
+ const documentationState = modelsDocumentationState;
551
+ const darkMode =
552
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled;
553
+
554
+ return (
555
+ <div
556
+ className={clsx('models-documentation__viewer__grid', {
557
+ 'models-documentation__grid': documentationState.showFilterPanel,
558
+ 'ag-theme-balham': !darkMode,
559
+ 'ag-theme-balham-dark': darkMode,
560
+ })}
561
+ >
562
+ <DataGrid
563
+ rowData={documentationState.filteredSearchResults}
564
+ overlayNoRowsTemplate={`<div class="models-documentation__grid--empty">No documentation found</div>`}
565
+ // highlight element row
566
+ getRowClass={(params) =>
567
+ params.data?.entry instanceof ModelDocumentationEntry
568
+ ? 'models-documentation__grid__element-row'
569
+ : undefined
570
+ }
571
+ alwaysShowVerticalScroll={true}
572
+ gridOptions={{
573
+ suppressScrollOnNewData: true,
574
+ getRowId: (rowData) => rowData.data.uuid,
575
+ }}
576
+ suppressFieldDotNotation={true}
577
+ columnDefs={[
578
+ {
579
+ minWidth: 50,
580
+ sortable: true,
581
+ resizable: true,
582
+ cellRendererParams: {
583
+ modelsDocumentationState,
584
+ applicationStore,
585
+ },
586
+ cellRenderer: ElementContentCellRenderer,
587
+ headerName: 'Model',
588
+ flex: 1,
589
+ },
590
+ {
591
+ minWidth: 50,
592
+ sortable: false,
593
+ resizable: true,
594
+ cellRendererParams: {
595
+ modelsDocumentationState,
596
+ applicationStore,
597
+ },
598
+ cellRenderer: SubElementDocContentCellRenderer,
599
+ headerName: '',
600
+ flex: 1,
601
+ },
602
+ {
603
+ minWidth: 50,
604
+ sortable: false,
605
+ resizable: false,
606
+ headerClass: 'models-documentation__grid__last-column-header',
607
+ cellRenderer: ElementDocumentationCellRenderer,
608
+ headerName: 'Documentation',
609
+ flex: 1,
610
+ wrapText: true,
611
+ autoHeight: true,
612
+ },
613
+ ]}
614
+ />
615
+ </div>
616
+ );
617
+ },
618
+ );
619
+
620
+ export const getFilterTreeNodeIcon = (
621
+ node: ModelsDocumentationFilterTreeNodeData,
622
+ ): React.ReactNode | undefined => {
623
+ if (node instanceof ModelsDocumentationFilterTreeElementNodeData) {
624
+ if (node.typePath === CORE_PURE_PATH.CLASS) {
625
+ return (
626
+ <div className="models-documentation__filter__tree__node__type-icon models-documentation__filter__tree__node__type-icon--class">
627
+ C
628
+ </div>
629
+ );
630
+ } else if (node.typePath === CORE_PURE_PATH.ENUMERATION) {
631
+ return (
632
+ <div className="models-documentation__filter__tree__node__type-icon models-documentation__filter__tree__node__type-icon--enumeration">
633
+ E
634
+ </div>
635
+ );
636
+ } else if (node.typePath === CORE_PURE_PATH.ASSOCIATION) {
637
+ return (
638
+ <div className="models-documentation__filter__tree__node__type-icon models-documentation__filter__tree__node__type-icon--association">
639
+ A
640
+ </div>
641
+ );
642
+ }
643
+ } else if (node instanceof ModelsDocumentationFilterTreePackageNodeData) {
644
+ return (
645
+ <div className="models-documentation__filter__tree__node__type-icon models-documentation__filter__tree__node__type-icon--package">
646
+ <PackageIcon />
647
+ </div>
648
+ );
649
+ } else if (node instanceof ModelsDocumentationFilterTreeTypeNodeData) {
650
+ switch (node.typePath) {
651
+ case CORE_PURE_PATH.CLASS:
652
+ return (
653
+ <div className="models-documentation__filter__tree__node__type-icon models-documentation__filter__tree__node__type-icon--class">
654
+ C
655
+ </div>
656
+ );
657
+ case CORE_PURE_PATH.ENUMERATION:
658
+ return (
659
+ <div className="models-documentation__filter__tree__node__type-icon models-documentation__filter__tree__node__type-icon--enumeration">
660
+ E
661
+ </div>
662
+ );
663
+ case CORE_PURE_PATH.ASSOCIATION:
664
+ return (
665
+ <div className="models-documentation__filter__tree__node__type-icon models-documentation__filter__tree__node__type-icon--association">
666
+ A
667
+ </div>
668
+ );
669
+ default:
670
+ return undefined;
671
+ }
672
+ }
673
+ return undefined;
674
+ };
675
+
676
+ const getFilterNodeCount = (
677
+ node: ModelsDocumentationFilterTreeNodeData,
678
+ documentationState: ViewerModelsDocumentationState,
679
+ ): number | undefined => {
680
+ if (node instanceof ModelsDocumentationFilterTreeElementNodeData) {
681
+ return documentationState.searchResults.filter(
682
+ (result) => node.elementPath === result.elementEntry.path,
683
+ ).length;
684
+ } else if (node instanceof ModelsDocumentationFilterTreePackageNodeData) {
685
+ return documentationState.searchResults.filter(
686
+ (result) =>
687
+ node.packagePath === result.elementEntry.path ||
688
+ result.elementEntry.path.startsWith(
689
+ `${node.packagePath}${ELEMENT_PATH_DELIMITER}`,
690
+ ),
691
+ ).length;
692
+ } else if (node instanceof ModelsDocumentationFilterTreeTypeNodeData) {
693
+ return node.typePath === CORE_PURE_PATH.CLASS
694
+ ? documentationState.searchResults.filter(
695
+ (entry) => entry.elementEntry instanceof ClassDocumentationEntry,
696
+ ).length
697
+ : node.typePath === CORE_PURE_PATH.ENUMERATION
698
+ ? documentationState.searchResults.filter(
699
+ (entry) =>
700
+ entry.elementEntry instanceof EnumerationDocumentationEntry,
701
+ ).length
702
+ : node.typePath === CORE_PURE_PATH.ASSOCIATION
703
+ ? documentationState.searchResults.filter(
704
+ (entry) =>
705
+ entry.elementEntry instanceof AssociationDocumentationEntry,
706
+ ).length
707
+ : undefined;
708
+ } else if (node instanceof ModelsDocumentationFilterTreeRootNodeData) {
709
+ return documentationState.searchResults.length;
710
+ }
711
+ return undefined;
712
+ };
713
+
714
+ const ModelsDocumentationFilterTreeNodeContainer = observer(
715
+ (
716
+ props: TreeNodeContainerProps<
717
+ ModelsDocumentationFilterTreeNodeData,
718
+ {
719
+ documentationState: ViewerModelsDocumentationState;
720
+ refreshTreeData: () => void;
721
+ uncheckTree: () => void;
722
+ updateFilter: () => void;
723
+ }
724
+ >,
725
+ ) => {
726
+ const { node, level, innerProps } = props;
727
+ const { documentationState, refreshTreeData, uncheckTree, updateFilter } =
728
+ innerProps;
729
+ const isExpandable = Boolean(node.childrenIds.length);
730
+ const expandIcon = isExpandable ? (
731
+ node.isOpen ? (
732
+ <ChevronDownIcon />
733
+ ) : (
734
+ <ChevronRightIcon />
735
+ )
736
+ ) : (
737
+ <div />
738
+ );
739
+ const checkerIcon =
740
+ node.checkType === ModelsDocumentationFilterTreeNodeCheckType.CHECKED ? (
741
+ <CheckSquareIcon />
742
+ ) : node.checkType ===
743
+ ModelsDocumentationFilterTreeNodeCheckType.PARTIALLY_CHECKED ? (
744
+ <MinusSquareIcon />
745
+ ) : (
746
+ <EmptySquareIcon />
747
+ );
748
+ const nodeCount = getFilterNodeCount(node, documentationState);
749
+ const toggleChecker: React.MouseEventHandler = (event) => {
750
+ event.stopPropagation();
751
+
752
+ if (
753
+ node.checkType === ModelsDocumentationFilterTreeNodeCheckType.CHECKED
754
+ ) {
755
+ uncheckFilterTreeNode(node);
756
+ } else {
757
+ checkFilterTreeNode(node);
758
+ }
759
+
760
+ refreshTreeData();
761
+ updateFilter();
762
+ };
763
+ const toggleExpandNode: React.MouseEventHandler = (event) => {
764
+ event.stopPropagation();
765
+
766
+ if (isExpandable) {
767
+ node.setIsOpen(!node.isOpen);
768
+ refreshTreeData();
769
+ }
770
+ };
771
+ const onNodeClick = (): void => {
772
+ uncheckTree();
773
+ checkFilterTreeNode(node);
774
+
775
+ if (isExpandable && !node.isOpen) {
776
+ node.setIsOpen(true);
777
+ }
778
+ refreshTreeData();
779
+ updateFilter();
780
+ };
781
+
782
+ return (
783
+ <div
784
+ className="tree-view__node__container models-documentation__filter__tree__node__container"
785
+ style={{
786
+ paddingLeft: `${(level - 1) * 1.4}rem`,
787
+ display: 'flex',
788
+ }}
789
+ onClick={onNodeClick}
790
+ >
791
+ <div
792
+ className="models-documentation__filter__tree__node__expand-icon"
793
+ onClick={toggleExpandNode}
794
+ >
795
+ {expandIcon}
796
+ </div>
797
+ <div
798
+ className="models-documentation__filter__tree__node__checker"
799
+ onClick={toggleChecker}
800
+ >
801
+ {checkerIcon}
802
+ </div>
803
+ {getFilterTreeNodeIcon(node)}
804
+ <div className="tree-view__node__label models-documentation__filter__tree__node__label">
805
+ {node.label}
806
+ </div>
807
+ {nodeCount !== undefined && (
808
+ <div className="tree-view__node__label models-documentation__filter__tree__node__count">
809
+ {nodeCount}
810
+ </div>
811
+ )}
812
+ </div>
813
+ );
814
+ },
815
+ );
816
+
817
+ const ModelsDocumentationFilterPanel = observer(
818
+ (props: { modelsDocumentationState: ViewerModelsDocumentationState }) => {
819
+ const { modelsDocumentationState } = props;
820
+ const documentationState = modelsDocumentationState;
821
+ const resetAll = (): void => documentationState.resetAllFilters();
822
+ const resetTypeFilter = (): void => documentationState.resetTypeFilter();
823
+ const resetPackageFilter = (): void =>
824
+ documentationState.resetPackageFilter();
825
+
826
+ return (
827
+ <div className="models-documentation__filter__panel">
828
+ <div className="models-documentation__filter__group">
829
+ <div className="models-documentation__filter__group__header">
830
+ <div className="models-documentation__filter__group__header__label">
831
+ Filter
832
+ </div>
833
+ <div className="models-documentation__filter__group__header__actions">
834
+ <button
835
+ className="models-documentation__filter__group__header__reset"
836
+ tabIndex={-1}
837
+ disabled={!documentationState.isFilterCustomized}
838
+ onClick={resetAll}
839
+ >
840
+ Reset All
841
+ </button>
842
+ </div>
843
+ </div>
844
+ </div>
845
+ <div className="models-documentation__filter__group models-documentation__filter__group--by-type">
846
+ <div className="models-documentation__filter__group__header">
847
+ <div className="models-documentation__filter__group__header__label">
848
+ Filter by Type
849
+ </div>
850
+ <div className="models-documentation__filter__group__header__actions">
851
+ <button
852
+ className="models-documentation__filter__group__header__reset"
853
+ tabIndex={-1}
854
+ disabled={!documentationState.isTypeFilterCustomized}
855
+ onClick={resetTypeFilter}
856
+ >
857
+ Reset
858
+ </button>
859
+ </div>
860
+ </div>
861
+ <div className="models-documentation__filter__group__content">
862
+ <TreeView
863
+ components={{
864
+ TreeNodeContainer: ModelsDocumentationFilterTreeNodeContainer,
865
+ }}
866
+ treeData={documentationState.typeFilterTreeData}
867
+ getChildNodes={(node) =>
868
+ node.childrenIds
869
+ .map((id) =>
870
+ documentationState.typeFilterTreeData.nodes.get(id),
871
+ )
872
+ .filter(isNonNullable)
873
+ .sort((a, b) => a.label.localeCompare(b.label))
874
+ }
875
+ innerProps={{
876
+ documentationState,
877
+ refreshTreeData: (): void =>
878
+ documentationState.resetTypeFilterTreeData(),
879
+ uncheckTree: (): void =>
880
+ uncheckAllFilterTree(documentationState.typeFilterTreeData),
881
+ updateFilter: (): void => documentationState.updateTypeFilter(),
882
+ }}
883
+ />
884
+ </div>
885
+ </div>
886
+ <div className="models-documentation__filter__group models-documentation__filter__group--by-package">
887
+ <div className="models-documentation__filter__group__header">
888
+ <div className="models-documentation__filter__group__header__label">
889
+ Filter by Package
890
+ </div>
891
+ <div className="models-documentation__filter__group__header__actions">
892
+ <button
893
+ className="models-documentation__filter__group__header__reset"
894
+ tabIndex={-1}
895
+ disabled={!documentationState.isPackageFilterCustomized}
896
+ onClick={resetPackageFilter}
897
+ >
898
+ Reset
899
+ </button>
900
+ </div>
901
+ </div>
902
+ <div className="models-documentation__filter__group__content">
903
+ <TreeView
904
+ components={{
905
+ TreeNodeContainer: ModelsDocumentationFilterTreeNodeContainer,
906
+ }}
907
+ treeData={documentationState.packageFilterTreeData}
908
+ getChildNodes={(node) =>
909
+ node.childrenIds
910
+ .map((id) =>
911
+ documentationState.packageFilterTreeData.nodes.get(id),
912
+ )
913
+ .filter(isNonNullable)
914
+ .sort((a, b) => a.label.localeCompare(b.label))
915
+ }
916
+ innerProps={{
917
+ documentationState,
918
+ refreshTreeData: (): void =>
919
+ documentationState.resetPackageFilterTreeData(),
920
+ uncheckTree: (): void =>
921
+ uncheckAllFilterTree(
922
+ documentationState.packageFilterTreeData,
923
+ ),
924
+ updateFilter: (): void =>
925
+ documentationState.updatePackageFilter(),
926
+ }}
927
+ />
928
+ </div>
929
+ </div>
930
+ </div>
931
+ );
932
+ },
933
+ );
934
+
935
+ const ModelsDocumentationSearchBar = observer(
936
+ (props: { modelsDocumentationState: ViewerModelsDocumentationState }) => {
937
+ const { modelsDocumentationState } = props;
938
+ const searchInputRef = useRef<HTMLInputElement>(null);
939
+ const searchConfigTriggerRef = useRef<HTMLButtonElement>(null);
940
+ const documentationState = modelsDocumentationState;
941
+ const searchText = documentationState.searchText;
942
+ const debouncedSearch = useMemo(
943
+ () => debounce(() => documentationState.search(), 100),
944
+ [documentationState],
945
+ );
946
+ const onSearchTextChange: React.ChangeEventHandler<HTMLInputElement> = (
947
+ event,
948
+ ) => {
949
+ documentationState.setSearchText(event.target.value);
950
+ debouncedSearch.cancel();
951
+ debouncedSearch();
952
+ };
953
+
954
+ // actions
955
+ const clearSearchText = (): void => {
956
+ documentationState.resetSearch();
957
+ documentationState.focusSearchInput();
958
+ };
959
+ const toggleSearchConfigMenu = (): void =>
960
+ documentationState.setShowSearchConfigurationMenu(
961
+ !documentationState.showSearchConfigurationMenu,
962
+ );
963
+ const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
964
+ if (event.code === 'Escape') {
965
+ documentationState.selectSearchInput();
966
+ }
967
+ };
968
+
969
+ // search config menu
970
+ const closeSearchConfigMenu = (): void =>
971
+ documentationState.setShowSearchConfigurationMenu(false);
972
+ const onSearchConfigMenuOpen = (): void =>
973
+ documentationState.focusSearchInput();
974
+
975
+ useEffect(() => {
976
+ if (searchInputRef.current) {
977
+ documentationState.setSearchInput(searchInputRef.current);
978
+ }
979
+ return () => documentationState.setSearchInput(undefined);
980
+ }, [documentationState]);
981
+
982
+ return (
983
+ <div className="models-documentation__search">
984
+ <input
985
+ ref={searchInputRef}
986
+ onKeyDown={onKeyDown}
987
+ className="models-documentation__search__input input"
988
+ spellCheck={false}
989
+ onChange={onSearchTextChange}
990
+ value={searchText}
991
+ placeholder="Search (Ctrl + Shift + F)"
992
+ />
993
+ <button
994
+ ref={searchConfigTriggerRef}
995
+ className={clsx(
996
+ 'models-documentation__search__input__config__trigger',
997
+ {
998
+ 'models-documentation__search__input__config__trigger--toggled':
999
+ documentationState.showSearchConfigurationMenu,
1000
+ 'models-documentation__search__input__config__trigger--active':
1001
+ documentationState.searchConfigurationState
1002
+ .isAdvancedSearchActive,
1003
+ },
1004
+ )}
1005
+ tabIndex={-1}
1006
+ onClick={toggleSearchConfigMenu}
1007
+ title={`${
1008
+ documentationState.searchConfigurationState.isAdvancedSearchActive
1009
+ ? 'Advanced search is currently active\n'
1010
+ : ''
1011
+ }Click to toggle search config menu`}
1012
+ >
1013
+ <CogIcon />
1014
+ </button>
1015
+ <BasePopover
1016
+ open={Boolean(documentationState.showSearchConfigurationMenu)}
1017
+ slotProps={{
1018
+ transition: {
1019
+ onEnter: onSearchConfigMenuOpen,
1020
+ },
1021
+ }}
1022
+ anchorEl={searchConfigTriggerRef.current}
1023
+ onClose={closeSearchConfigMenu}
1024
+ anchorOrigin={{
1025
+ vertical: 'bottom',
1026
+ horizontal: 'center',
1027
+ }}
1028
+ transformOrigin={{
1029
+ vertical: 'top',
1030
+ horizontal: 'center',
1031
+ }}
1032
+ >
1033
+ <FuzzySearchAdvancedConfigMenu
1034
+ configState={documentationState.searchConfigurationState}
1035
+ />
1036
+ </BasePopover>
1037
+ {!searchText ? (
1038
+ <div className="models-documentation__search__input__search__icon">
1039
+ <SearchIcon />
1040
+ </div>
1041
+ ) : (
1042
+ <button
1043
+ className="models-documentation__search__input__clear-btn"
1044
+ tabIndex={-1}
1045
+ onClick={clearSearchText}
1046
+ title="Clear"
1047
+ >
1048
+ <TimesIcon />
1049
+ </button>
1050
+ )}
1051
+ </div>
1052
+ );
1053
+ },
1054
+ );
1055
+
1056
+ const ProductWikiPlaceholder: React.FC<{ message: string }> = (props) => (
1057
+ <div className="models-documentation__viewer__wiki__placeholder">
1058
+ {props.message}
1059
+ </div>
1060
+ );
1061
+
1062
+ export const ModelsDocumentation = observer(
1063
+ (props: {
1064
+ modelsDocumentationState: ViewerModelsDocumentationState;
1065
+ applicationStore: GenericLegendApplicationStore;
1066
+ }) => {
1067
+ const { modelsDocumentationState, applicationStore } = props;
1068
+ const sectionRef = useRef<HTMLDivElement>(null);
1069
+ const elementDocs = modelsDocumentationState.elementDocs;
1070
+
1071
+ useCommands(modelsDocumentationState);
1072
+
1073
+ const toggleFilterPanel = (): void =>
1074
+ modelsDocumentationState.setShowFilterPanel(
1075
+ !modelsDocumentationState.showFilterPanel,
1076
+ );
1077
+
1078
+ return (
1079
+ <div
1080
+ ref={sectionRef}
1081
+ className="models-documentation__viewer__wiki__section"
1082
+ >
1083
+ <div className="models-documentation__viewer__wiki__section__header">
1084
+ <div className="models-documentation__viewer__wiki__section__header__label">
1085
+ Models Documentation
1086
+ <button
1087
+ className="models-documentation__viewer__wiki__section__header__anchor"
1088
+ tabIndex={-1}
1089
+ >
1090
+ <AnchorLinkIcon />
1091
+ </button>
1092
+ </div>
1093
+ </div>
1094
+ <div className="models-documentation__viewer__wiki__section__content">
1095
+ {elementDocs.length > 0 && (
1096
+ <div className="models-documentation">
1097
+ <div className="models-documentation__header">
1098
+ <button
1099
+ className="models-documentation__filter__toggler"
1100
+ title="Toggle Filter Panel"
1101
+ tabIndex={-1}
1102
+ onClick={toggleFilterPanel}
1103
+ >
1104
+ <div className="models-documentation__filter__toggler__arrow">
1105
+ {modelsDocumentationState.showFilterPanel ? (
1106
+ <CaretLeftIcon />
1107
+ ) : (
1108
+ <CaretRightIcon />
1109
+ )}
1110
+ </div>
1111
+ <div className="models-documentation__filter__toggler__icon">
1112
+ <FilterIcon />
1113
+ </div>
1114
+ </button>
1115
+ <ModelsDocumentationSearchBar
1116
+ modelsDocumentationState={modelsDocumentationState}
1117
+ />
1118
+ </div>
1119
+ <div className="models-documentation__content">
1120
+ {modelsDocumentationState.showFilterPanel && (
1121
+ <ModelsDocumentationFilterPanel
1122
+ modelsDocumentationState={modelsDocumentationState}
1123
+ />
1124
+ )}
1125
+ <ModelsDocumentationGridPanel
1126
+ modelsDocumentationState={modelsDocumentationState}
1127
+ applicationStore={applicationStore}
1128
+ />
1129
+ </div>
1130
+ </div>
1131
+ )}
1132
+ {elementDocs.length === 0 && (
1133
+ <ProductWikiPlaceholder message="(not specified)" />
1134
+ )}
1135
+ </div>
1136
+ </div>
1137
+ );
1138
+ },
1139
+ );