diff --git a/.changeset/hungry-badgers-end.md b/.changeset/hungry-badgers-end.md new file mode 100644 index 000000000..3eeee1349 --- /dev/null +++ b/.changeset/hungry-badgers-end.md @@ -0,0 +1,5 @@ +--- +"@lblod/ember-rdfa-editor-lblod-plugins": minor +--- + +GN-4693: Insert LPDC diff --git a/addon/components/lpdc-plugin/lpdc-insert.hbs b/addon/components/lpdc-plugin/lpdc-insert.hbs new file mode 100644 index 000000000..e7400ed65 --- /dev/null +++ b/addon/components/lpdc-plugin/lpdc-insert.hbs @@ -0,0 +1,19 @@ +{{! @glint-nocheck: not typesafe yet }} +
  • + + {{t 'lpdc-plugin.insert.title'}} + +
  • + + \ No newline at end of file diff --git a/addon/components/lpdc-plugin/lpdc-insert.ts b/addon/components/lpdc-plugin/lpdc-insert.ts new file mode 100644 index 000000000..042428641 --- /dev/null +++ b/addon/components/lpdc-plugin/lpdc-insert.ts @@ -0,0 +1,94 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { AddIcon } from '@appuniversum/ember-appuniversum/components/icons/add'; + +import { SayController } from '@lblod/ember-rdfa-editor'; +import { sayDataFactory } from '@lblod/ember-rdfa-editor/core/say-data-factory'; +import { addProperty } from '@lblod/ember-rdfa-editor/commands'; +import { + LPDC, + type LpdcPluginConfig, +} from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/lpdc-plugin'; +import { v4 as uuidv4 } from 'uuid'; +import { getCurrentBesluitRange } from '../../plugins/besluit-topic-plugin/utils/helpers'; +import { SRO } from '../../utils/constants'; + +interface Args { + controller: SayController; + config: LpdcPluginConfig; +} + +export default class LpdcPluginsInsertComponent extends Component { + AddIcon = AddIcon; + + @tracked showModal = false; + + get controller() { + return this.args.controller; + } + + @action + openModal() { + this.controller.focus(); + this.showModal = true; + } + + @action + closeModal() { + this.showModal = false; + } + + get isButtonDisabled() { + return !getCurrentBesluitRange(this.controller); + } + + @action + onLpdcInsert(lpdc: LPDC) { + const rdfaId = uuidv4(); + + const uri = lpdc.uri; + const name = lpdc.name; + + const currentBesluitRange = getCurrentBesluitRange(this.controller); + + const resource = + (currentBesluitRange && + 'node' in currentBesluitRange && + (currentBesluitRange.node.attrs.subject as string)) || + undefined; + + if (!resource) { + throw new Error('No besluit found in selection'); + } + + this.controller.withTransaction( + (tr) => { + const node = this.controller.schema.node( + 'inline_rdfa', + { + rdfaNodeType: 'literal', + __rdfaId: rdfaId, + subject: uri, + }, + [this.controller.schema.text(name)], + ); + + return tr.replaceSelectionWith(node); + }, + { view: this.controller.mainEditorView }, + ); + + this.controller.doCommand( + addProperty({ + resource, + property: { + predicate: SRO('bekrachtigt').full, + object: sayDataFactory.literalNode(rdfaId), + }, + }), + ); + + this.closeModal(); + } +} diff --git a/addon/components/lpdc-plugin/lpdc-modal.hbs b/addon/components/lpdc-plugin/lpdc-modal.hbs new file mode 100644 index 000000000..e23fedaac --- /dev/null +++ b/addon/components/lpdc-plugin/lpdc-modal.hbs @@ -0,0 +1,44 @@ +{{! @glint-nocheck: not typesafe yet }} + + + + +
    + {{#if this.error}} + + {{else}} + + {{/if}} +
    + + + + {{t 'lpdc-plugin.modal.close'}} + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/addon/components/lpdc-plugin/lpdc-modal.ts b/addon/components/lpdc-plugin/lpdc-modal.ts new file mode 100644 index 000000000..7055e0852 --- /dev/null +++ b/addon/components/lpdc-plugin/lpdc-modal.ts @@ -0,0 +1,79 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { restartableTask, timeout } from 'ember-concurrency'; +import { task as trackedTask } from 'ember-resources/util/ember-concurrency'; + +import { tracked } from '@glimmer/tracking'; + +import { + fetchLpdcs, + LPDC, + type LpdcPluginConfig, +} from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/lpdc-plugin'; +import { trackedReset } from 'tracked-toolbox'; + +interface Signature { + Args: { + config: LpdcPluginConfig; + onLpdcInsert: (lpdc: LPDC) => void; + closeModal: () => void; + open: boolean; + }; +} + +export default class LpdcPluginModalComponent extends Component { + // Filtering + @tracked searchText: string | null = null; + + // Display + @tracked error: unknown; + + /** + * Paginating the search results + */ + @trackedReset('searchText') + pageNumber = 0; + + get config() { + return this.args.config; + } + + @action + closeModal() { + this.args.closeModal(); + } + + lpdcSearch = restartableTask(async () => { + await timeout(100); + this.error = null; + const abortController = new AbortController(); + try { + const results = await fetchLpdcs({ + filter: { + name: this.searchText ?? undefined, + }, + pageNumber: this.pageNumber, + config: this.args.config, + }); + + return Object.assign(results.lpdc, { + meta: results.meta, + }); + } catch (error) { + this.error = error; + return []; + } finally { + //Abort all requests now that this task has either successfully finished or has been cancelled + abortController.abort(); + } + }); + + lpdcResource = trackedTask(this, this.lpdcSearch, () => [ + this.searchText, + this.pageNumber, + ]); + + @action onSearchTextChange(event: InputEvent): void { + this.searchText = (event.target as HTMLInputElement).value; + } +} diff --git a/addon/components/lpdc-plugin/lpdc-table-view.hbs b/addon/components/lpdc-plugin/lpdc-table-view.hbs new file mode 100644 index 000000000..a31653845 --- /dev/null +++ b/addon/components/lpdc-plugin/lpdc-table-view.hbs @@ -0,0 +1,53 @@ +{{! @glint-nocheck: not typesafe yet }} +
    + {{#let + (t 'lpdc-plugin.table.header.name') + (t 'lpdc-plugin.search.placeholder') + (t 'lpdc-plugin.modal.insert') + as |name search insert| + }} + + + + + + + + + + + + + + + {{insert}} + + + {{row.name}} + + + {{insert}} + + + + + + {{/let}} +
    \ No newline at end of file diff --git a/addon/components/lpdc-plugin/lpdc-table-view.ts b/addon/components/lpdc-plugin/lpdc-table-view.ts new file mode 100644 index 000000000..d2672e709 --- /dev/null +++ b/addon/components/lpdc-plugin/lpdc-table-view.ts @@ -0,0 +1,23 @@ +import Component from '@glimmer/component'; +import { LPDC } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/lpdc-plugin'; + +interface Args { + lpdc: LPDC[] & { + meta: { + count: number; + pagination: { first: { number: number }; last: { number: number } }; + }; + }; + isLoading: boolean; + onLpdcInsert: (lpdc: LPDC) => void; + // Filtering + nameFilter: string | null; + // Pagination + pageNumber: number; +} + +export default class LpdcTableViewComponent extends Component { + get lpdc() { + return this.args.lpdc; + } +} diff --git a/addon/plugins/lpdc-plugin/api.ts b/addon/plugins/lpdc-plugin/api.ts new file mode 100644 index 000000000..5943252fa --- /dev/null +++ b/addon/plugins/lpdc-plugin/api.ts @@ -0,0 +1,111 @@ +import { LPDC } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/lpdc-plugin/types'; + +import { LpdcPluginConfig } from './index'; + +type LPDCInstance = { + id: string; // UUID + '@id': string; // URI + naam: { + nl: string; + }; + linkedConcept: string; // URI of linked concept + linkedConceptId: string; // UUID of linked concept + linkedConceptProductnummer: string; +}; + +type FetchResults = { + hydraPageIndex: number; + hydraLimit: number; + hydraTotalItems: number; + hydraMember: Array; + hydraView: { + /** + * String like + * https://ipdc.tni-vlaanderen.be/doc/instantie?limit=25&pageIndex=0&sortBy=LAATST_GEWIJZIGD + */ + hydraFirst: string; + hydraLast: string; + hydraNext?: string; + hydraPrevious?: string; + }; +}; + +const getPageIndex = (url: string): number => { + const urlSearchParams = new URL(url).searchParams; + + return parseInt(urlSearchParams.get('pageIndex') ?? '0'); +}; + +export const fetchLpdcs = async ({ + config, + filter, + pageNumber, +}: { + pageNumber: number; + config: LpdcPluginConfig; + filter?: { + name?: string; + }; +}): Promise<{ + lpdc: Array; + pageIndex: number; + meta: { + count: number; + pagination: { + first: { number: number }; + last: { number: number }; + next?: { number: number }; + prev?: { number: number }; + }; + }; +}> => { + const endpoint = config?.endpoint; + + const url = new URL(`${endpoint}/doc/instantie`); + + if (filter?.name) { + url.searchParams.append('zoekterm', filter.name); + } + + if (pageNumber) { + url.searchParams.append('pageIndex', pageNumber.toString()); + } + + const results = await fetch(url.toString(), { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }); + + const resultJson = (await results.json()) as FetchResults; + + return { + lpdc: (resultJson.hydraMember ?? []).map((lpdc) => ({ + uri: lpdc['@id'], + name: lpdc.naam.nl, + })), + pageIndex: resultJson.hydraPageIndex, + meta: { + count: resultJson.hydraTotalItems, + pagination: { + first: { + number: getPageIndex(resultJson.hydraView.hydraFirst), + }, + next: resultJson.hydraView.hydraNext + ? { + number: getPageIndex(resultJson.hydraView.hydraNext), + } + : undefined, + last: { + number: getPageIndex(resultJson.hydraView.hydraLast), + }, + prev: resultJson.hydraView.hydraPrevious + ? { + number: getPageIndex(resultJson.hydraView.hydraPrevious), + } + : undefined, + }, + }, + }; +}; diff --git a/addon/plugins/lpdc-plugin/index.ts b/addon/plugins/lpdc-plugin/index.ts new file mode 100644 index 000000000..35829bc3b --- /dev/null +++ b/addon/plugins/lpdc-plugin/index.ts @@ -0,0 +1,2 @@ +export { type LpdcPluginConfig, LPDC } from './types'; +export { fetchLpdcs } from './api'; diff --git a/addon/plugins/lpdc-plugin/types.ts b/addon/plugins/lpdc-plugin/types.ts new file mode 100644 index 000000000..c1567f41c --- /dev/null +++ b/addon/plugins/lpdc-plugin/types.ts @@ -0,0 +1,5 @@ +export type LpdcPluginConfig = { + endpoint: string; +}; + +export type LPDC = { uri: string; name: string }; diff --git a/addon/utils/constants.ts b/addon/utils/constants.ts index c67dd45a2..92567979b 100644 --- a/addon/utils/constants.ts +++ b/addon/utils/constants.ts @@ -31,3 +31,7 @@ export const GEOSPARQL = namespace( 'http://www.opengis.net/ont/geosparql#', 'geosparql', ); +export const SRO = namespace( + 'https://data.vlaanderen.be/ns/slimmeraadpleegomgeving#', + 'sro', +); diff --git a/app/components/lpdc-plugin/lpdc-insert.ts b/app/components/lpdc-plugin/lpdc-insert.ts new file mode 100644 index 000000000..4ef43d318 --- /dev/null +++ b/app/components/lpdc-plugin/lpdc-insert.ts @@ -0,0 +1 @@ +export { default } from '@lblod/ember-rdfa-editor-lblod-plugins/components/lpdc-plugin/lpdc-insert'; diff --git a/app/components/lpdc-plugin/lpdc-modal.ts b/app/components/lpdc-plugin/lpdc-modal.ts new file mode 100644 index 000000000..78beb4e1d --- /dev/null +++ b/app/components/lpdc-plugin/lpdc-modal.ts @@ -0,0 +1 @@ +export { default } from '@lblod/ember-rdfa-editor-lblod-plugins/components/lpdc-plugin/lpdc-modal'; diff --git a/app/components/lpdc-plugin/lpdc-table-view.ts b/app/components/lpdc-plugin/lpdc-table-view.ts new file mode 100644 index 000000000..82b9ff19d --- /dev/null +++ b/app/components/lpdc-plugin/lpdc-table-view.ts @@ -0,0 +1 @@ +export { default } from '@lblod/ember-rdfa-editor-lblod-plugins/components/lpdc-plugin/lpdc-table-view'; diff --git a/tests/dummy/app/controllers/besluit-sample.ts b/tests/dummy/app/controllers/besluit-sample.ts index a37703db2..355f86963 100644 --- a/tests/dummy/app/controllers/besluit-sample.ts +++ b/tests/dummy/app/controllers/besluit-sample.ts @@ -269,7 +269,7 @@ export default class BesluitSampleController extends Controller { citation: { type: 'nodes', activeInNodeTypes(schema: Schema): Set { - return new Set([schema.nodes.motivering]); + return new Set([schema.nodes.doc]); }, endpoint: 'https://codex.opendata.api.vlaanderen.be:8888/sparql', decisionsEndpoint: @@ -302,6 +302,9 @@ export default class BesluitSampleController extends Controller { worship: { endpoint: 'https://data.lblod.info/sparql', }, + lpdc: { + endpoint: 'http://localhost/lpdc-service', + }, }; } diff --git a/tests/dummy/app/templates/besluit-sample.hbs b/tests/dummy/app/templates/besluit-sample.hbs index 792ce819a..6885f6ab5 100644 --- a/tests/dummy/app/templates/besluit-sample.hbs +++ b/tests/dummy/app/templates/besluit-sample.hbs @@ -136,6 +136,10 @@ @controller={{this.controller}} @config={{this.config.worship}} /> + - {{#if (and this.activeNode this.editableNodes)}} - - - - {{/if}} + + + {{/if}} diff --git a/translations/en-US.yaml b/translations/en-US.yaml index eaebef708..beb73c9b6 100644 --- a/translations/en-US.yaml +++ b/translations/en-US.yaml @@ -416,3 +416,16 @@ worship-plugin: sort: Sort asc: Sort ascending desc: Sort descending + +lpdc-plugin: + insert: + title: Insert LPDC + modal: + title: Insert LPDC + insert: Insert + close: Close + table: + header: + name: Name + search: + placeholder: LPDC Name/Description diff --git a/translations/nl-BE.yaml b/translations/nl-BE.yaml index ffa33e86c..b2add32b3 100644 --- a/translations/nl-BE.yaml +++ b/translations/nl-BE.yaml @@ -416,3 +416,16 @@ worship-plugin: sort: Sorteren asc: Oplopend sorteren desc: Aflopend sorteren + +lpdc-plugin: + insert: + title: LPDC invoegen + modal: + title: LPDC invoegen + insert: Invoegen + close: Sluiten + table: + header: + name: Naam + search: + placeholder: LPDC-naam/beschrijving