diff --git a/.changeset/gentle-berries-exist.md b/.changeset/gentle-berries-exist.md new file mode 100644 index 000000000..0ff4393e7 --- /dev/null +++ b/.changeset/gentle-berries-exist.md @@ -0,0 +1,5 @@ +--- +'@lblod/ember-rdfa-editor-lblod-plugins': minor +--- + +When removing last snippet of a 'group', replace the placeholder instead of completely deleting diff --git a/.changeset/hungry-trees-perform.md b/.changeset/hungry-trees-perform.md new file mode 100644 index 000000000..7ced646d6 --- /dev/null +++ b/.changeset/hungry-trees-perform.md @@ -0,0 +1,5 @@ +--- +'@lblod/ember-rdfa-editor-lblod-plugins': patch +--- + +Fix bug with snippet list names containing a `,` displaying as multiple lists diff --git a/addon/components/snippet-plugin/nodes/placeholder.gts b/addon/components/snippet-plugin/nodes/placeholder.gts index 841026175..a2da54b71 100644 --- a/addon/components/snippet-plugin/nodes/placeholder.gts +++ b/addon/components/snippet-plugin/nodes/placeholder.gts @@ -8,13 +8,13 @@ import { service } from '@ember/service'; import IntlService from 'ember-intl/services/intl'; interface Signature { - Args: EmberNodeArgs; + Args: Pick; } export default class SnippetPluginPlaceholder extends Component { @service declare intl: IntlService; get listNames() { - return this.args.node.attrs.listNames; + return this.args.node.attrs.snippetListNames; } get isSingleList() { return this.listNames.length === 1; diff --git a/addon/components/snippet-plugin/nodes/snippet.gts b/addon/components/snippet-plugin/nodes/snippet.gts index 57e43996d..534e5bbf8 100644 --- a/addon/components/snippet-plugin/nodes/snippet.gts +++ b/addon/components/snippet-plugin/nodes/snippet.gts @@ -28,6 +28,8 @@ import insertSnippet from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippe import { isNone } from '@lblod/ember-rdfa-editor/utils/_private/option'; import { transactionCombinator } from '@lblod/ember-rdfa-editor/utils/transaction-utils'; import { recalculateNumbers } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/structure-plugin/recalculate-structure-numbers'; +import { createSnippetPlaceholder } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin/nodes/snippet-placeholder'; +import { hasDecendant } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/has-descendant'; interface ButtonSig { Args: { @@ -62,6 +64,15 @@ export default class SnippetNode extends Component { get controller() { return this.args.controller; } + get schema() { + return this.controller.schema; + } + get snippetOrPlaceholder() { + return [ + this.schema.nodes.snippet, + this.schema.nodes.snippet_placeholder, + ].filter(Boolean); + } get node() { return this.args.node; } @@ -94,17 +105,46 @@ export default class SnippetNode extends Component { deleteFragment() { const position = this.args.getPos(); if (position !== undefined) { - this.controller.withTransaction((tr) => { - return transactionCombinator( - this.controller.mainEditorState, - tr.deleteRange(position, position + this.node.nodeSize), - )([recalculateNumbers]).transaction; - }); + const matchingSnippetExists = hasDecendant( + this.controller.mainEditorState.doc, + (node) => + node !== this.node && + this.snippetOrPlaceholder.includes(node.type) && + node.attrs.placeholderId === this.node.attrs.placeholderId, + ); + if (matchingSnippetExists) { + this.controller.withTransaction((tr) => { + return transactionCombinator( + this.controller.mainEditorState, + tr.deleteRange(position, position + this.node.nodeSize), + )([recalculateNumbers]).transaction; + }); + } else { + const node = createSnippetPlaceholder({ + listProperties: { + placeholderId: this.node.attrs.placeholderId, + listIds: this.node.attrs.snippetListIds, + names: this.node.attrs.snippetListNames, + importedResources: this.node.attrs.importedResources, + }, + schema: this.schema, + allowMultipleSnippets: this.allowMultipleSnippets, + }); + + this.args.controller.withTransaction( + (tr) => + transactionCombinator( + this.controller.mainEditorState, + tr.replaceWith(position, position + this.node.nodeSize, node), + )([recalculateNumbers]).transaction, + { view: this.args.controller.mainEditorView }, + ); + } } } createSliceFromElement(element: Element) { return new Slice( - ProseParser.fromSchema(this.controller.schema).parse(element, { + ProseParser.fromSchema(this.schema).parse(element, { preserveWhitespace: true, }).content, 0, @@ -127,7 +167,6 @@ export default class SnippetNode extends Component { @action onInsert(content: string, title: string) { this.closeModal(); - const assignedSnippetListsIds = this.node.attrs.assignedSnippetListsIds; let start = 0; let end = 0; const pos = this.args.getPos(); @@ -147,8 +186,12 @@ export default class SnippetNode extends Component { insertSnippet({ content, title, - assignedSnippetListsIds, - importedResources: this.node.attrs.importedResources, + listProperties: { + placeholderId: this.node.attrs.placeholderId, + listIds: this.node.attrs.snippetListIds, + names: this.node.attrs.snippetListNames, + importedResources: this.node.attrs.importedResources, + }, range: { start, end }, allowMultipleSnippets: this.allowMultipleSnippets, }), @@ -189,7 +232,7 @@ export default class SnippetNode extends Component { @closeModal={{this.closeModal}} @config={{this.node.attrs.config}} @onInsert={{this.onInsert}} - @assignedSnippetListsIds={{this.node.attrs.assignedSnippetListsIds}} + @snippetListIds={{this.node.attrs.snippetListIds}} /> } diff --git a/addon/components/snippet-plugin/search-modal.ts b/addon/components/snippet-plugin/search-modal.ts index bdb03c60f..82966a8db 100644 --- a/addon/components/snippet-plugin/search-modal.ts +++ b/addon/components/snippet-plugin/search-modal.ts @@ -11,7 +11,7 @@ import { SnippetPluginConfig } from '@lblod/ember-rdfa-editor-lblod-plugins/plug interface Args { config: SnippetPluginConfig; - assignedSnippetListsIds: string[] | undefined; + snippetListIds: string[] | undefined; closeModal: () => void; open: boolean; onInsert: (content: string, title: string) => void; @@ -64,8 +64,7 @@ export default class SnippetPluginSearchModalComponent extends Component { abortSignal: abortController.signal, filter: { name: this.inputSearchText ?? undefined, - assignedSnippetListIds: - this.args.assignedSnippetListsIds ?? undefined, + snippetListIds: this.args.snippetListIds ?? undefined, }, pagination: { pageNumber: this.pageNumber, @@ -88,7 +87,7 @@ export default class SnippetPluginSearchModalComponent extends Component { this.inputSearchText, this.pageNumber, this.pageSize, - this.args.assignedSnippetListsIds, + this.args.snippetListIds, ]); @action diff --git a/addon/components/snippet-plugin/snippet-insert-placeholder.gts b/addon/components/snippet-plugin/snippet-insert-placeholder.gts index 0ecf868ff..9a4e52cc3 100644 --- a/addon/components/snippet-plugin/snippet-insert-placeholder.gts +++ b/addon/components/snippet-plugin/snippet-insert-placeholder.gts @@ -43,11 +43,11 @@ export default class SnippetPluginSnippetInsertPlaceholder extends Component { @@ -72,7 +72,7 @@ export default class SnippetPluginSnippetInsertPlaceholder extends Component { - get disableInsert() { - return (this.snippetListProperties?.listIds.length ?? 0) === 0; - } - - get snippetListProperties(): - | { listIds: string[]; importedResources: ImportedResourceMap } - | undefined { + get listProperties(): SnippetListProperties | undefined { const activeNode = this.args.node.value; const activeNodeSnippetListIds = getSnippetListIdsProperties(activeNode); @@ -38,6 +32,8 @@ export default class SnippetInsertRdfaComponent extends Component { listIds: getAssignedSnippetListsIdsFromProperties( activeNodeSnippetListIds, ), + placeholderId: activeNode.attrs.placeholderId, + names: activeNode.attrs.snippetListNames, importedResources: activeNode.attrs.importedResources, }; } @@ -58,6 +54,8 @@ export default class SnippetInsertRdfaComponent extends Component { if (properties.length > 0) { return { listIds: getAssignedSnippetListsIdsFromProperties(properties), + placeholderId: parentNode.node.attrs.placeholderId, + names: parentNode.node.attrs.snippetListNames, importedResources: parentNode.node.attrs.importedResources, }; } @@ -79,8 +77,7 @@ export default class SnippetInsertRdfaComponent extends Component { diff --git a/addon/components/snippet-plugin/snippet-insert.gts b/addon/components/snippet-plugin/snippet-insert.gts index 215a4b606..f5b186f2e 100644 --- a/addon/components/snippet-plugin/snippet-insert.gts +++ b/addon/components/snippet-plugin/snippet-insert.gts @@ -11,7 +11,7 @@ import { Slice, } from '@lblod/ember-rdfa-editor'; import { SnippetPluginConfig } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin'; -import { type ImportedResourceMap } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin'; +import { type SnippetListProperties } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin'; import insertSnippet from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin/commands/insert-snippet'; import SearchModal from './search-modal'; @@ -19,10 +19,7 @@ interface Sig { Args: { controller: SayController; config: SnippetPluginConfig; - snippetListProperties: - | { listIds: string[]; importedResources: ImportedResourceMap } - | undefined; - disabled?: boolean; + listProperties: SnippetListProperties | undefined; allowMultipleSnippets?: boolean; }; } @@ -33,6 +30,9 @@ export default class SnippetInsertComponent extends Component { get controller() { return this.args.controller; } + get disabled() { + return (this.args.listProperties?.listIds.length ?? 0) === 0; + } @action openModal() { @@ -58,19 +58,16 @@ export default class SnippetInsertComponent extends Component { @action onInsert(content: string, title: string) { this.closeModal(); - this.controller.doCommand( - insertSnippet({ - content, - title, - assignedSnippetListsIds: this.args.snippetListProperties?.listIds || [], - importedResources: this.args.snippetListProperties?.importedResources, - allowMultipleSnippets: this.args.allowMultipleSnippets, - }), - ); - } - - get disabled() { - return this.args.disabled ?? false; + if (this.args.listProperties) { + this.controller.doCommand( + insertSnippet({ + content, + title, + listProperties: this.args.listProperties, + allowMultipleSnippets: this.args.allowMultipleSnippets, + }), + ); + } } } diff --git a/addon/components/snippet-plugin/snippet-list-select.gts b/addon/components/snippet-plugin/snippet-list-select.gts index a25f25e5d..b4f7a5ede 100644 --- a/addon/components/snippet-plugin/snippet-list-select.gts +++ b/addon/components/snippet-plugin/snippet-list-select.gts @@ -53,7 +53,7 @@ export default class SnippetListSelect extends Component { return getSnippetListIdsProperties(this.args.node.value); } - get assignedSnippetListsIds(): string[] { + get snippetListIds(): string[] { return getAssignedSnippetListsIdsFromProperties( this.snippetListIdsProperties, ); @@ -99,7 +99,7 @@ export default class SnippetListSelect extends Component { void; - assignedSnippetListsIds: string[] | undefined; + snippetListIds: string[] | undefined; closeModal: () => void; open: boolean; allowMultipleSnippets?: boolean; @@ -40,10 +40,8 @@ export default class SnippetListModalComponent extends Component { // Display @tracked error: unknown; - @trackedReset('args.assignedSnippetListsIds') - assignedSnippetListsIds: string[] = [ - ...(this.args.assignedSnippetListsIds ?? []), - ]; + @trackedReset('args.snippetListIds') + snippetListIds: string[] = [...(this.args.snippetListIds ?? [])]; @localCopy('args.allowMultipleSnippets') allowMultipleSnippets = false; @@ -65,7 +63,7 @@ export default class SnippetListModalComponent extends Component { @action saveAndClose() { const snippetLists = this.snippetListResource.value?.filter((snippetList) => - this.assignedSnippetListsIds.includes(snippetList.id), + this.snippetListIds.includes(snippetList.id), ); this.args.onSaveSnippetLists( snippetLists || [], @@ -73,7 +71,7 @@ export default class SnippetListModalComponent extends Component { ); this.args.closeModal(); // Clear selection for next time - this.assignedSnippetListsIds = []; + this.snippetListIds = []; } snippetListSearch = restartableTask(async () => { @@ -107,7 +105,7 @@ export default class SnippetListModalComponent extends Component { ); @action - onChange(assignedSnippetListsIds: string[]) { - this.assignedSnippetListsIds = assignedSnippetListsIds; + onChange(snippetListIds: string[]) { + this.snippetListIds = snippetListIds; } } diff --git a/addon/components/snippet-plugin/snippet-list/snippet-list-view.hbs b/addon/components/snippet-plugin/snippet-list/snippet-list-view.hbs index c08bf985d..f373efce6 100644 --- a/addon/components/snippet-plugin/snippet-list/snippet-list-view.hbs +++ b/addon/components/snippet-plugin/snippet-list/snippet-list-view.hbs @@ -53,7 +53,7 @@ {{row.label}} diff --git a/addon/components/snippet-plugin/snippet-list/snippet-list-view.ts b/addon/components/snippet-plugin/snippet-list/snippet-list-view.ts index 1226c8e49..ef38218cc 100644 --- a/addon/components/snippet-plugin/snippet-list/snippet-list-view.ts +++ b/addon/components/snippet-plugin/snippet-list/snippet-list-view.ts @@ -4,25 +4,22 @@ import { SnippetList } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snip interface Args { snippetLists: SnippetList[]; - assignedSnippetListsIds: string[]; + snippetListIds: string[]; listNameFilter: string | null; isLoading: boolean; - onChange: (assignedSnippetListsIds: string[]) => void; + onChange: (snippetListIds: string[]) => void; } export default class SnippetListViewComponent extends Component { @action onChange(snippetId: string, isSelected: boolean) { if (isSelected) { - const newSnippetListIds = [ - ...this.args.assignedSnippetListsIds, - snippetId, - ]; + const newSnippetListIds = [...this.args.snippetListIds, snippetId]; return this.args.onChange(newSnippetListIds); } - const newSnippetListIds = this.args.assignedSnippetListsIds.filter( + const newSnippetListIds = this.args.snippetListIds.filter( (id) => id !== snippetId, ); @@ -49,8 +46,7 @@ export default class SnippetListViewComponent extends Component { return; } - const isSelected = - this.args.assignedSnippetListsIds.includes(snippetListId); + const isSelected = this.args.snippetListIds.includes(snippetListId); this.onChange(snippetListId, !isSelected); } diff --git a/addon/plugins/snippet-plugin/commands/insert-snippet.ts b/addon/plugins/snippet-plugin/commands/insert-snippet.ts index 6bdf2eee1..47270fbf6 100644 --- a/addon/plugins/snippet-plugin/commands/insert-snippet.ts +++ b/addon/plugins/snippet-plugin/commands/insert-snippet.ts @@ -3,7 +3,7 @@ import { transactionCombinator } from '@lblod/ember-rdfa-editor/utils/transactio import { addPropertyToNode } from '@lblod/ember-rdfa-editor/utils/rdfa-utils'; import { recalculateNumbers } from '../../structure-plugin/recalculate-structure-numbers'; import { createSnippet } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin/nodes/snippet'; -import { type ImportedResourceMap } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin'; +import { type SnippetListProperties } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin'; import { isSome, unwrap, @@ -12,8 +12,7 @@ import { export interface InsertSnippetCommandArgs { content: string; title: string; - assignedSnippetListsIds: string[]; - importedResources?: ImportedResourceMap; + listProperties: SnippetListProperties; range?: { start: number; end: number }; allowMultipleSnippets?: boolean; } @@ -21,8 +20,7 @@ export interface InsertSnippetCommandArgs { const insertSnippet = ({ content, title, - assignedSnippetListsIds, - importedResources, + listProperties, range, allowMultipleSnippets, }: InsertSnippetCommandArgs): Command => { @@ -44,13 +42,12 @@ const insertSnippet = ({ schema: state.schema, content, title, - snippetListIds: assignedSnippetListsIds, - importedResources, + listProperties, allowMultipleSnippets, }); const addImportedResourceProperties = Object.values( - importedResources ?? {}, + listProperties.importedResources ?? {}, ) .map((linked) => { const newProperties = diff --git a/addon/plugins/snippet-plugin/commands/update-snippet-placeholder.ts b/addon/plugins/snippet-plugin/commands/update-snippet-placeholder.ts index c94d33eaf..0040b0077 100644 --- a/addon/plugins/snippet-plugin/commands/update-snippet-placeholder.ts +++ b/addon/plugins/snippet-plugin/commands/update-snippet-placeholder.ts @@ -2,7 +2,7 @@ import { Command } from '@lblod/ember-rdfa-editor'; import { addProperty, removeProperty } from '@lblod/ember-rdfa-editor/commands'; import { sayDataFactory } from '@lblod/ember-rdfa-editor/core/say-data-factory'; import { SNIPPET_LIST_RDFA_PREDICATE } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin/utils/rdfa-predicate'; -import { getSnippetUriFromId } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin'; +import { getSnippetUriFromId } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin/utils/rdfa-predicate'; import { type OutgoingTriple } from '@lblod/ember-rdfa-editor/core/rdfa-processor'; import { type ResolvedPNode } from '@lblod/ember-rdfa-editor/utils/_private/types'; import { @@ -59,7 +59,7 @@ export const updateSnippetPlaceholder = ({ }); transaction = transaction.setNodeAttribute( node.pos, - 'listNames', + 'snippetListNames', newSnippetLists.map((list) => list.label), ); transaction = transaction.setNodeAttribute( diff --git a/addon/plugins/snippet-plugin/index.ts b/addon/plugins/snippet-plugin/index.ts index 7ccc1d47c..2ccb2ea6c 100644 --- a/addon/plugins/snippet-plugin/index.ts +++ b/addon/plugins/snippet-plugin/index.ts @@ -6,6 +6,7 @@ import { } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/option'; import { dateValue } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/strings'; import { SafeString } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/types'; +import { getSnippetIdFromUri } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin/utils/rdfa-predicate'; export const DEFAULT_CONTENT_STRING = 'block+'; @@ -34,6 +35,13 @@ export class Snippet { export type ImportedResourceMap = Record>; +export type SnippetListProperties = { + placeholderId: string; + listIds: string[]; + names: string[]; + importedResources: ImportedResourceMap; +}; + export type SnippetListArgs = { id: string; label: string; @@ -41,13 +49,6 @@ export type SnippetListArgs = { importedResources: string[]; }; -const snippetListBase = 'http://lblod.data.gift/snippet-lists/'; - -export const getSnippetUriFromId = (id: string) => `${snippetListBase}${id}`; - -export const getSnippetIdFromUri = (uri: string) => - uri.replace(snippetListBase, ''); - export class SnippetList { id: string; label: string; diff --git a/addon/plugins/snippet-plugin/nodes/snippet-placeholder.ts b/addon/plugins/snippet-plugin/nodes/snippet-placeholder.ts index ee18c89cb..45eb909b1 100644 --- a/addon/plugins/snippet-plugin/nodes/snippet-placeholder.ts +++ b/addon/plugins/snippet-plugin/nodes/snippet-placeholder.ts @@ -20,11 +20,12 @@ import { hasOutgoingNamedNodeTriple } from '@lblod/ember-rdfa-editor-lblod-plugi import { getTranslationFunction } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/translation'; import { jsonParse } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/strings'; import { - getSnippetUriFromId, + type SnippetListProperties, type ImportedResourceMap, type SnippetList, } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin'; -import { SNIPPET_LIST_RDFA_PREDICATE } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin/utils/rdfa-predicate'; +import { tripleForSnippetListId } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin/utils/rdfa-predicate'; +import { OutgoingTriple } from '@lblod/ember-rdfa-editor/core/rdfa-processor'; export function importedResourcesFromSnippetLists( lists: SnippetList[], @@ -38,27 +39,56 @@ export function importedResourcesFromSnippetLists( ); } -export function createSnippetPlaceholder( - lists: SnippetList[], - schema: Schema, - allowMultipleSnippets?: boolean, -) { +type CreateSnippetPlaceholderArgs = { + schema: Schema; + allowMultipleSnippets?: boolean; +} & ( + | { + listProperties: SnippetListProperties; + } + | { + lists: SnippetList[]; + } +); + +export function createSnippetPlaceholder({ + schema, + allowMultipleSnippets, + ...args +}: CreateSnippetPlaceholderArgs) { + let additionalProperties: OutgoingTriple[]; + let listProps: Omit; + if ('lists' in args) { + listProps = { + // This is a completely new placeholder, so new id + placeholderId: uuidv4(), + names: args.lists.map((list) => list.label), + importedResources: importedResourcesFromSnippetLists(args.lists), + }; + additionalProperties = args.lists.map((list) => + tripleForSnippetListId(list.id), + ); + } else { + // Replacing the last snippet, so keep the id + listProps = args.listProperties; + additionalProperties = args.listProperties.listIds.map( + tripleForSnippetListId, + ); + } const mappingResource = `http://example.net/lblod-snippet-placeholder/${uuidv4()}`; return schema.nodes.snippet_placeholder.create({ rdfaNodeType: 'resource', - listNames: lists.map((list) => list.label), + placeholderId: listProps.placeholderId, + snippetListNames: listProps.names, subject: mappingResource, properties: [ { predicate: RDF('type').full, object: sayDataFactory.namedNode(EXT('SnippetPlaceholder').full), }, - ...lists.map((list) => ({ - predicate: SNIPPET_LIST_RDFA_PREDICATE.full, - object: sayDataFactory.namedNode(getSnippetUriFromId(list.id)), - })), + ...additionalProperties, ], - importedResources: importedResourcesFromSnippetLists(lists), + importedResources: listProps.importedResources, allowMultipleSnippets, }); } @@ -73,20 +103,22 @@ const emberNodeConfig: EmberNodeConfig = { attrs: { ...rdfaAttrSpec({ rdfaAware: true }), typeof: { default: EXT('SnippetPlaceholder') }, - listNames: { default: [] }, + placeholderId: { default: '' }, + snippetListNames: { default: [] }, importedResources: { default: {} }, allowMultipleSnippets: { default: false }, }, component: SnippetPlaceholderComponent, serialize(node, editorState) { const t = getTranslationFunction(editorState); + const listNames = node.attrs.snippetListNames as string[]; return renderRdfaAware({ renderable: node, tag: 'div', attrs: { ...node.attrs, class: 'say-snippet-placeholder-node', - 'data-list-names': (node.attrs.listNames as string[]).join(','), + 'data-list-names': listNames && JSON.stringify(listNames), 'data-imported-resources': JSON.stringify(node.attrs.importedResources), 'data-allow-multiple-snippets': node.attrs.allowMultipleSnippets, }, @@ -112,9 +144,20 @@ const emberNodeConfig: EmberNodeConfig = { EXT('SnippetPlaceholder'), ) ) { + let snippetListNames = jsonParse( + node.getAttribute('data-list-names'), + ); + if (!snippetListNames) { + // We might have an older version which is comma separated + snippetListNames = node.getAttribute('data-list-names')?.split(','); + } return { ...rdfaAttrs, - listNames: node.getAttribute('data-list-names')?.split(','), + // Generate a placeholderId any time we deserialise, this way we don't need to handle + // generating new ids whenever we re-use parts of a document (e.g. copy-paste or + // placeholders inside snippets) + placeholderId: uuidv4(), + snippetListNames, importedResources: jsonParse( node.getAttribute('data-imported-resources'), ), diff --git a/addon/plugins/snippet-plugin/nodes/snippet.ts b/addon/plugins/snippet-plugin/nodes/snippet.ts index cb8b44d22..50b9eecc2 100644 --- a/addon/plugins/snippet-plugin/nodes/snippet.ts +++ b/addon/plugins/snippet-plugin/nodes/snippet.ts @@ -32,7 +32,7 @@ import { hasOutgoingNamedNodeTriple } from '@lblod/ember-rdfa-editor-lblod-plugi import { jsonParse } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/strings'; import { DEFAULT_CONTENT_STRING, - type ImportedResourceMap, + SnippetListProperties, type SnippetPluginConfig, } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin'; @@ -60,11 +60,10 @@ function outgoingFromBacklink( interface CreateSnippetArgs { schema: Schema; + allowMultipleSnippets?: boolean; content: string; title: string; - snippetListIds: string[]; - importedResources?: ImportedResourceMap; - allowMultipleSnippets?: boolean; + listProperties: SnippetListProperties; } /** @@ -77,9 +76,13 @@ export function createSnippet({ schema, content, title, - snippetListIds, - importedResources, allowMultipleSnippets, + listProperties: { + listIds: snippetListIds, + names: snippetListNames, + importedResources, + placeholderId, + }, }: CreateSnippetArgs): [PNode, Map] { // Replace instances of linked to uris with the resources that exist in the outer document. let replacedContent = content; @@ -100,7 +103,9 @@ export function createSnippet({ const node = schema.node( 'snippet', { - assignedSnippetListsIds: snippetListIds, + placeholderId, + snippetListIds, + snippetListNames, title, subject: `http://data.lblod.info/snippets/${uuidv4()}`, importedResources, @@ -153,7 +158,9 @@ const emberNodeConfig = (options: SnippetPluginConfig): EmberNodeConfig => ({ ], }, rdfaNodeType: { default: 'resource' }, - assignedSnippetListsIds: { default: [] }, + placeholderId: { default: '' }, + snippetListNames: { default: [] }, + snippetListIds: { default: [] }, importedResources: { default: {} }, title: { default: '' }, config: { default: options }, @@ -162,14 +169,17 @@ const emberNodeConfig = (options: SnippetPluginConfig): EmberNodeConfig => ({ component: SnippetComponent, content: options.allowedContent || DEFAULT_CONTENT_STRING, serialize(node) { + const listNames = node.attrs.snippetListNames as string[]; return renderRdfaAware({ renderable: node, tag: 'div', attrs: { ...node.attrs, + 'data-snippet-placeholder-id': node.attrs.placeholderId, 'data-assigned-snippet-ids': ( - node.attrs.assignedSnippetListsIds as string[] - ).join(','), + node.attrs.snippetListIds as string[] + )?.join(','), + 'data-list-names': listNames && JSON.stringify(listNames), 'data-imported-resources': JSON.stringify(node.attrs.importedResources), 'data-snippet-title': node.attrs.title, 'data-allow-multiple-snippets': node.attrs.allowMultipleSnippets, @@ -186,11 +196,19 @@ const emberNodeConfig = (options: SnippetPluginConfig): EmberNodeConfig => ({ if ( hasOutgoingNamedNodeTriple(rdfaAttrs, RDF('type'), EXT('Snippet')) ) { + // For older documents without placeholder ids, treat each inserted snippet separately. + // This means that pressing 'remove snippet' will add a placeholder in it's place, which + // is not expected, but simple to remove (hitting backspace). This is better than + // risking having no ability to insert another snippet. + const placeholderId = + node.getAttribute('data-snippet-placeholder-id') || uuidv4(); return { ...rdfaAttrs, - assignedSnippetListsIds: node + placeholderId, + snippetListIds: node .getAttribute('data-assigned-snippet-ids') ?.split(','), + snippetListNames: jsonParse(node.getAttribute('data-list-names')), importedResources: jsonParse( node.getAttribute('data-imported-resources'), ), diff --git a/addon/plugins/snippet-plugin/utils/fetch-data.ts b/addon/plugins/snippet-plugin/utils/fetch-data.ts index 10d69ed6a..a5244945c 100644 --- a/addon/plugins/snippet-plugin/utils/fetch-data.ts +++ b/addon/plugins/snippet-plugin/utils/fetch-data.ts @@ -6,7 +6,7 @@ import { } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/sparql-helpers'; import { Snippet, SnippetList, SnippetListArgs } from '../index'; -type Filter = { name?: string; assignedSnippetListIds?: string[] }; +type Filter = { name?: string; snippetListIds?: string[] }; export type OrderBy = | 'label' | 'created-on' @@ -16,7 +16,7 @@ export type OrderBy = | null; type Pagination = { pageNumber: number; pageSize: number }; -const buildSnippetCountQuery = ({ name, assignedSnippetListIds }: Filter) => { +const buildSnippetCountQuery = ({ name, snippetListIds }: Filter) => { return /* sparql */ ` PREFIX schema: PREFIX dct: @@ -42,8 +42,8 @@ const buildSnippetCountQuery = ({ name, assignedSnippetListIds }: Filter) => { : '' } ${ - assignedSnippetListIds && assignedSnippetListIds.length - ? `FILTER (?snippetListId IN (${assignedSnippetListIds + snippetListIds && snippetListIds.length + ? `FILTER (?snippetListId IN (${snippetListIds .map((from) => sparqlEscapeString(from)) .join(', ')}))` : '' @@ -69,7 +69,7 @@ const buildSnippetCountQuery = ({ name, assignedSnippetListIds }: Filter) => { // }; const buildSnippetFetchQuery = ({ - filter: { name, assignedSnippetListIds }, + filter: { name, snippetListIds }, pagination: { pageSize, pageNumber }, }: { filter: Filter; @@ -106,8 +106,8 @@ const buildSnippetFetchQuery = ({ : '' } ${ - assignedSnippetListIds && assignedSnippetListIds.length - ? `FILTER (?snippetListId IN (${assignedSnippetListIds + snippetListIds && snippetListIds.length + ? `FILTER (?snippetListId IN (${snippetListIds .map((from) => sparqlEscapeString(from)) .join(', ')}))` : '' @@ -173,7 +173,7 @@ export const fetchSnippets = async ({ filter: Filter; pagination: Pagination; }) => { - if (!filter.assignedSnippetListIds?.length) { + if (!filter.snippetListIds?.length) { return { totalCount: 0, results: [] }; } diff --git a/addon/plugins/snippet-plugin/utils/rdfa-predicate.ts b/addon/plugins/snippet-plugin/utils/rdfa-predicate.ts index 3de38f8b7..961872031 100644 --- a/addon/plugins/snippet-plugin/utils/rdfa-predicate.ts +++ b/addon/plugins/snippet-plugin/utils/rdfa-predicate.ts @@ -1,11 +1,25 @@ -import { PNode } from '@lblod/ember-rdfa-editor'; -import { OutgoingTriple } from '@lblod/ember-rdfa-editor/core/rdfa-processor'; -import { getSnippetIdFromUri } from '@lblod/ember-rdfa-editor-lblod-plugins/plugins/snippet-plugin'; +import { type PNode } from '@lblod/ember-rdfa-editor'; +import { type OutgoingTriple } from '@lblod/ember-rdfa-editor/core/rdfa-processor'; +import { sayDataFactory } from '@lblod/ember-rdfa-editor/core/say-data-factory'; import { SAY } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/constants'; import { getOutgoingTripleList } from '@lblod/ember-rdfa-editor-lblod-plugins/utils/namespace'; export const SNIPPET_LIST_RDFA_PREDICATE = SAY('allowedSnippetList'); +const snippetListBase = 'http://lblod.data.gift/snippet-lists/'; + +export const getSnippetUriFromId = (id: string) => `${snippetListBase}${id}`; + +export const getSnippetIdFromUri = (uri: string) => + uri.replace(snippetListBase, ''); + +export function tripleForSnippetListId(id: string) { + return { + predicate: SNIPPET_LIST_RDFA_PREDICATE.full, + object: sayDataFactory.namedNode(getSnippetUriFromId(id)), + }; +} + export const getSnippetListIdsProperties = (node: PNode) => { return getOutgoingTripleList(node.attrs, SNIPPET_LIST_RDFA_PREDICATE); }; diff --git a/addon/utils/has-descendant.ts b/addon/utils/has-descendant.ts new file mode 100644 index 000000000..a89b104f1 --- /dev/null +++ b/addon/utils/has-descendant.ts @@ -0,0 +1,19 @@ +import { type PNode } from '@lblod/ember-rdfa-editor'; + +export function hasDecendant( + nodeToDescendInto: PNode, + matchFunc: (node: PNode) => boolean, +) { + let foundMatch = false; + nodeToDescendInto.descendants((node) => { + // Already found a match, stop descending or checking + if (foundMatch) return false; + if (matchFunc(node)) { + foundMatch = true; + return false; + } + return true; + }); + + return foundMatch; +}