From 7d0034c123f858f9ec4a1c8a22d02707e117b21a Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Tue, 12 Aug 2025 22:20:05 +0700 Subject: [PATCH] EQMS-1635 Add generic action for copying link (#9668) Signed-off-by: Alexander Onnikov --- models/card/package.json | 1 + models/card/src/index.ts | 3 ++ models/card/src/plugin.ts | 3 +- models/chunter/src/actions.ts | 3 +- models/document/src/index.ts | 21 ------------ models/recruit/src/index.ts | 6 ++-- models/tracker/src/actions.ts | 3 +- models/view/src/index.ts | 18 ++++++++++ models/view/src/plugin.ts | 7 ++-- plugins/document-resources/src/index.ts | 2 -- plugins/document-resources/src/plugin.ts | 1 - plugins/document/src/plugin.ts | 1 - plugins/view-assets/lang/cs.json | 3 +- plugins/view-assets/lang/de.json | 3 +- plugins/view-assets/lang/en.json | 3 +- plugins/view-assets/lang/es.json | 3 +- plugins/view-assets/lang/fr.json | 3 +- plugins/view-assets/lang/it.json | 3 +- plugins/view-assets/lang/ja.json | 3 +- plugins/view-assets/lang/pt.json | 3 +- plugins/view-assets/lang/ru.json | 3 +- plugins/view-assets/lang/zh.json | 3 +- plugins/view-resources/src/index.ts | 6 ++-- plugins/view-resources/src/utils.ts | 34 +++++++++++++++++-- plugins/view/src/index.ts | 4 ++- .../sanity/tests/documents/documents.spec.ts | 2 +- 26 files changed, 97 insertions(+), 48 deletions(-) diff --git a/models/card/package.json b/models/card/package.json index 4959614ee4..d81e49a658 100644 --- a/models/card/package.json +++ b/models/card/package.json @@ -42,6 +42,7 @@ "@hcengineering/time": "^0.6.0", "@hcengineering/card": "^0.6.0", "@hcengineering/model-attachment": "^0.6.0", + "@hcengineering/model-guest": "^0.6.0", "@hcengineering/model-presentation": "^0.6.0", "@hcengineering/model-preference": "^0.6.0", "@hcengineering/workbench": "^0.6.16", diff --git a/models/card/src/index.ts b/models/card/src/index.ts index 7b0b2ffc8e..ed2364a393 100644 --- a/models/card/src/index.ts +++ b/models/card/src/index.ts @@ -57,6 +57,7 @@ import { } from '@hcengineering/model' import attachment from '@hcengineering/model-attachment' import { TAttachedDoc, TClass, TDoc, TMixin, TSpace } from '@hcengineering/model-core' +import { createPublicLinkAction } from '@hcengineering/model-guest' import presentation from '@hcengineering/model-presentation' import setting from '@hcengineering/model-setting' import view, { createAction, type Viewlet } from '@hcengineering/model-view' @@ -875,6 +876,8 @@ export function createModel (builder: Builder): void { match: card.function.CardCustomLinkMatch, encode: card.function.CardCustomLinkEncode }) + + createPublicLinkAction(builder, card.class.Card, card.action.PublicLink) } function defineTabs (builder: Builder): void { diff --git a/models/card/src/plugin.ts b/models/card/src/plugin.ts index 4ad6d6420a..d56b266260 100644 --- a/models/card/src/plugin.ts +++ b/models/card/src/plugin.ts @@ -34,7 +34,8 @@ export default mergeIds(cardId, card, { action: { DeleteMasterTag: '' as Ref, SetParent: '' as Ref>, - UnsetParent: '' as Ref> + UnsetParent: '' as Ref>, + PublicLink: '' as Ref> }, category: { Card: '' as Ref, diff --git a/models/chunter/src/actions.ts b/models/chunter/src/actions.ts index 17562c3e02..0dd1619c17 100644 --- a/models/chunter/src/actions.ts +++ b/models/chunter/src/actions.ts @@ -97,7 +97,8 @@ function defineMessageActions (builder: Builder): void { mode: ['context', 'browser'], application: chunter.app.Chunter, group: 'copy' - } + }, + override: [view.action.CopyLink] }, chunter.action.CopyChatMessageLink ) diff --git a/models/document/src/index.ts b/models/document/src/index.ts index 94eb41ae01..024909f6ca 100644 --- a/models/document/src/index.ts +++ b/models/document/src/index.ts @@ -369,27 +369,6 @@ function defineDocument (builder: Builder): void { document.action.CreateChildDocument ) - createAction( - builder, - { - action: view.actionImpl.CopyTextToClipboard, - actionProps: { - textProvider: document.function.GetDocumentLink - }, - label: document.string.CopyDocumentUrl, - icon: view.icon.CopyLink, - input: 'focus', - category: document.category.Document, - target: document.class.Document, - context: { - mode: ['context', 'browser'], - application: document.app.Documents, - group: 'copy' - } - }, - document.action.CopyDocumentLink - ) - createAction( builder, { diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index d38c781ca6..1db7678b7c 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -1286,7 +1286,8 @@ export function createModel (builder: Builder): void { mode: ['context', 'browser'], application: recruit.app.Recruit, group: 'copy' - } + }, + override: [view.action.CopyLink] }, recruit.action.CopyApplicationLink ) @@ -1306,7 +1307,8 @@ export function createModel (builder: Builder): void { mode: ['context', 'browser'], application: recruit.app.Recruit, group: 'copy' - } + }, + override: [view.action.CopyLink] }, recruit.action.CopyCandidateLink ) diff --git a/models/tracker/src/actions.ts b/models/tracker/src/actions.ts index c3de1615f2..27784de88c 100644 --- a/models/tracker/src/actions.ts +++ b/models/tracker/src/actions.ts @@ -633,7 +633,8 @@ export function createActions (builder: Builder, issuesId: string, componentsId: mode: ['context', 'browser'], application: tracker.app.Tracker, group: 'copy' - } + }, + override: [view.action.CopyLink] }, tracker.action.CopyIssueLink ) diff --git a/models/view/src/index.ts b/models/view/src/index.ts index d24dc9d73a..f9e012dd24 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -746,6 +746,24 @@ export function createModel (builder: Builder): void { view.action.Delete ) + createAction( + builder, + { + action: view.actionImpl.CopyTextToClipboard, + actionProps: { + textProvider: view.function.GetLink + }, + label: view.string.CopyLink, + icon: view.icon.CopyLink, + category: view.category.General, + input: 'any', + target: core.class.Doc, + context: { mode: ['context', 'browser'] }, + visibilityTester: view.function.CanCopyLink + }, + view.action.CopyLink + ) + createAction( builder, { diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index 450ff6e8ef..527a1b613b 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -120,7 +120,8 @@ export default mergeIds(viewId, view, { Navigation: '' as IntlString, Editor: '' as IntlString, MarkdownFormatting: '' as IntlString, - HideArchived: '' as IntlString + HideArchived: '' as IntlString, + CopyLink: '' as IntlString }, function: { FilterArrayAllResult: '' as FilterFunction, @@ -153,7 +154,9 @@ export default mergeIds(viewId, view, { CanLeaveSpace: '' as Resource<(doc?: Doc | Doc[]) => Promise>, IsClipboardAvailable: '' as Resource<(doc?: Doc | Doc[]) => Promise>, BlobImageMetadata: '' as Resource<(file: FileOrBlob, blob: Ref) => Promise>, - BlobVideoMetadata: '' as Resource<(file: FileOrBlob, blob: Ref) => Promise> + BlobVideoMetadata: '' as Resource<(file: FileOrBlob, blob: Ref) => Promise>, + GetLink: '' as Resource<(doc?: Doc | Doc[]) => Promise>, + CanCopyLink: '' as Resource<(doc?: Doc | Doc[]) => Promise> }, pipeline: { PresentationMiddleware: '' as Ref, diff --git a/plugins/document-resources/src/index.ts b/plugins/document-resources/src/index.ts index b4b08519df..058c0b021d 100644 --- a/plugins/document-resources/src/index.ts +++ b/plugins/document-resources/src/index.ts @@ -51,7 +51,6 @@ import { documentTitleProvider, getDocumentLink, getDocumentLinkId, - getDocumentUrl, parseDocumentId, resolveLocation } from './utils' @@ -192,7 +191,6 @@ export default async (): Promise => ({ UnlockContent: unlockContent }, function: { - GetDocumentLink: getDocumentUrl, GetObjectLinkFragment: getDocumentLink, DocumentTitleProvider: documentTitleProvider, CanLockDocument: canLockDocument, diff --git a/plugins/document-resources/src/plugin.ts b/plugins/document-resources/src/plugin.ts index 28e46f7f2a..2b1382943b 100644 --- a/plugins/document-resources/src/plugin.ts +++ b/plugins/document-resources/src/plugin.ts @@ -28,7 +28,6 @@ export default mergeIds(documentId, document, { }, function: { DocumentTitleProvider: '' as Resource<(client: Client, ref: Ref, doc?: T) => Promise>, - GetDocumentLink: '' as Resource<(doc: Doc, props: Record) => Promise>, GetObjectLinkFragment: '' as Resource<(doc: Doc, props: Record) => Promise>, GetDocumentLinkId: '' as Resource<(doc: Doc) => Promise>, ParseDocumentId: '' as Resource<(id: string) => Promise | undefined>> diff --git a/plugins/document/src/plugin.ts b/plugins/document/src/plugin.ts index 7613d355f7..a8a47f81a0 100644 --- a/plugins/document/src/plugin.ts +++ b/plugins/document/src/plugin.ts @@ -44,7 +44,6 @@ export const documentPlugin = plugin(documentId, { DocumentSearchIcon: '' as AnyComponent }, action: { - CopyDocumentLink: '' as Ref>, CreateChildDocument: '' as Ref, CreateDocument: '' as Ref, EditTeamspace: '' as Ref diff --git a/plugins/view-assets/lang/cs.json b/plugins/view-assets/lang/cs.json index d56e5999a7..a3fa6c77a7 100644 --- a/plugins/view-assets/lang/cs.json +++ b/plugins/view-assets/lang/cs.json @@ -146,6 +146,7 @@ "PermissionWarningMessage": "Vaše současná role má omezená oprávnění. Chcete-li odemknout tuto a další pokročilé funkce, požádejte správce svého pracovního prostoru o zvýšení úrovně přístupu!", "Icon": "Ikona", "Color": "Barva", - "AutomationOnly": "Pouze automatizace" + "AutomationOnly": "Pouze automatizace", + "CopyLink": "Kopírovat odkaz" } } diff --git a/plugins/view-assets/lang/de.json b/plugins/view-assets/lang/de.json index 3c3bd60a5e..30faa135cc 100644 --- a/plugins/view-assets/lang/de.json +++ b/plugins/view-assets/lang/de.json @@ -146,6 +146,7 @@ "PermissionWarningMessage": "Ihre aktuelle Rolle hat eingeschränkte Berechtigungen. Um diese und andere erweiterte Funktionen freizuschalten, bitten Sie Ihren Arbeitsbereich-Administrator, Ihr Zugriffslevel zu erhöhen!", "Icon": "Symbol", "Color": "Farbe", - "AutomationOnly": "Nur Automatisierung" + "AutomationOnly": "Nur Automatisierung", + "CopyLink": "Link kopieren" } } diff --git a/plugins/view-assets/lang/en.json b/plugins/view-assets/lang/en.json index 9ede6ffa24..400e2dbcda 100644 --- a/plugins/view-assets/lang/en.json +++ b/plugins/view-assets/lang/en.json @@ -146,6 +146,7 @@ "PermissionWarningMessage": "Your current role has limited permissions. To unlock this and other advanced features, ask your workspace admin to upgrade your access level!", "Icon": "Icon", "Color": "Color", - "AutomationOnly": "Automation only" + "AutomationOnly": "Automation only", + "CopyLink": "Copy link" } } diff --git a/plugins/view-assets/lang/es.json b/plugins/view-assets/lang/es.json index cc039617c6..08a36ff027 100644 --- a/plugins/view-assets/lang/es.json +++ b/plugins/view-assets/lang/es.json @@ -141,6 +141,7 @@ "PermissionWarningMessage": "Tu rol actual tiene permisos limitados. ¡Para desbloquear esta y otras funciones avanzadas, pídele al administrador de tu espacio de trabajo que actualice tu nivel de acceso!", "Icon": "Icono", "Color": "Color", - "AutomationOnly": "Solo automatización" + "AutomationOnly": "Solo automatización", + "CopyLink": "Copiar enlace" } } diff --git a/plugins/view-assets/lang/fr.json b/plugins/view-assets/lang/fr.json index 1a6bbd35b5..6e6e78b560 100644 --- a/plugins/view-assets/lang/fr.json +++ b/plugins/view-assets/lang/fr.json @@ -141,6 +141,7 @@ "PermissionWarningMessage": "Votre rôle actuel a des autorisations limitées. Pour débloquer cette fonctionnalité et d'autres fonctionnalités avancées, demandez à l'administrateur de votre espace de travail de mettre à jour votre niveau d'accès !", "Icon": "Icône", "Color": "Couleur", - "AutomationOnly": "Automatisation uniquement" + "AutomationOnly": "Automatisation uniquement", + "CopyLink": "Copier le lien" } } diff --git a/plugins/view-assets/lang/it.json b/plugins/view-assets/lang/it.json index ed90fe96b5..1fd12b9afe 100644 --- a/plugins/view-assets/lang/it.json +++ b/plugins/view-assets/lang/it.json @@ -141,6 +141,7 @@ "PermissionWarningMessage": "Il tuo ruolo attuale ha autorizzazioni limitate. Per sbloccare questa e altre funzionalità avanzate, chiedi all'amministratore del tuo spazio di lavoro di aggiornare il tuo livello di accesso!", "Icon": "Icona", "Color": "Colore", - "AutomationOnly": "Solo automazione" + "AutomationOnly": "Solo automazione", + "CopyLink": "Copia link" } } diff --git a/plugins/view-assets/lang/ja.json b/plugins/view-assets/lang/ja.json index fcf02760aa..162e3323a2 100644 --- a/plugins/view-assets/lang/ja.json +++ b/plugins/view-assets/lang/ja.json @@ -141,6 +141,7 @@ "PermissionWarningMessage": "現在のロールの権限は制限されています。この機能や他の高度な機能を利用するには、ワークスペース管理者にアクセスレベルのアップグレードを依頼してください!", "Icon": "アイコン", "Color": "色", - "AutomationOnly": "自動化のみ" + "AutomationOnly": "自動化のみ", + "CopyLink": "リンクをコピー" } } diff --git a/plugins/view-assets/lang/pt.json b/plugins/view-assets/lang/pt.json index 32b126399b..ce2e3fd9d7 100644 --- a/plugins/view-assets/lang/pt.json +++ b/plugins/view-assets/lang/pt.json @@ -141,6 +141,7 @@ "PermissionWarningMessage": "Seu cargo atual tem permissões limitadas. Para desbloquear este e outros recursos avançados, peça ao administrador do seu espaço de trabalho para atualizar seu nível de acesso!", "Icon": "Ícone", "Color": "Cor", - "AutomationOnly": "Apenas automação" + "AutomationOnly": "Apenas automação", + "CopyLink": "Copiar link" } } diff --git a/plugins/view-assets/lang/ru.json b/plugins/view-assets/lang/ru.json index 6646a80120..9b4599045d 100644 --- a/plugins/view-assets/lang/ru.json +++ b/plugins/view-assets/lang/ru.json @@ -143,6 +143,7 @@ "PermissionWarningMessage": "Ваша текущая роль имеет ограниченные права. Чтобы разблокировать эту и другие продвинутые функции, попросите администратора вашего пространства повысить ваш уровень доступа!", "Icon": "Иконка", "Color": "Цвет", - "AutomationOnly": "Только автоматизация" + "AutomationOnly": "Только автоматизация", + "CopyLink": "Скопировать ссылку" } } diff --git a/plugins/view-assets/lang/zh.json b/plugins/view-assets/lang/zh.json index 44a1784edc..b727a2b47c 100644 --- a/plugins/view-assets/lang/zh.json +++ b/plugins/view-assets/lang/zh.json @@ -146,6 +146,7 @@ "PermissionWarningMessage": "您当前角色的权限有限。要解锁此功能和其他高级功能,请让您的工作区管理员升级您的访问权限!", "Icon": "图标", "Color": "颜色", - "AutomationOnly": "仅限自动化" + "AutomationOnly": "仅限自动化", + "CopyLink": "复制链接" } } diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index 84b71e330d..4eff0df4ab 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -149,7 +149,7 @@ import { canLeaveSpace, isClipboardAvailable } from './visibilityTester' -import { openDocFromRef } from './utils' +import { canCopyLink, getLink, openDocFromRef } from './utils' import ForbiddenNotification from './components/ForbiddenNotification.svelte' export { canArchiveSpace, canDeleteObject, canDeleteSpace, canEditSpace } from './visibilityTester' export { getActions, getContextActions, invokeAction, showMenu } from './actions' @@ -376,6 +376,8 @@ export default async (): Promise => ({ IsClipboardAvailable: isClipboardAvailable, BlobImageMetadata: blobImageMetadata, BlobVideoMetadata: blobVideoMetadata, - OpenDocument: openDocFromRef + OpenDocument: openDocFromRef, + CanCopyLink: canCopyLink, + GetLink: getLink } }) diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts index 39afcd2d8c..a08a78a91f 100644 --- a/plugins/view-resources/src/utils.ts +++ b/plugins/view-resources/src/utils.ts @@ -57,8 +57,8 @@ import core, { } from '@hcengineering/core' import { type Restrictions } from '@hcengineering/guest' import type { Asset, IntlString } from '@hcengineering/platform' -import { getResource, translate } from '@hcengineering/platform' -import { +import { getMetadata, getResource, translate } from '@hcengineering/platform' +import presentation, { createQuery, getAttributePresenterClass, getClient, @@ -1238,6 +1238,36 @@ export async function openDocFromRef (_class: Ref> return false } +export async function canCopyLink (doc?: Doc | Doc[]): Promise { + if (doc === null || doc === undefined) { + return false + } + if (Array.isArray(doc) && doc.length !== 1) { + return false + } + return true +} + +export async function getLink (doc?: Doc | Doc[]): Promise { + doc = Array.isArray(doc) ? doc[0] : doc + if (doc === undefined) { + return '' + } + + const client = getClient() + const hierarchy = client.getHierarchy() + + const panelComponent = hierarchy.classHierarchyMixin(doc._class, view.mixin.ObjectPanel) + const comp = panelComponent?.component ?? view.component.EditDoc + const loc = await getObjectLinkFragment(hierarchy, doc, {}, comp) + const url = locationToUrl(loc) + + const frontUrl = getMetadata(presentation.metadata.FrontUrl) + const protocolAndHost = frontUrl ?? `${window.location.protocol}//${window.location.host}` + + return `${protocolAndHost}${url}` +} + /** * @public */ diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 3b1d0c458c..98b654c3cd 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -154,7 +154,9 @@ const view = plugin(viewId, { // Edit document Open: '' as Ref, OpenInNewTab: '' as Ref, - RemoveRelation: '' as Ref + RemoveRelation: '' as Ref, + + CopyLink: '' as Ref> }, viewlet: { Table: '' as Ref, diff --git a/tests/sanity/tests/documents/documents.spec.ts b/tests/sanity/tests/documents/documents.spec.ts index a07198e4e7..196b06cad8 100644 --- a/tests/sanity/tests/documents/documents.spec.ts +++ b/tests/sanity/tests/documents/documents.spec.ts @@ -253,7 +253,7 @@ test.describe('Documents tests', () => { await documentsPage.clickOnButtonCreateDocument() await documentsPage.createDocument(newDocument) await documentsPage.selectMoreActionOfDocument(newDocument.title, 'Lock') - await documentsPage.selectMoreActionOfDocument(newDocument.title, 'Copy document URL to clipboard') + await documentsPage.selectMoreActionOfDocument(newDocument.title, 'Copy link') await context.grantPermissions(['clipboard-read']) const handle = await page.evaluateHandle(() => navigator.clipboard.readText()) const clipboardContent = await handle.jsonValue()