From 65cb5987bf675dbbb50b14c2299da447aaa76206 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Wed, 1 Jul 2026 19:40:17 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20implement=20reference=20versioning=20fu?= =?UTF-8?q?nctionality=20across=20various=20com=E2=80=A6=20(#10941)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement reference versioning functionality across various components and plugins Signed-off-by: Denis Bykhov * fix: simplify provider function calls and label retrieval in MentionPopup component Signed-off-by: Denis Bykhov * refactor: improve readability of version selection and reference handling in MentionVersionPopup and reference.ts Signed-off-by: Denis Bykhov * feat: add CardReferenceObjectProvider and integrate into card model and resources Signed-off-by: Denis Bykhov --------- Signed-off-by: Denis Bykhov --- .../core/packages/text/src/nodes/reference.ts | 19 ++- models/card/src/index.ts | 4 + models/controlled-documents/src/index.ts | 4 + models/view/src/index.ts | 8 ++ plugins/card-resources/src/index.ts | 2 + plugins/card-resources/src/plugin.ts | 5 +- plugins/card-resources/src/utils.ts | 20 +++ .../src/index.ts | 2 + .../src/plugin.ts | 7 +- .../src/utils.ts | 25 +++- plugins/text-editor-assets/lang/en.json | 5 +- plugins/text-editor-assets/lang/ru.json | 11 +- .../src/components/MentionPopup.svelte | 125 +++++++++++++++--- .../src/components/MentionVersionPopup.svelte | 90 +++++++++++++ .../src/components/extension/reference.ts | 12 +- plugins/text-editor-resources/src/plugin.ts | 3 +- plugins/view/src/index.ts | 2 + plugins/view/src/types.ts | 17 +++ 18 files changed, 328 insertions(+), 33 deletions(-) create mode 100644 plugins/text-editor-resources/src/components/MentionVersionPopup.svelte diff --git a/foundations/core/packages/text/src/nodes/reference.ts b/foundations/core/packages/text/src/nodes/reference.ts index 613a51eca1..8a9bbf685a 100644 --- a/foundations/core/packages/text/src/nodes/reference.ts +++ b/foundations/core/packages/text/src/nodes/reference.ts @@ -22,6 +22,7 @@ export interface ReferenceNodeProps { id: Ref objectclass: Ref> label: string + fixed?: boolean } export interface ReferenceOptions { @@ -42,7 +43,19 @@ export const ReferenceNode = Node.create({ return { id: getDataAttribute('id'), objectclass: getDataAttribute('objectclass'), - label: getDataAttribute('label') + label: getDataAttribute('label'), + fixed: { + default: null, + parseHTML: (element) => element.getAttribute('data-fixed') === 'true', + renderHTML: (attributes) => { + if (attributes.fixed !== true) { + return {} + } + return { + 'data-fixed': 'true' + } + } + } } }, @@ -91,6 +104,7 @@ function getAttrs (el: HTMLSpanElement): Attrs | false { const id = el.dataset.id?.trim() const label = el.dataset.label?.trim() const objectclass = el.dataset.objectclass?.trim() + const fixed = el.getAttribute('data-fixed') === 'true' if (id == null || label == null || objectclass == null) { return false @@ -99,6 +113,7 @@ function getAttrs (el: HTMLSpanElement): Attrs | false { return { id, label, - objectclass + objectclass, + fixed: fixed || null } } diff --git a/models/card/src/index.ts b/models/card/src/index.ts index a8572e163d..8726893bb5 100644 --- a/models/card/src/index.ts +++ b/models/card/src/index.ts @@ -963,6 +963,10 @@ export function createModel (builder: Builder): void { titleProvider: card.function.CardTitleProvider }) + builder.mixin(card.class.Card, core.class.Class, view.mixin.ReferenceObjectProvider, { + provider: card.function.CardReferenceObjectProvider + }) + builder.mixin(card.class.Card, core.class.Class, view.mixin.LinkProvider, { encode: card.function.GetCardLink }) diff --git a/models/controlled-documents/src/index.ts b/models/controlled-documents/src/index.ts index 168d30159a..e699731090 100644 --- a/models/controlled-documents/src/index.ts +++ b/models/controlled-documents/src/index.ts @@ -471,6 +471,10 @@ export function createModel (builder: Builder): void { titleProvider: documents.function.DocumentMetaTitleProvider }) + builder.mixin(documents.class.DocumentMeta, core.class.Class, view.mixin.ReferenceVersionsProvider, { + provider: documents.function.DocumentMetaReferenceVersionsProvider + }) + builder.mixin(documents.class.DocumentMeta, core.class.Class, view.mixin.LinkProvider, { encode: documents.function.GetDocumentMetaLinkFragment }) diff --git a/models/view/src/index.ts b/models/view/src/index.ts index d8670ffb18..b5b501624c 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -92,6 +92,8 @@ import { type ObjectValidator, type PreviewPresenter, type ReferenceObjectProvider, + type ReferenceVersion, + type ReferenceVersionsProvider, type SortFunc, type SpaceHeader, type SpaceName, @@ -275,6 +277,11 @@ export class TReferenceObjectProvider extends TClass implements ReferenceObjectP provider!: Resource<(client: Client, ref: Ref, doc?: T) => Promise> } +@Mixin(view.mixin.ReferenceVersionsProvider, core.class.Class) +export class TReferenceVersionsProvider extends TClass implements ReferenceVersionsProvider { + provider!: Resource<(client: Client, ref: Ref, doc?: T) => Promise> +} + @Mixin(view.mixin.ObjectTooltip, core.class.Class) export class TObjectTooltip extends TClass implements ObjectTooltip { provider!: Resource<(client: Client, doc?: Doc | null) => Promise> @@ -523,6 +530,7 @@ export function createModel (builder: Builder): void { TGroupping, TObjectIdentifier, TReferenceObjectProvider, + TReferenceVersionsProvider, TObjectTooltip, TObjectIcon, TAttrPresenter, diff --git a/plugins/card-resources/src/index.ts b/plugins/card-resources/src/index.ts index c527522011..95b4d58fa1 100644 --- a/plugins/card-resources/src/index.ts +++ b/plugins/card-resources/src/index.ts @@ -28,6 +28,7 @@ import { editSpace, cardCustomLinkEncode, cardCustomLinkMatch, + cardReferenceObjectProvider, openCardInSidebar, checkRelationsSectionVisibility, checkOldMessagesSectionVisibility, @@ -169,6 +170,7 @@ export default async (): Promise => ({ }, function: { CardTitleProvider: getCardTitle, + CardReferenceObjectProvider: cardReferenceObjectProvider, GetCardLink: getCardLink, CardCustomLinkMatch: cardCustomLinkMatch, CardCustomLinkEncode: cardCustomLinkEncode, diff --git a/plugins/card-resources/src/plugin.ts b/plugins/card-resources/src/plugin.ts index e0b057e5bd..c0dd8e9d35 100644 --- a/plugins/card-resources/src/plugin.ts +++ b/plugins/card-resources/src/plugin.ts @@ -14,7 +14,7 @@ // import card, { type Card, cardId } from '@hcengineering/card' -import { type Permission, type Doc, type Ref } from '@hcengineering/core' +import { type Client, type Permission, type Doc, type Ref } from '@hcengineering/core' import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform' import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/presentation' import { type AnyComponent } from '@hcengineering/ui/src/types' @@ -62,6 +62,9 @@ export default mergeIds(cardId, card, { }, function: { CardFactory: '' as Resource<(props?: Record) => Promise | undefined>>, + CardReferenceObjectProvider: '' as Resource< + (client: Client, ref: Ref, doc?: T) => Promise + >, FormatCardMarkdownValue: '' as Resource, CheckChildrenSectionVisibility: '' as Resource<(doc: Card) => Promise> }, diff --git a/plugins/card-resources/src/utils.ts b/plugins/card-resources/src/utils.ts index 67867efc0e..154c68f083 100644 --- a/plugins/card-resources/src/utils.ts +++ b/plugins/card-resources/src/utils.ts @@ -486,6 +486,26 @@ export async function getCardTitle (client: TxOperations, ref: Ref, doc?: return ids + ' ' + object.title + ' ' + version } +export async function cardReferenceObjectProvider ( + client: Client, + ref: Ref, + doc?: T +): Promise { + const object = + (doc as unknown as Card | undefined) ?? (await client.findOne(card.class.Card, { _id: ref as any as Ref })) + if (object === undefined) return + + const versioningEnabled = client + .getHierarchy() + .classHierarchyMixin(object._class, core.mixin.VersionableClass)?.enabled + if (versioningEnabled !== true) return object + + const baseId = object.baseId ?? object._id + if (object.isLatest === true) return object + + return (await client.findOne(object._class, { baseId, isLatest: true } as any)) ?? object +} + export async function getCardLink (doc: Card): Promise { const loc = getCurrentResolvedLocation() loc.path.length = 2 diff --git a/plugins/controlled-documents-resources/src/index.ts b/plugins/controlled-documents-resources/src/index.ts index b1953e3ac5..327ba9bf91 100644 --- a/plugins/controlled-documents-resources/src/index.ts +++ b/plugins/controlled-documents-resources/src/index.ts @@ -103,6 +103,7 @@ import { createFolder, createTemplate, deleteFolder, + documentMetaReferenceVersionsProvider, documentIdentifierProvider, controlledDocumentReferenceObjectProvider, projectDocumentReferenceObjectProvider, @@ -442,6 +443,7 @@ export default async (): Promise => ({ DocumentIdentifierProvider: documentIdentifierProvider, ControlledDocumentReferenceObjectProvider: controlledDocumentReferenceObjectProvider, ProjectDocumentReferenceObjectProvider: projectDocumentReferenceObjectProvider, + DocumentMetaReferenceVersionsProvider: documentMetaReferenceVersionsProvider, ControlledDocumentTitleProvider: getControlledDocumentTitle, DocumentMetaTitleProvider: getDocumentMetaTitle, FormatDocumentMarkdownValue: formatControlledDocumentValue, diff --git a/plugins/controlled-documents-resources/src/plugin.ts b/plugins/controlled-documents-resources/src/plugin.ts index 9b34a5e35f..e3f27f0503 100644 --- a/plugins/controlled-documents-resources/src/plugin.ts +++ b/plugins/controlled-documents-resources/src/plugin.ts @@ -21,7 +21,7 @@ import documents, { import { type Client, type Doc, type Ref, type Space } from '@hcengineering/core' import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform' import { type AnyComponent, type Location } from '@hcengineering/ui/src/types' -import type { KeyFilter, SortFunc, ViewActionAvailabilityFunction } from '@hcengineering/view' +import type { KeyFilter, ReferenceVersion, SortFunc, ViewActionAvailabilityFunction } from '@hcengineering/view' export default mergeIds(documentsId, documents, { component: { @@ -256,6 +256,9 @@ export default mergeIds(documentsId, documents, { CanPrintDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise>, CanTransferDocument: '' as Resource<(doc?: Doc | Doc[]) => Promise>, ControlledDocumentTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise>, - DocumentMetaTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise> + DocumentMetaTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: Doc) => Promise>, + DocumentMetaReferenceVersionsProvider: '' as Resource< + (client: Client, ref: Ref, doc?: Doc) => Promise + > } }) diff --git a/plugins/controlled-documents-resources/src/utils.ts b/plugins/controlled-documents-resources/src/utils.ts index 4753141797..d7cc3609d1 100644 --- a/plugins/controlled-documents-resources/src/utils.ts +++ b/plugins/controlled-documents-resources/src/utils.ts @@ -64,7 +64,7 @@ import { createQuery, getClient, MessageBox } from '@hcengineering/presentation' import request, { type Request, RequestStatus } from '@hcengineering/request' import { isEmptyMarkup } from '@hcengineering/text' import { type Location, getUserTimezone, showPopup } from '@hcengineering/ui' -import { type KeyFilter } from '@hcengineering/view' +import { type KeyFilter, type ReferenceVersion } from '@hcengineering/view' import { makeRank } from '@hcengineering/rank' import { getProjectDocumentLink } from './navigation' @@ -842,6 +842,29 @@ export async function getDocumentMetaTitle ( return object.title + ` (${hint})` } +export async function documentMetaReferenceVersionsProvider ( + client: Client, + ref: Ref +): Promise { + const versions = await client.findAll( + documents.class.ControlledDocument, + { attachedTo: ref }, + { + sort: { + major: SortingOrder.Descending, + minor: SortingOrder.Descending + } + } + ) + + return versions.map((doc) => ({ + id: doc._id, + objectclass: doc._class, + label: `${doc.title} (${getDocumentVersionString(doc)})`, + fixed: true + })) +} + export async function controlledDocumentReferenceObjectProvider ( client: Client, ref: Ref, diff --git a/plugins/text-editor-assets/lang/en.json b/plugins/text-editor-assets/lang/en.json index 1bc5268d6a..986eba0696 100644 --- a/plugins/text-editor-assets/lang/en.json +++ b/plugins/text-editor-assets/lang/en.json @@ -87,6 +87,7 @@ "UnableToLoadEmbeddedContent": "Link preview couldn't be loaded due to permission settings or unsupported content", "CannotConnectToCollaborationService": "Cannot connect to collaboration service", "SourceURL": "Source URL", - "SelectedDocuments": "Selected documents ({count, plural, =1 {1 item} other {# items}})" + "SelectedDocuments": "Selected documents ({count, plural, =1 {1 item} other {# items}})", + "LatestVersion": "Latest" } -} \ No newline at end of file +} diff --git a/plugins/text-editor-assets/lang/ru.json b/plugins/text-editor-assets/lang/ru.json index fd2bc0d695..380e1704b4 100644 --- a/plugins/text-editor-assets/lang/ru.json +++ b/plugins/text-editor-assets/lang/ru.json @@ -82,10 +82,11 @@ "CannotConnectToCollaborationService": "Не удалось подключиться к сервису совместной работы", "SourceURL": "Исходный URL", "SelectedDocuments": "Выбранные документы ({count, plural, =1 {1 элемент} other {# элементов}})", - "Highlight": "Highlight", - "Subscript": "Subscript", - "Superscript": "Superscript", - "Mathematics": "Mathematics", - "MathematicsBlock": "Mathematics block" + "Highlight": "Выделение", + "Subscript": "Нижний индекс", + "Superscript": "Верхний индекс", + "Mathematics": "Формула", + "MathematicsBlock": "Блок формул", + "LatestVersion": "Последняя" } } diff --git a/plugins/text-editor-resources/src/components/MentionPopup.svelte b/plugins/text-editor-resources/src/components/MentionPopup.svelte index af6f6666f4..18a3dd9345 100644 --- a/plugins/text-editor-resources/src/components/MentionPopup.svelte +++ b/plugins/text-editor-resources/src/components/MentionPopup.svelte @@ -14,19 +14,22 @@ // limitations under the License. --> + +