EQMS-1635 Add generic action for copying link (#9668)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov
2025-08-12 22:20:05 +07:00
committed by GitHub
parent 8fc81ac57c
commit 7d0034c123
26 changed files with 97 additions and 48 deletions
+1
View File
@@ -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",
+3
View File
@@ -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 {
+2 -1
View File
@@ -34,7 +34,8 @@ export default mergeIds(cardId, card, {
action: {
DeleteMasterTag: '' as Ref<Action>,
SetParent: '' as Ref<Action<Doc, any>>,
UnsetParent: '' as Ref<Action<Doc, any>>
UnsetParent: '' as Ref<Action<Doc, any>>,
PublicLink: '' as Ref<Action<Doc, any>>
},
category: {
Card: '' as Ref<ActionCategory>,
+2 -1
View File
@@ -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
)
-21
View File
@@ -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,
{
+4 -2
View File
@@ -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
)
+2 -1
View File
@@ -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
)
+18
View File
@@ -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,
{
+5 -2
View File
@@ -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<boolean>>,
IsClipboardAvailable: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
BlobImageMetadata: '' as Resource<(file: FileOrBlob, blob: Ref<Blob>) => Promise<BlobMetadata | undefined>>,
BlobVideoMetadata: '' as Resource<(file: FileOrBlob, blob: Ref<Blob>) => Promise<BlobMetadata | undefined>>
BlobVideoMetadata: '' as Resource<(file: FileOrBlob, blob: Ref<Blob>) => Promise<BlobMetadata | undefined>>,
GetLink: '' as Resource<(doc?: Doc | Doc[]) => Promise<string>>,
CanCopyLink: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>
},
pipeline: {
PresentationMiddleware: '' as Ref<PresentationMiddlewareFactory>,
-2
View File
@@ -51,7 +51,6 @@ import {
documentTitleProvider,
getDocumentLink,
getDocumentLinkId,
getDocumentUrl,
parseDocumentId,
resolveLocation
} from './utils'
@@ -192,7 +191,6 @@ export default async (): Promise<Resources> => ({
UnlockContent: unlockContent
},
function: {
GetDocumentLink: getDocumentUrl,
GetObjectLinkFragment: getDocumentLink,
DocumentTitleProvider: documentTitleProvider,
CanLockDocument: canLockDocument,
-1
View File
@@ -28,7 +28,6 @@ export default mergeIds(documentId, document, {
},
function: {
DocumentTitleProvider: '' as Resource<<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<string>>,
GetDocumentLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetObjectLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
GetDocumentLinkId: '' as Resource<(doc: Doc) => Promise<string>>,
ParseDocumentId: '' as Resource<(id: string) => Promise<Ref<Document> | undefined>>
-1
View File
@@ -44,7 +44,6 @@ export const documentPlugin = plugin(documentId, {
DocumentSearchIcon: '' as AnyComponent
},
action: {
CopyDocumentLink: '' as Ref<Action<Doc, any>>,
CreateChildDocument: '' as Ref<Action>,
CreateDocument: '' as Ref<Action>,
EditTeamspace: '' as Ref<Action>
+2 -1
View File
@@ -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"
}
}
+2 -1
View File
@@ -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"
}
}
+2 -1
View File
@@ -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"
}
}
+2 -1
View File
@@ -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"
}
}
+2 -1
View File
@@ -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"
}
}
+2 -1
View File
@@ -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"
}
}
+2 -1
View File
@@ -141,6 +141,7 @@
"PermissionWarningMessage": "現在のロールの権限は制限されています。この機能や他の高度な機能を利用するには、ワークスペース管理者にアクセスレベルのアップグレードを依頼してください!",
"Icon": "アイコン",
"Color": "色",
"AutomationOnly": "自動化のみ"
"AutomationOnly": "自動化のみ",
"CopyLink": "リンクをコピー"
}
}
+2 -1
View File
@@ -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"
}
}
+2 -1
View File
@@ -143,6 +143,7 @@
"PermissionWarningMessage": "Ваша текущая роль имеет ограниченные права. Чтобы разблокировать эту и другие продвинутые функции, попросите администратора вашего пространства повысить ваш уровень доступа!",
"Icon": "Иконка",
"Color": "Цвет",
"AutomationOnly": "Только автоматизация"
"AutomationOnly": "Только автоматизация",
"CopyLink": "Скопировать ссылку"
}
}
+2 -1
View File
@@ -146,6 +146,7 @@
"PermissionWarningMessage": "您当前角色的权限有限。要解锁此功能和其他高级功能,请让您的工作区管理员升级您的访问权限!",
"Icon": "图标",
"Color": "颜色",
"AutomationOnly": "仅限自动化"
"AutomationOnly": "仅限自动化",
"CopyLink": "复制链接"
}
}
+4 -2
View File
@@ -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<Resources> => ({
IsClipboardAvailable: isClipboardAvailable,
BlobImageMetadata: blobImageMetadata,
BlobVideoMetadata: blobVideoMetadata,
OpenDocument: openDocFromRef
OpenDocument: openDocFromRef,
CanCopyLink: canCopyLink,
GetLink: getLink
}
})
+32 -2
View File
@@ -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<T extends Doc = Doc> (_class: Ref<Class<T>>
return false
}
export async function canCopyLink (doc?: Doc | Doc[]): Promise<boolean> {
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<string> {
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
*/
+3 -1
View File
@@ -154,7 +154,9 @@ const view = plugin(viewId, {
// Edit document
Open: '' as Ref<Action>,
OpenInNewTab: '' as Ref<Action>,
RemoveRelation: '' as Ref<Action>
RemoveRelation: '' as Ref<Action>,
CopyLink: '' as Ref<Action<Doc, any>>
},
viewlet: {
Table: '' as Ref<ViewletDescriptor>,
@@ -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()