@myrmidon/cadmus-graph-ui
Version:
Cadmus - semantic graph components.
1 lines • 116 kB
Source Map (JSON)
{"version":3,"file":"myrmidon-cadmus-graph-ui.mjs","sources":["../../../../projects/myrmidon/cadmus-graph-ui/src/lib/services/graph-node-lookup.service.ts","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/state/graph-node-list.repository.ts","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/components/graph-node-filter/graph-node-filter.component.ts","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/components/graph-node-filter/graph-node-filter.component.html","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/components/graph-node-editor/graph-node-editor.component.ts","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/components/graph-node-editor/graph-node-editor.component.html","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/components/graph-node-list/graph-node-list.component.ts","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/components/graph-node-list/graph-node-list.component.html","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/state/graph-triple-list.repository.ts","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/components/graph-triple-filter/graph-triple-filter.component.ts","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/components/graph-triple-filter/graph-triple-filter.component.html","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/components/graph-triple-editor/graph-triple-editor.component.ts","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/components/graph-triple-editor/graph-triple-editor.component.html","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/components/graph-triple-list/graph-triple-list.component.ts","../../../../projects/myrmidon/cadmus-graph-ui/src/lib/components/graph-triple-list/graph-triple-list.component.html","../../../../projects/myrmidon/cadmus-graph-ui/src/public-api.ts","../../../../projects/myrmidon/cadmus-graph-ui/src/myrmidon-cadmus-graph-ui.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\r\nimport { map, Observable, of } from 'rxjs';\r\n\r\nimport {\r\n RefLookupFilter,\r\n RefLookupService,\r\n} from '@myrmidon/cadmus-refs-lookup';\r\nimport { GraphService, UriNode } from '@myrmidon/cadmus-api';\r\n\r\nexport interface GraphNodeLookupFilter extends RefLookupFilter {\r\n isClass?: boolean | null;\r\n tag?: string;\r\n}\r\n\r\n/**\r\n * Graph node lookup service.\r\n */\r\n@Injectable({\r\n providedIn: 'root',\r\n})\r\nexport class GraphNodeLookupService implements RefLookupService {\r\n public readonly id = 'graph-node';\r\n\r\n constructor(private _graphService: GraphService) {}\r\n\r\n getById(id: string): Observable<any | undefined> {\r\n return this._graphService.getNode(parseInt(id, 10));\r\n }\r\n\r\n lookup(filter: GraphNodeLookupFilter, options?: any): Observable<any[]> {\r\n if (!filter.text) {\r\n return of([]);\r\n }\r\n return this._graphService\r\n .getNodes(1, filter.limit || 10, {\r\n label: filter.text,\r\n isClass:\r\n filter.isClass === undefined || filter.isClass === null\r\n ? undefined\r\n : filter.isClass\r\n ? true\r\n : false,\r\n tag: filter.tag,\r\n })\r\n .pipe(map((p) => p.items));\r\n }\r\n\r\n getName(item: any): string {\r\n return (item as UriNode)?.label;\r\n }\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { BehaviorSubject, forkJoin, Observable, tap } from 'rxjs';\r\n\r\nimport { NodeFilter, UriNode } from '@myrmidon/cadmus-api';\r\nimport {\r\n PagedListStore,\r\n PagedListStoreService,\r\n} from '@myrmidon/paged-data-browsers';\r\nimport { DataPage } from '@myrmidon/ngx-tools';\r\nimport { GraphService } from '@myrmidon/cadmus-api';\r\n\r\n/**\r\n * Graph nodes list repository.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class NodeListRepository\r\n implements PagedListStoreService<NodeFilter, UriNode>\r\n{\r\n private readonly _store: PagedListStore<NodeFilter, UriNode>;\r\n private readonly _loading$: BehaviorSubject<boolean | undefined>;\r\n private readonly _filter$: BehaviorSubject<NodeFilter>;\r\n private readonly _linkedNode$: BehaviorSubject<UriNode | undefined>;\r\n private readonly _classNodes$: BehaviorSubject<UriNode[]>;\r\n\r\n public get loading$(): Observable<boolean | undefined> {\r\n return this._loading$.asObservable();\r\n }\r\n public get filter$(): Observable<NodeFilter> {\r\n return this._filter$.asObservable();\r\n }\r\n public get page$(): Observable<DataPage<UriNode>> {\r\n return this._store.page$;\r\n }\r\n public get linkedNode$(): Observable<UriNode | undefined> {\r\n return this._linkedNode$.asObservable();\r\n }\r\n public get classNodes$(): Observable<UriNode[] | undefined> {\r\n return this._classNodes$.asObservable();\r\n }\r\n\r\n constructor(private _graphService: GraphService) {\r\n this._store = new PagedListStore<NodeFilter, UriNode>(this);\r\n this._loading$ = new BehaviorSubject<boolean | undefined>(undefined);\r\n this._filter$ = new BehaviorSubject<NodeFilter>({});\r\n this._linkedNode$ = new BehaviorSubject<UriNode | undefined>(undefined);\r\n this._classNodes$ = new BehaviorSubject<UriNode[]>([]);\r\n this._store.reset();\r\n }\r\n\r\n public getLinkedNode(): UriNode | undefined {\r\n return this._linkedNode$.value;\r\n }\r\n\r\n public getClassNodes(): UriNode[] {\r\n return this._classNodes$.value;\r\n }\r\n\r\n public loadPage(\r\n pageNumber: number,\r\n pageSize: number,\r\n filter: NodeFilter\r\n ): Observable<DataPage<UriNode>> {\r\n this._loading$.next(true);\r\n return this._graphService.getNodes(pageNumber, pageSize, filter).pipe(\r\n tap({\r\n next: () => this._loading$.next(false),\r\n error: () => this._loading$.next(false),\r\n })\r\n );\r\n }\r\n\r\n public async reset(): Promise<void> {\r\n this._loading$.next(true);\r\n try {\r\n await this._store.reset();\r\n } catch (error) {\r\n throw error;\r\n } finally {\r\n this._loading$.next(false);\r\n }\r\n }\r\n\r\n public async setFilter(filter: NodeFilter): Promise<void> {\r\n this._loading$.next(true);\r\n try {\r\n await this._store.setFilter(filter);\r\n } catch (error) {\r\n throw error;\r\n } finally {\r\n this._loading$.next(false);\r\n }\r\n }\r\n\r\n public getFilter(): NodeFilter {\r\n return this._store.getFilter();\r\n }\r\n\r\n public async setPage(pageNumber: number, pageSize: number): Promise<void> {\r\n this._loading$.next(true);\r\n try {\r\n await this._store.setPage(pageNumber, pageSize);\r\n } catch (error) {\r\n throw error;\r\n } finally {\r\n this._loading$.next(false);\r\n }\r\n }\r\n\r\n /**\r\n * Set the linked node used in filter.\r\n *\r\n * @param node The node or undefined.\r\n */\r\n public setLinkedNode(node?: UriNode): void {\r\n this._linkedNode$.next(node);\r\n }\r\n\r\n /**\r\n * Set the linked node used in filter by its ID.\r\n *\r\n * @param id The node ID.\r\n */\r\n public setLinkedNodeId(id?: number): void {\r\n if (!id) {\r\n this._linkedNode$.next(undefined);\r\n } else {\r\n this._graphService.getNode(id).subscribe({\r\n next: (node) => {\r\n this._linkedNode$.next(node);\r\n },\r\n error: (error) => {\r\n console.error(`Node ID ${id} not found`, error);\r\n console.warn('Node ID not found: ' + id);\r\n },\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Add the specified node to the filter class nodes.\r\n * If the node already exists, nothing is done.\r\n *\r\n * @param node The node to add.\r\n */\r\n public addClassNode(node: UriNode): void {\r\n const nodes = [...this._classNodes$.value];\r\n if (nodes.some((n) => n.id === node.id)) {\r\n return;\r\n }\r\n nodes.push(node);\r\n this._classNodes$.next(nodes);\r\n }\r\n\r\n /**\r\n * Set the class node IDs in the filter.\r\n *\r\n * @param ids The class nodes IDs or undefined.\r\n */\r\n public setClassNodeIds(ids?: number[]): void {\r\n if (!ids || !ids.length) {\r\n this._classNodes$.next([]);\r\n return;\r\n }\r\n\r\n const requests: Observable<UriNode>[] = [];\r\n ids.forEach((id) => {\r\n requests.push(this._graphService.getNode(id));\r\n });\r\n forkJoin(requests).subscribe({\r\n next: (nodes: UriNode[]) => {\r\n this._classNodes$.next(nodes);\r\n },\r\n error: (error) => {\r\n console.error('Error getting nodes', error);\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Delete the specified node from the filter class nodes.\r\n * If the node does not exist, nothing is done.\r\n *\r\n * @param id The node's ID.\r\n */\r\n public deleteClassNode(id: number): void {\r\n const nodes = [...this._classNodes$.value];\r\n const i = nodes.findIndex((n) => n.id === id);\r\n if (i > -1) {\r\n nodes.splice(i, 1);\r\n this._classNodes$.next(nodes);\r\n }\r\n }\r\n}\r\n","import { Component, input, OnDestroy, OnInit } from '@angular/core';\nimport {\n FormBuilder,\n FormControl,\n FormGroup,\n FormsModule,\n ReactiveFormsModule,\n} from '@angular/forms';\nimport { AsyncPipe } from '@angular/common';\nimport { Observable, Subscription } from 'rxjs';\n\nimport { MatFormField, MatLabel } from '@angular/material/form-field';\nimport { MatInput } from '@angular/material/input';\nimport { MatSelect } from '@angular/material/select';\nimport { MatOption } from '@angular/material/core';\nimport { MatCheckbox } from '@angular/material/checkbox';\nimport { MatIconButton } from '@angular/material/button';\nimport { MatTooltip } from '@angular/material/tooltip';\nimport { MatIcon } from '@angular/material/icon';\nimport {\n MatChipListbox,\n MatChipOption,\n MatChipRemove,\n} from '@angular/material/chips';\n\nimport { RefLookupComponent } from '@myrmidon/cadmus-refs-lookup';\n\nimport { NodeFilter, UriNode } from '@myrmidon/cadmus-api';\n\nimport { NodeListRepository } from '../../state/graph-node-list.repository';\nimport { GraphNodeLookupService } from '../../services/graph-node-lookup.service';\n\n/**\n * Graph nodes filter used in graph nodes list.\n * Its data are in the graph nodes store, which gets updated when\n * users apply new filters.\n */\n@Component({\n selector: 'cadmus-graph-node-filter',\n templateUrl: './graph-node-filter.component.html',\n styleUrls: ['./graph-node-filter.component.css'],\n imports: [\n FormsModule,\n ReactiveFormsModule,\n MatFormField,\n MatLabel,\n MatInput,\n MatSelect,\n MatOption,\n MatCheckbox,\n RefLookupComponent,\n MatIconButton,\n MatTooltip,\n MatIcon,\n MatChipListbox,\n MatChipOption,\n MatChipRemove,\n AsyncPipe,\n ],\n})\nexport class GraphNodeFilterComponent implements OnInit, OnDestroy {\n private _sub?: Subscription;\n public filter$: Observable<NodeFilter>;\n public linkedNode$: Observable<UriNode | undefined>;\n public classNodes$: Observable<UriNode[] | undefined>;\n\n public readonly disabled = input<boolean>();\n\n public label: FormControl<string | null>;\n public isClass: FormControl<number>;\n public uid: FormControl<string | null>;\n public tag: FormControl<string | null>;\n public sourceType: FormControl<number | null>;\n public sid: FormControl<string | null>;\n public sidPrefix: FormControl<boolean>;\n public linkedNodeRole: FormControl<'S' | 'O' | null>;\n public form: FormGroup;\n\n constructor(\n formBuilder: FormBuilder,\n public lookupService: GraphNodeLookupService,\n private _repository: NodeListRepository\n ) {\n this.filter$ = _repository.filter$;\n this.linkedNode$ = _repository.linkedNode$;\n this.classNodes$ = _repository.classNodes$;\n // form\n this.label = formBuilder.control(null);\n this.isClass = formBuilder.control(0, { nonNullable: true });\n this.uid = formBuilder.control(null);\n this.tag = formBuilder.control(null);\n this.sourceType = formBuilder.control(null);\n this.sid = formBuilder.control(null);\n this.sidPrefix = formBuilder.control(false, { nonNullable: true });\n this.linkedNodeRole = formBuilder.control(null);\n this.form = formBuilder.group({\n label: this.label,\n isClass: this.isClass,\n uid: this.uid,\n tag: this.tag,\n sourceType: this.sourceType,\n sid: this.sid,\n sidPrefix: this.sidPrefix,\n linkedNodeRole: this.linkedNodeRole,\n });\n }\n\n public ngOnInit(): void {\n this._sub = this.filter$.subscribe((f) => {\n this.updateForm(f);\n });\n }\n\n public ngOnDestroy(): void {\n this._sub?.unsubscribe();\n }\n\n private updateForm(filter: NodeFilter): void {\n this.label.setValue(filter.label || null);\n // is-class: 0=unset, 1=class, 2=not-class\n if (filter.isClass !== undefined && filter.isClass !== null) {\n this.isClass.setValue(filter.isClass ? 1 : 2);\n } else {\n this.isClass.setValue(0);\n }\n this.uid.setValue(filter.uid || null);\n this.tag.setValue(filter.tag || null);\n if (filter.sourceType === undefined || filter.sourceType === null) {\n this.sourceType.setValue(null);\n } else {\n this.sourceType.setValue(filter.sourceType);\n }\n this.sid.setValue(filter.sid || null);\n this.sidPrefix.setValue(filter.isSidPrefix ? true : false);\n this._repository.setLinkedNodeId(filter.linkedNodeId);\n this.linkedNodeRole.setValue(filter.linkedNodeRole || 'S');\n this._repository.setClassNodeIds(filter.classIds);\n this.form.markAsPristine();\n }\n\n private getFilter(): NodeFilter {\n return {\n label: this.label.value?.trim(),\n isClass: this.isClass.value === 0 ? undefined : this.isClass.value === 1,\n uid: this.uid.value?.trim(),\n tag: this.tag.value?.trim(),\n sourceType:\n this.sourceType.value === null ? undefined : this.sourceType.value,\n sid: this.sid.value?.trim(),\n isSidPrefix: this.sidPrefix.value,\n linkedNodeId: this._repository.getLinkedNode()?.id,\n linkedNodeRole: this.linkedNodeRole.value || undefined,\n classIds: this._repository.getClassNodes()?.map((n) => n.id),\n };\n }\n\n public onResetLinkedNode(): void {\n this._repository.setLinkedNode();\n }\n\n public onLinkedNodeSet(node: unknown): void {\n this._repository.setLinkedNode((node as UriNode) || undefined);\n }\n\n public clearLinkedNode(): void {\n this._repository.setLinkedNode();\n }\n\n public onClassAdd(node: unknown): void {\n if (node) {\n this._repository.addClassNode(node as UriNode);\n }\n }\n\n public onClassRemove(id: number): void {\n this._repository.deleteClassNode(id);\n }\n\n public reset(): void {\n this.form.reset();\n this.apply();\n }\n\n public apply(): void {\n if (this.form.invalid) {\n return;\n }\n const filter = this.getFilter();\n\n // update filter in state\n this._repository.setFilter(filter);\n }\n}\n","<form\r\n [formGroup]=\"form\"\r\n (submit)=\"apply()\"\r\n [attr.disabled]=\"disabled() ? true : null\"\r\n>\r\n <div class=\"form-row\">\r\n <!-- label -->\r\n <div>\r\n <mat-form-field>\r\n <mat-label>label</mat-label>\r\n <input matInput [formControl]=\"label\" />\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- uid -->\r\n <div>\r\n <mat-form-field>\r\n <mat-label>uid</mat-label>\r\n <input matInput [formControl]=\"uid\" />\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- isClass -->\r\n <div>\r\n <mat-form-field>\r\n <mat-label>class</mat-label>\r\n <mat-select [formControl]=\"isClass\">\r\n <mat-option [value]=\"0\">(any)</mat-option>\r\n <mat-option [value]=\"1\">not-class</mat-option>\r\n <mat-option [value]=\"2\">class</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- tag -->\r\n <div>\r\n <mat-form-field>\r\n <mat-label>tag</mat-label>\r\n <input matInput [formControl]=\"tag\" />\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- sourceType -->\r\n <div>\r\n <mat-form-field>\r\n <mat-label>source type</mat-label>\r\n <mat-select [formControl]=\"sourceType\">\r\n <mat-option [value]=\"null\">(any)</mat-option>\r\n <mat-option [value]=\"0\">user</mat-option>\r\n <mat-option [value]=\"1\">item</mat-option>\r\n <mat-option [value]=\"2\">part</mat-option>\r\n <mat-option [value]=\"3\">thesaurus</mat-option>\r\n <mat-option [value]=\"4\">implicit</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n <!-- sid, sidPrefix -->\r\n <div>\r\n <mat-form-field>\r\n <mat-label>source ID</mat-label>\r\n <input matInput [formControl]=\"sid\" />\r\n </mat-form-field>\r\n \r\n <mat-checkbox [formControl]=\"sidPrefix\">prefix</mat-checkbox>\r\n </div>\r\n\r\n <!-- linkedNode, linkedNodeRole -->\r\n <div>\r\n <fieldset>\r\n <legend>linked node</legend>\r\n <cadmus-refs-lookup\r\n label=\"node\"\r\n [item]=\"linkedNode$ | async\"\r\n [service]=\"lookupService\"\r\n (itemChange)=\"onLinkedNodeSet($event)\"\r\n />\r\n @if (linkedNode$ | async) {\r\n <div>\r\n <mat-form-field>\r\n <mat-label>role</mat-label>\r\n <mat-select [formControl]=\"linkedNodeRole\">\r\n <mat-option value=\"S\">subject</mat-option>\r\n <mat-option value=\"O\">object</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Reset linked node\"\r\n (click)=\"clearLinkedNode()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n </div>\r\n }\r\n </fieldset>\r\n </div>\r\n\r\n <!-- classes -->\r\n <div>\r\n <fieldset>\r\n <legend>classes</legend>\r\n <cadmus-refs-lookup\r\n label=\"class\"\r\n [service]=\"lookupService\"\r\n [baseFilter]=\"{ isClass: true }\"\r\n (itemChange)=\"onClassAdd($event)\"\r\n />\r\n @if (classNodes$ | async; as classNodes) {\r\n <mat-chip-listbox>\r\n @for (node of classNodes; track node.id) {\r\n <mat-chip-option\r\n [removable]=\"true\"\r\n (removed)=\"onClassRemove(node.id)\"\r\n matTooltip=\"{{ node.uri }}\"\r\n >{{ node.label }}\r\n <button type=\"button\" matChipRemove>\r\n <mat-icon>clear</mat-icon>\r\n </button>\r\n </mat-chip-option>\r\n }\r\n </mat-chip-listbox>\r\n }\r\n </fieldset>\r\n </div>\r\n </div>\r\n\r\n <div id=\"toolbar\" class=\"btn-group\" role=\"group\" aria-label=\"toolbar\">\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"reset()\"\r\n matTooltip=\"Reset filters\"\r\n [disabled]=\"disabled()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n mat-icon-button\r\n [disabled]=\"disabled()\"\r\n matTooltip=\"Apply filters\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n","import { Component, effect, input, model, output, signal } from '@angular/core';\r\nimport {\r\n FormBuilder,\r\n FormControl,\r\n FormGroup,\r\n Validators,\r\n FormsModule,\r\n ReactiveFormsModule,\r\n} from '@angular/forms';\r\n\r\nimport { MatFormField, MatLabel, MatError } from '@angular/material/form-field';\r\nimport { MatInput } from '@angular/material/input';\r\nimport { MatCheckbox } from '@angular/material/checkbox';\r\nimport { MatSelect } from '@angular/material/select';\r\nimport { MatOption } from '@angular/material/core';\r\nimport { MatIconButton } from '@angular/material/button';\r\nimport { MatIcon } from '@angular/material/icon';\r\n\r\nimport { ThesaurusEntry } from '@myrmidon/cadmus-core';\r\nimport { UriNode, NodeSourceType } from '@myrmidon/cadmus-api';\r\n\r\n/**\r\n * Graph node editor.\r\n */\r\n@Component({\r\n selector: 'cadmus-graph-node-editor',\r\n templateUrl: './graph-node-editor.component.html',\r\n styleUrls: ['./graph-node-editor.component.css'],\r\n imports: [\r\n FormsModule,\r\n ReactiveFormsModule,\r\n MatFormField,\r\n MatLabel,\r\n MatInput,\r\n MatError,\r\n MatCheckbox,\r\n MatSelect,\r\n MatOption,\r\n MatIconButton,\r\n MatIcon,\r\n ],\r\n})\r\nexport class GraphNodeEditorComponent {\r\n /**\r\n * The node being edited. A new node has ID=0 and no uri.\r\n */\r\n public readonly node = model<UriNode>();\r\n\r\n /**\r\n * The optional set of thesaurus entries for node's tags.\r\n */\r\n public readonly tagEntries = input<ThesaurusEntry[]>();\r\n\r\n /**\r\n * Emitted when the user requested to close the editor.\r\n */\r\n public readonly editorClose = output();\r\n\r\n public readonly isNew = signal<boolean>(true);\r\n\r\n public uri: FormControl<string | null>;\r\n public label: FormControl<string | null>;\r\n public isClass: FormControl<boolean>;\r\n public tag: FormControl<string | null>;\r\n public form: FormGroup;\r\n\r\n constructor(formBuilder: FormBuilder) {\r\n // form\r\n this.uri = formBuilder.control(null, [\r\n Validators.required,\r\n Validators.maxLength(500),\r\n ]);\r\n this.label = formBuilder.control(null, [\r\n Validators.required,\r\n Validators.maxLength(500),\r\n ]);\r\n this.isClass = formBuilder.control(false, { nonNullable: true });\r\n this.tag = formBuilder.control(null, Validators.maxLength(50));\r\n this.form = formBuilder.group({\r\n uri: this.uri,\r\n label: this.label,\r\n isClass: this.isClass,\r\n tag: this.tag,\r\n });\r\n\r\n effect(() => {\r\n this.updateForm(this.node());\r\n });\r\n }\r\n\r\n private updateForm(node?: UriNode): void {\r\n if (!node) {\r\n this.form.reset();\r\n this.isNew.set(true);\r\n return;\r\n }\r\n this.uri.setValue(node.uri);\r\n this.label.setValue(node.label);\r\n this.isClass.setValue(node.isClass ? true : false);\r\n this.tag.setValue(node.tag || null);\r\n this.isNew.set(node.id ? false : true);\r\n this.form.markAsPristine();\r\n }\r\n\r\n private getNode(): UriNode {\r\n return {\r\n id: this.node()?.id || 0,\r\n sourceType: this.node()?.sourceType || NodeSourceType.User,\r\n uri: this.uri.value?.trim() || '',\r\n label: this.label.value?.trim() || '',\r\n isClass: this.isClass.value,\r\n tag: this.tag.value?.trim(),\r\n };\r\n }\r\n\r\n public cancel(): void {\r\n this.editorClose.emit();\r\n }\r\n\r\n public save(): void {\r\n if (this.form.invalid) {\r\n return;\r\n }\r\n this.node.set(this.getNode());\r\n }\r\n}\r\n","<form [formGroup]=\"form\" (submit)=\"save()\">\r\n <!-- uri (readonly if not new) -->\r\n <div class=\"form-row\">\r\n @if (isNew()) {\r\n <mat-form-field>\r\n <mat-label>uri</mat-label>\r\n <input matInput [formControl]=\"uri\" />\r\n @if ($any(uri).errors?.required && (uri.dirty || uri.touched)) {\r\n <mat-error>uri required</mat-error>\r\n } @if ($any(uri).errors?.maxLength && (uri.dirty || uri.touched)) {\r\n <mat-error>uri too long</mat-error>\r\n }\r\n </mat-form-field>\r\n } @else {\r\n <span style=\"color: silver\">{{ uri.value }} </span>\r\n }\r\n\r\n <!-- label -->\r\n <mat-form-field>\r\n <mat-label>label</mat-label>\r\n <input matInput [formControl]=\"label\" />\r\n @if ($any(label).errors?.required && (label.dirty || label.touched)) {\r\n <mat-error>label required</mat-error>\r\n } @if ($any(label).errors?.maxLength && (label.dirty || label.touched)) {\r\n <mat-error>label too long</mat-error>\r\n }\r\n </mat-form-field>\r\n\r\n <!-- class -->\r\n <mat-checkbox [formControl]=\"isClass\">is class</mat-checkbox>\r\n\r\n <!-- tag -->\r\n @if (tagEntries()?.length) {\r\n <mat-form-field>\r\n <mat-label>tag</mat-label>\r\n <mat-select [formControl]=\"tag\">\r\n <mat-option [value]=\"null\">(no tag)</mat-option>\r\n @for (e of tagEntries(); track e.id) {\r\n <mat-option [value]=\"e.id\">{{ e.value }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n } @else {\r\n <mat-form-field>\r\n <mat-label>tag</mat-label>\r\n <input matInput [formControl]=\"tag\" />\r\n @if ($any(tag).errors?.maxLength && (tag.dirty || tag.touched)) {\r\n <mat-error>tag too long</mat-error>\r\n }\r\n </mat-form-field>\r\n }\r\n </div>\r\n\r\n <!-- buttons -->\r\n <div>\r\n <button mat-icon-button type=\"button\" (click)=\"cancel()\">\r\n <mat-icon class=\"mat-warn\">cancel</mat-icon>\r\n </button>\r\n \r\n <button mat-icon-button type=\"submit\" [disabled]=\"form.invalid\">\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n","import { Component, input, model, output } from '@angular/core';\nimport { Observable } from 'rxjs';\nimport { AsyncPipe } from '@angular/common';\nimport { take } from 'rxjs/operators';\n\nimport { PageEvent, MatPaginator } from '@angular/material/paginator';\nimport { MatSnackBar } from '@angular/material/snack-bar';\nimport { MatCard, MatCardContent } from '@angular/material/card';\nimport { MatProgressBar } from '@angular/material/progress-bar';\nimport { MatIconButton, MatButton } from '@angular/material/button';\nimport { MatTooltip } from '@angular/material/tooltip';\nimport { MatIcon } from '@angular/material/icon';\nimport {\n MatExpansionPanel,\n MatExpansionPanelHeader,\n} from '@angular/material/expansion';\n\nimport { DialogService } from '@myrmidon/ngx-mat-tools';\nimport { DataPage } from '@myrmidon/ngx-tools';\n\nimport { ThesaurusEntry } from '@myrmidon/cadmus-core';\nimport { GraphService, UriNode, NodeSourceType } from '@myrmidon/cadmus-api';\n\nimport { NodeListRepository } from '../../state/graph-node-list.repository';\nimport { GraphNodeFilterComponent } from '../graph-node-filter/graph-node-filter.component';\nimport { GraphNodeEditorComponent } from '../graph-node-editor/graph-node-editor.component';\n\n/**\n * List of graph nodes. This includes a graph node filter, a list, and a graph\n * editor.\n */\n@Component({\n selector: 'cadmus-graph-node-list',\n templateUrl: './graph-node-list.component.html',\n styleUrls: ['./graph-node-list.component.css'],\n imports: [\n MatCard,\n MatCardContent,\n GraphNodeFilterComponent,\n MatProgressBar,\n MatIconButton,\n MatTooltip,\n MatIcon,\n MatButton,\n MatPaginator,\n MatExpansionPanel,\n MatExpansionPanelHeader,\n GraphNodeEditorComponent,\n AsyncPipe,\n ],\n})\nexport class GraphNodeListComponent {\n public loading$: Observable<boolean | undefined>;\n public page$: Observable<DataPage<UriNode>>;\n\n /**\n * The currently edited node if any.\n */\n public readonly editedNode = model<UriNode>();\n\n /**\n * The optional set of thesaurus entries for node's tags.\n */\n public readonly tagEntries = input<ThesaurusEntry[]>();\n\n /**\n * True if this node list should have a walker button for each node.\n */\n public readonly hasWalker = input<boolean>();\n\n /**\n * Emitted when walking a specific node is requested.\n */\n public readonly nodeWalk = output<UriNode>();\n\n constructor(\n private _repository: NodeListRepository,\n private _graphService: GraphService,\n private _dialogService: DialogService,\n private _snackbar: MatSnackBar\n ) {\n this.loading$ = _repository.loading$;\n this.page$ = _repository.page$;\n }\n\n public onPageChange(event: PageEvent): void {\n this._repository.setPage(event.pageIndex + 1, event.pageSize);\n }\n\n public addNode(): void {\n this.editedNode.set({\n uri: '',\n id: 0,\n sourceType: NodeSourceType.User,\n label: '',\n });\n }\n\n public editNode(node: UriNode): void {\n this.editedNode.set(node);\n }\n\n public onNodeChange(node: UriNode): void {\n this._graphService.addNode(node).subscribe({\n next: (n) => {\n this.editedNode.set(undefined);\n this._repository.reset();\n this._snackbar.open('Node saved', 'OK', {\n duration: 1500,\n });\n },\n error: (error) => {\n console.error('Error saving node', error);\n this._snackbar.open('Error saving node', 'OK');\n },\n });\n }\n\n public onEditorClose(): void {\n this.editedNode.set(undefined);\n }\n\n public deleteNode(node: UriNode): void {\n this._dialogService\n .confirm('Delete Node', 'Delete node ' + node.label + '?')\n .pipe(take(1))\n .subscribe((yes) => {\n if (yes) {\n this._graphService\n .deleteNode(node.id)\n .pipe(take(1))\n .subscribe({\n next: (_) => {\n this._repository.reset();\n },\n error: (error) => {\n console.error('Error deleting node', error);\n this._snackbar.open('Error deleting node', 'OK');\n },\n });\n }\n });\n }\n\n public walkNode(node: UriNode): void {\n this.nodeWalk.emit(node);\n }\n\n public reset(): void {\n this._repository.reset();\n }\n}\n","<div id=\"container\">\n <!-- filters -->\n <div id=\"filters\">\n <mat-card appearance=\"outlined\">\n <mat-card-content>\n <cadmus-graph-node-filter\n [disabled]=\"(loading$ | async) ? true : false\"\n />\n </mat-card-content>\n </mat-card>\n </div>\n\n <!-- list -->\n @if (page$ | async; as page) {\n <div id=\"list\">\n @if (loading$ | async) {\n <div>\n <mat-progress-bar mode=\"indeterminate\"></mat-progress-bar>\n </div>\n }\n <!-- list -->\n @if (page) {\n <div>\n <table>\n <thead>\n <th></th>\n <th>id</th>\n <th>cls</th>\n <th>label</th>\n <th>uri</th>\n <th class=\"noif-lt-md\">srct</th>\n <th class=\"noif-lt-md\">sid</th>\n <th class=\"noif-lt-md\">tag</th>\n </thead>\n <tbody>\n @for (d of page.items; track d.id) {\n <tr [class.selected]=\"d === editedNode()\">\n <td class=\"fit-width\">\n @if (hasWalker()) {\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Walk node\"\n (click)=\"walkNode(d)\"\n >\n <mat-icon class=\"mat-primary\">flare</mat-icon>\n </button>\n }\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Edit node\"\n (click)=\"editNode(d)\"\n >\n <mat-icon class=\"mat-primary\">edit</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Delete node\"\n (click)=\"deleteNode(d)\"\n >\n <mat-icon class=\"mat-warn\">delete</mat-icon>\n </button>\n </td>\n <td>{{ d.id }}</td>\n <td>{{ d.isClass ? \"C\" : \"\" }}</td>\n <td>{{ d.label }}</td>\n <td>{{ d.uri }}</td>\n <td class=\"noif-lt-md\">{{ \"UIPTX\"[d.sourceType] }}</td>\n <td class=\"noif-lt-md\">{{ d.sid }}</td>\n <td class=\"noif-lt-md\">{{ d.tag }}</td>\n </tr>\n }\n </tbody>\n </table>\n <div>\n <button\n class=\"mat-primary\"\n type=\"button\"\n mat-flat-button\n (click)=\"addNode()\"\n >\n <mat-icon>add_circle</mat-icon>\n add node\n </button>\n </div>\n <!-- pagination -->\n <div class=\"form-row\">\n <button\n type=\"button\"\n mat-icon-button\n matTooltip=\"Reset list\"\n (click)=\"reset()\"\n >\n <mat-icon class=\"mat-warn\">autorenew</mat-icon>\n </button>\n <mat-paginator\n [length]=\"page.total\"\n [pageIndex]=\"page.pageNumber - 1\"\n [pageSize]=\"page.pageSize\"\n [pageSizeOptions]=\"[5, 10, 20, 50, 100]\"\n (page)=\"onPageChange($event)\"\n [showFirstLastButtons]=\"true\"\n />\n </div>\n </div>\n }\n <!-- editor -->\n <mat-expansion-panel\n [expanded]=\"editedNode() ? true : false\"\n [disabled]=\"editedNode() ? false : true\"\n id=\"editor\"\n >\n <mat-expansion-panel-header>Node</mat-expansion-panel-header>\n <cadmus-graph-node-editor\n [node]=\"editedNode()\"\n [tagEntries]=\"tagEntries()\"\n (nodeChange)=\"onNodeChange($event!)\"\n (editorClose)=\"onEditorClose()\"\n />\n </mat-expansion-panel>\n </div>\n }\n</div>\n","import { Injectable } from '@angular/core';\r\nimport { BehaviorSubject, Observable, tap } from 'rxjs';\r\n\r\nimport { DataPage } from '@myrmidon/ngx-tools';\r\nimport {\r\n PagedListStore,\r\n PagedListStoreService,\r\n} from '@myrmidon/paged-data-browsers';\r\n\r\nimport { UriNode, TripleFilter, UriTriple } from '@myrmidon/cadmus-api';\r\nimport { GraphService } from '@myrmidon/cadmus-api';\r\n\r\n/**\r\n * Graph nodes list repository.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class GraphTripleListRepository\r\n implements PagedListStoreService<TripleFilter, UriTriple>\r\n{\r\n private readonly _store: PagedListStore<TripleFilter, UriTriple>;\r\n private readonly _loading$: BehaviorSubject<boolean | undefined>;\r\n private readonly _filter$: BehaviorSubject<TripleFilter>;\r\n private readonly _subjectNode$: BehaviorSubject<UriNode | undefined>;\r\n private readonly _predicateNode$: BehaviorSubject<UriNode | undefined>;\r\n private readonly _objectNode$: BehaviorSubject<UriNode | undefined>;\r\n\r\n public get loading$(): Observable<boolean | undefined> {\r\n return this._loading$.asObservable();\r\n }\r\n public get filter$(): Observable<TripleFilter> {\r\n return this._filter$.asObservable();\r\n }\r\n public get page$(): Observable<DataPage<UriTriple>> {\r\n return this._store.page$;\r\n }\r\n public get subjectNode$(): Observable<UriNode | undefined> {\r\n return this._subjectNode$.asObservable();\r\n }\r\n public get predicateNode$(): Observable<UriNode | undefined> {\r\n return this._predicateNode$.asObservable();\r\n }\r\n public get objectNode$(): Observable<UriNode | undefined> {\r\n return this._objectNode$.asObservable();\r\n }\r\n\r\n constructor(private _graphService: GraphService) {\r\n this._store = new PagedListStore<TripleFilter, UriTriple>(this);\r\n this._filter$ = new BehaviorSubject<TripleFilter>({});\r\n this._subjectNode$ = new BehaviorSubject<UriNode | undefined>(undefined);\r\n this._predicateNode$ = new BehaviorSubject<UriNode | undefined>(undefined);\r\n this._objectNode$ = new BehaviorSubject<UriNode | undefined>(undefined);\r\n this._loading$ = new BehaviorSubject<boolean | undefined>(undefined);\r\n this._store.reset();\r\n }\r\n\r\n public async reset(): Promise<void> {\r\n this._loading$.next(true);\r\n try {\r\n await this._store.reset();\r\n } catch (error) {\r\n throw error;\r\n } finally {\r\n this._loading$.next(false);\r\n }\r\n }\r\n\r\n public loadPage(\r\n pageNumber: number,\r\n pageSize: number,\r\n filter: TripleFilter\r\n ): Observable<DataPage<UriTriple>> {\r\n this._loading$.next(true);\r\n return this._graphService.getTriples(pageNumber, pageSize, filter).pipe(\r\n tap({\r\n next: () => this._loading$.next(false),\r\n error: () => this._loading$.next(false),\r\n })\r\n );\r\n }\r\n\r\n public async setFilter(filter: TripleFilter): Promise<void> {\r\n this._loading$.next(true);\r\n try {\r\n await this._store.setFilter(filter);\r\n } catch (error) {\r\n throw error;\r\n } finally {\r\n this._loading$.next(false);\r\n }\r\n }\r\n\r\n public getFilter(): TripleFilter {\r\n return this._store.getFilter();\r\n }\r\n\r\n public async setPage(pageNumber: number, pageSize: number): Promise<void> {\r\n this._loading$.next(true);\r\n try {\r\n await this._store.setPage(pageNumber, pageSize);\r\n } catch (error) {\r\n throw error;\r\n } finally {\r\n this._loading$.next(false);\r\n }\r\n }\r\n\r\n /**\r\n * Set the node term used in filter.\r\n *\r\n * @param node The node or null/undefined.\r\n * @param type The type: subject, predicate, object.\r\n */\r\n public setTerm(\r\n node: UriNode | null | undefined,\r\n type: 'S' | 'P' | 'O'\r\n ): void {\r\n switch (type) {\r\n case 'S':\r\n this._subjectNode$.next(node || undefined);\r\n break;\r\n case 'P':\r\n this._predicateNode$.next(node || undefined);\r\n break;\r\n case 'O':\r\n this._objectNode$.next(node || undefined);\r\n break;\r\n }\r\n }\r\n\r\n /**\r\n * Set the node term used in filter by its ID.\r\n *\r\n * @param id The node ID or null/undefined.\r\n * @param type The type: subject, predicate, object.\r\n */\r\n public setTermId(id: number | null | undefined, type: 'S' | 'P' | 'O'): void {\r\n if (!id) {\r\n this.setTerm(null, type);\r\n return;\r\n }\r\n this._graphService.getNode(id).subscribe({\r\n next: (node) => {\r\n this.setTerm(node, type);\r\n },\r\n error: (error) => {\r\n console.error(`Node ID ${id} not found`, error);\r\n },\r\n });\r\n }\r\n\r\n public selectTerm(type: 'S' | 'P' | 'O'): Observable<UriNode | undefined> {\r\n switch (type) {\r\n case 'S':\r\n return this.subjectNode$;\r\n case 'P':\r\n return this.predicateNode$;\r\n case 'O':\r\n return this.objectNode$;\r\n }\r\n }\r\n\r\n public getTerm(type: 'S' | 'P' | 'O'): UriNode | undefined {\r\n switch (type) {\r\n case 'S':\r\n return this._subjectNode$.value;\r\n case 'P':\r\n return this._predicateNode$.value;\r\n case 'O':\r\n return this._objectNode$.value;\r\n }\r\n }\r\n}\r\n","import { Component, input, OnDestroy, OnInit } from '@angular/core';\nimport { AsyncPipe } from '@angular/common';\nimport {\n FormBuilder,\n FormControl,\n FormGroup,\n Validators,\n FormsModule,\n ReactiveFormsModule,\n} from '@angular/forms';\nimport { Observable, Subscription } from 'rxjs';\n\nimport { MatIconButton } from '@angular/material/button';\nimport { MatIcon } from '@angular/material/icon';\nimport { MatCheckbox } from '@angular/material/checkbox';\nimport { MatLabel, MatFormField } from '@angular/material/form-field';\nimport { MatInput } from '@angular/material/input';\nimport { MatTooltip } from '@angular/material/tooltip';\n\nimport { RefLookupComponent } from '@myrmidon/cadmus-refs-lookup';\n\nimport { UriNode, TripleFilter } from '@myrmidon/cadmus-api';\n\nimport { GraphTripleListRepository } from '../../state/graph-triple-list.repository';\nimport { GraphNodeLookupService } from '../../services/graph-node-lookup.service';\n\n/**\n * Graph triples filter used in graph triples list.\n * Its data are in the graph triples store, which gets updated when\n * users apply new filters.\n */\n@Component({\n selector: 'cadmus-graph-triple-filter',\n templateUrl: './graph-triple-filter.component.html',\n styleUrls: ['./graph-triple-filter.component.css'],\n imports: [\n FormsModule,\n ReactiveFormsModule,\n RefLookupComponent,\n MatIconButton,\n MatIcon,\n MatCheckbox,\n MatLabel,\n MatFormField,\n MatInput,\n MatTooltip,\n AsyncPipe,\n ],\n})\nexport class GraphTripleFilterComponent implements OnInit, OnDestroy {\n private _sub?: Subscription;\n public filter$: Observable<TripleFilter>;\n public literal: FormControl<boolean>;\n public objectLit: FormControl<string | null>;\n public sid: FormControl<string | null>;\n public sidPrefix: FormControl<boolean>;\n public tag: FormControl<string | null>;\n public form: FormGroup;\n\n public subjectNode$: Observable<UriNode | undefined>;\n public predicateNode$: Observable<UriNode | undefined>;\n public objectNode$: Observable<UriNode | undefined>;\n\n public readonly disabled = input<boolean>();\n\n constructor(\n formBuilder: FormBuilder,\n public lookupService: GraphNodeLookupService,\n private _repository: GraphTripleListRepository\n ) {\n this.filter$ = _repository.filter$;\n this.subjectNode$ = _repository.subjectNode$;\n this.predicateNode$ = _repository.predicateNode$;\n this.objectNode$ = _repository.objectNode$;\n // form\n this.literal = formBuilder.control(false, { nonNullable: true });\n this.objectLit = formBuilder.control(null, Validators.maxLength(100));\n this.sid = formBuilder.control(null);\n this.sidPrefix = formBuilder.control(false, { nonNullable: true });\n this.tag = formBuilder.control(null);\n this.form = formBuilder.group({\n literal: this.literal,\n objectLit: this.objectLit,\n sid: this.sid,\n sidPrefix: this.sidPrefix,\n tag: this.tag,\n });\n }\n\n public ngOnInit(): void {\n this._sub = this.filter$.subscribe((f) => {\n this.updateForm(f);\n });\n }\n\n public ngOnDestroy(): void {\n this._sub?.unsubscribe();\n }\n\n private updateForm(filter: TripleFilter): void {\n this._repository.setTermId(filter.subjectId, 'S');\n this._repository.setTermId(\n filter.predicateIds?.length ? filter.predicateIds[0] : null,\n 'P'\n );\n this._repository.setTermId(filter.objectId, 'O');\n this.literal.setValue(filter.literalPattern ? true : false);\n this.objectLit.setValue(filter.literalPattern || null);\n this.sid.setValue(filter.sid || null);\n this.tag.setValue(filter.tag || null);\n this.form.markAsPristine();\n }\n\n private getFilter(): TripleFilter {\n const pid = this._repository.getTerm('P')?.id;\n return {\n subjectId: this._repository.getTerm('S')?.id,\n predicateIds: pid ? [pid] : undefined,\n objectId: this.literal.value\n ? undefined\n : this._repository.getTerm('O')?.id,\n literalPattern: this.literal.value\n ? this.objectLit.value?.trim()\n : undefined,\n sid: this.sid.value?.trim(),\n tag: this.tag.value?.trim(),\n };\n }\n\n public onSubjectNodeChange(node?: unknown): void {\n this._repository.setTerm(node as UriNode, 'S');\n }\n\n public clearSubjectNode(): void {\n this._repository.setTerm(null, 'S');\n }\n\n public onPredicateNodeChange(node?: unknown): void {\n this._repository.setTerm(node as UriNode, 'P');\n }\n\n public clearPredicateNode(): void {\n this._repository.setTerm(null, 'P');\n }\n\n public onObjectNodeChange(node?: unknown): void {\n this._repository.setTerm(node as UriNode, 'O');\n }\n\n public clearObjectNode(): void {\n this._repository.setTerm(null, 'O');\n }\n\n public reset(): void {\n this.form.reset();\n this.apply();\n }\n\n public apply(): void {\n if (this.form.invalid) {\n return;\n }\n const filter = this.getFilter();\n\n // update filter in state\n this._repository.setFilter(filter);\n }\n}\n","<form [formGroup]=\"form\" (submit)=\"apply()\" [attr.disabled]=\"disabled()\">\r\n <!-- subject -->\r\n <div class=\"form-row\">\r\n <cadmus-refs-lookup\r\n [service]=\"lookupService\"\r\n [item]=\"subjectNode$ | async\"\r\n label=\"subject\"\r\n (itemChange)=\"onSubjectNodeChange($event)\"\r\n />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n style=\"margin-top: 20px\"\r\n (click)=\"clearSubjectNode()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n </div>\r\n <!-- predicate -->\r\n <div class=\"form-row\">\r\n <cadmus-refs-lookup\r\n [service]=\"lookupService\"\r\n label=\"predicate\"\r\n [item]=\"predicateNode$ | async\"\r\n [baseFilter]=\"{ tag: 'property' }\"\r\n (itemChange)=\"onPredicateNodeChange($event)\"\r\n />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n style=\"margin-top: 20px\"\r\n (click)=\"clearPredicateNode()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n </div>\r\n <!-- object, objectLit -->\r\n <div>\r\n <mat-checkbox [formControl]=\"literal\">literal</mat-checkbox>\r\n </div>\r\n @if (literal.value) {\r\n <div>\r\n <mat-label>literal</mat-label>\r\n <mat-form-field>\r\n <input matInput [formControl]=\"objectLit\" maxlength=\"500\" />\r\n </mat-form-field>\r\n </div>\r\n } @else {\r\n <div class=\"form-row\">\r\n <cadmus-refs-lookup\r\n [service]=\"lookupService\"\r\n [item]=\"objectNode$ | async\"\r\n label=\"object\"\r\n (itemChange)=\"onObjectNodeChange($event)\"\r\n />\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n style=\"margin-top: 20px\"\r\n (click)=\"clearObjectNode()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n </div>\r\n }\r\n <!-- sid, sidPrefix -->\r\n <div>\r\n <mat-form-field>\r\n <mat-label>sid</mat-label>\r\n <input matInput [formControl]=\"sid\" maxlength=\"500\" />\r\n </mat-form-field>\r\n \r\n <mat-checkbox [formControl]=\"sidPrefix\">prefix</mat-checkbox>\r\n </div>\r\n <!-- tag -->\r\n <br />\r\n <div\r\n class=\"btn-group\"\r\n role=\"group\"\r\n aria-label=\"toolbar\"\r\n style=\"margin-bottom: 10px\"\r\n >\r\n <button\r\n mat-icon-button\r\n type=\"button\"\r\n (click)=\"reset()\"\r\n matTooltip=\"Reset filters\"\r\n [disabled]=\"disabled()\"\r\n >\r\n <mat-icon class=\"mat-warn\">clear</mat-icon>\r\n </button>\r\n <button\r\n type=\"submit\"\r\n mat-icon-button\r\n [disabled]=\"disabled()\"\r\n matTooltip=\"Apply filters\"\r\n >\r\n <mat-icon class=\"mat-primary\">check_circle</mat-icon>\r\n </button>\r\n </div>\r\n</form>\r\n","import {\r\n Component,\r\n effect,\r\n model,\r\n OnDestroy,\r\n OnInit,\r\n output,\r\n signal,\r\n} from '@angular/core';\r\nimport {\r\n FormBuilder,\r\n FormControl,\r\n FormGroup,\r\n Validators,\r\n FormsModule,\r\n ReactiveFormsModule,\r\n} from '@angular/forms';\r\nimport { Subscription } from 'rxjs';\r\nimport { take } from 'rxjs/operators';\r\n\r\nimport { MatSnackBar } from '@angular/material/snack-bar';\r\nimport { MatCheckbox } from '@angular/material/checkbox';\r\nimport {\r\n MatFormField,\r\n MatLabel,\r\n MatError,\r\n MatHint,\r\n} from '@angular/material/form-field';\r\nimport { MatInput } from '@angular/material/input';\r\nimport { MatIconButton } from '@angular/material/button';\r\nimport { MatIcon } from '@angular/material/icon';\r\n\r\nimport { NgxToolsValidators } from '@myrmidon/ngx-tools';\r\nimport { RefLookupComponent } from '@myrmidon/cadmus-refs-lookup';\r\n\r\nimport { GraphService, UriNode, UriTriple } from '@myrmidon/cadmus-api';\r\n\r\nimport { GraphNodeLookupService } from '../../services/graph-node-lookup.service';\r\n\r\n@Component({\r\n selector: 'cadmus-graph-triple-editor',\r\n templateUrl: './graph-triple-editor.component.html',\r\n styleUrls: ['./graph-triple-editor.component.css'],\r\n imports: [\r\n FormsModule,\r\n ReactiveFormsModule,\r\n RefLookupComponent,\r\n MatCheckbox,\r\n MatFormField,\r\n MatLabel,\r\n MatInput,\r\n MatError,\r\n MatHint,\r\n MatIconButton,\r\n MatIcon,\r\n ],\r\n})\r\nexport class GraphTripleEditorComponent implements OnInit, OnDestroy {\r\n private _sub?: Subscription;\r\n\r\n public readonly triple = model<UriTriple>();\r\n\r\n /**\r\n * Emitted when the user requested to close the editor.\r\n */\r\n public readonly editorClose = output();\r\n\r\n public readonly isNew = signal<boolean>(true);\r\n\r\n public subjectNode: FormControl<UriNode | null>;\r\n public predicateNode: FormControl<UriNode | null>;\r\n public objectNode: FormControl<UriNode | null>;\r\n public isLiteral: FormControl<boolean>;\r\n public literal: FormControl<string | null>;\r\n public literalLang: FormControl<string | null>;\r\n public literalType: FormControl<string | null>;\r\n public tag: FormControl<string | null>;\r\n public form: FormGroup;\r\n\r\n constructor(\r\n formBuilder: FormBuilder,\r\n public lookupService: GraphNodeLookupService,\r\n private _snackbar: MatSnackBar,\r\n private _graphService: GraphService\r\n ) {\r\n // form\r\n this.subjectNode = formBuilder.control(null, Validators.required);\r\n this.predicateNode = formBuilder.control(null, Validators.required);\r\n this.objectNode = formBuilder.control(null, {\r\n validators: NgxToolsValidators.conditionalValidator(\r\n () => !this.isLiteral.value,\r\n Validators.required\r\n ),\r\n });\r\n this.isLiteral =