diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index d059a74853..341100a033 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -702,9 +702,9 @@ export function createModel (builder: Builder): void { createAction( builder, { - action: recruit.actionImpl.CopyToClipboard, + action: view.actionImpl.CopyTextToClipboard, actionProps: { - type: 'id' + textProvider: recruit.function.GetApplicationId }, label: recruit.string.CopyId, icon: recruit.icon.Application, @@ -723,9 +723,9 @@ export function createModel (builder: Builder): void { createAction( builder, { - action: recruit.actionImpl.CopyToClipboard, + action: view.actionImpl.CopyTextToClipboard, actionProps: { - type: 'link' + textProvider: recruit.function.GetApplicationLink }, label: recruit.string.CopyLink, icon: recruit.icon.Application, @@ -744,9 +744,9 @@ export function createModel (builder: Builder): void { createAction( builder, { - action: recruit.actionImpl.CopyToClipboard, + action: view.actionImpl.CopyTextToClipboard, actionProps: { - type: 'link' + textProvider: recruit.function.GetRecruitLink }, label: recruit.string.CopyLink, icon: recruit.icon.Application, diff --git a/models/recruit/src/plugin.ts b/models/recruit/src/plugin.ts index c5eb5903b4..f06ed4bc6b 100644 --- a/models/recruit/src/plugin.ts +++ b/models/recruit/src/plugin.ts @@ -32,12 +32,16 @@ export default mergeIds(recruitId, recruit, { CopyCandidateLink: '' as Ref }, actionImpl: { - CreateOpinion: '' as ViewAction, - CopyToClipboard: '' as ViewAction + CreateOpinion: '' as ViewAction }, category: { Recruit: '' as Ref }, + function: { + GetApplicationId: '' as Resource<(doc: Doc, props: Record) => Promise>, + GetApplicationLink: '' as Resource<(doc: Doc, props: Record) => Promise>, + GetRecruitLink: '' as Resource<(doc: Doc, props: Record) => Promise> + }, string: { ApplicationShort: '' as IntlString, ApplicationsShort: '' as IntlString, diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index f78ed37046..7ecfe7da61 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -1129,9 +1129,9 @@ export function createModel (builder: Builder): void { createAction( builder, { - action: tracker.actionImpl.CopyToClipboard, + action: view.actionImpl.CopyTextToClipboard, actionProps: { - type: 'id' + textProvider: tracker.function.GetIssueId }, label: tracker.string.CopyIssueId, icon: tracker.icon.CopyID, @@ -1150,9 +1150,9 @@ export function createModel (builder: Builder): void { createAction( builder, { - action: tracker.actionImpl.CopyToClipboard, + action: view.actionImpl.CopyTextToClipboard, actionProps: { - type: 'title' + textProvider: tracker.function.GetIssueTitle }, label: tracker.string.CopyIssueTitle, icon: tracker.icon.CopyBranch, @@ -1171,9 +1171,9 @@ export function createModel (builder: Builder): void { createAction( builder, { - action: tracker.actionImpl.CopyToClipboard, + action: view.actionImpl.CopyTextToClipboard, actionProps: { - type: 'link' + textProvider: tracker.function.GetIssueLink }, label: tracker.string.CopyIssueUrl, icon: tracker.icon.CopyURL, diff --git a/plugins/chunter-resources/src/components/Message.svelte b/plugins/chunter-resources/src/components/Message.svelte index 6080c1bc81..f3c41e704e 100644 --- a/plugins/chunter-resources/src/components/Message.svelte +++ b/plugins/chunter-resources/src/components/Message.svelte @@ -121,7 +121,8 @@ } else { location.path.length = 4 } - await copyTextToClipboard(`${window.location.origin}${locationToUrl(location)}`) + const text = `${window.location.origin}${locationToUrl(location)}` + await copyTextToClipboard(text) } } diff --git a/plugins/recruit-resources/src/index.ts b/plugins/recruit-resources/src/index.ts index d5e2e04571..1a618fd2c4 100644 --- a/plugins/recruit-resources/src/index.ts +++ b/plugins/recruit-resources/src/index.ts @@ -50,7 +50,7 @@ import VacancyItemPresenter from './components/VacancyItemPresenter.svelte' import VacancyModifiedPresenter from './components/VacancyModifiedPresenter.svelte' import VacancyPresenter from './components/VacancyPresenter.svelte' import recruit from './plugin' -import { copyToClipboard, getApplicationTitle } from './utils' +import { objectIdProvider, objectLinkProvider, getApplicationTitle } from './utils' import VacancyList from './components/VacancyList.svelte' async function createOpinion (object: Doc): Promise { @@ -254,8 +254,7 @@ async function noneApplicant (filter: Filter, onUpdate: () => void): Promise => ({ actionImpl: { - CreateOpinion: createOpinion, - CopyToClipboard: copyToClipboard + CreateOpinion: createOpinion }, validator: { ApplicantValidator: applicantValidator @@ -305,6 +304,9 @@ export default async (): Promise => ({ ApplicationTitleProvider: getApplicationTitle, HasActiveApplicant: hasActiveApplicant, HasNoActiveApplicant: hasNoActiveApplicant, - NoneApplications: noneApplicant + NoneApplications: noneApplicant, + GetApplicationId: objectIdProvider, + GetApplicationLink: objectLinkProvider, + GetRecruitLink: objectLinkProvider } }) diff --git a/plugins/recruit-resources/src/utils.ts b/plugins/recruit-resources/src/utils.ts index a85b9d2637..ce037f4bb1 100644 --- a/plugins/recruit-resources/src/utils.ts +++ b/plugins/recruit-resources/src/utils.ts @@ -1,6 +1,6 @@ import core, { Doc, Ref, TxOperations } from '@hcengineering/core' import { translate } from '@hcengineering/platform' -import { copyTextToClipboard, getClient } from '@hcengineering/presentation' +import { getClient } from '@hcengineering/presentation' import { Applicant, Candidate } from '@hcengineering/recruit' import { getPanelURI } from '@hcengineering/ui' import view from '@hcengineering/view' @@ -19,23 +19,13 @@ export async function getApplicationTitle (client: TxOperations, ref: Ref): return `${label}-${object.number}` } -export async function copyToClipboard ( - object: Applicant | Candidate, - ev: Event, - { type }: { type: string } -): Promise { +export async function objectIdProvider (doc: Applicant | Candidate): Promise { const client = getClient() - let text: string - switch (type) { - case 'id': - text = await getApplicationTitle(client, object._id) - break - case 'link': - // TODO: fix when short link is available - text = `${window.location.href}#${getPanelURI(view.component.EditDoc, object._id, object._class, 'content')}` - break - default: - return - } - await copyTextToClipboard(text) + return await getApplicationTitle(client, doc._id) +} + +export async function objectLinkProvider (doc: Applicant | Candidate): Promise { + return await Promise.resolve( + `${window.location.href}#${getPanelURI(view.component.EditDoc, doc._id, doc._class, 'content')}` + ) } diff --git a/plugins/tracker-resources/src/index.ts b/plugins/tracker-resources/src/index.ts index f38db044d8..e3da502f0e 100644 --- a/plugins/tracker-resources/src/index.ts +++ b/plugins/tracker-resources/src/index.ts @@ -59,7 +59,14 @@ import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte' import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte' import Views from './components/views/Views.svelte' import Statuses from './components/workflow/Statuses.svelte' -import { copyToClipboard, getIssueId, getIssueTitle, resolveLocation } from './issues' +import { + getIssueId, + getIssueTitle, + issueIdProvider, + issueLinkProvider, + issueTitleProvider, + resolveLocation +} from './issues' import tracker from './plugin' import SprintEditor from './components/sprints/SprintEditor.svelte' @@ -212,10 +219,12 @@ export default async (): Promise => ({ await queryIssue(tracker.class.Issue, client, query, filter) }, function: { - IssueTitleProvider: getIssueTitle + IssueTitleProvider: getIssueTitle, + GetIssueId: issueIdProvider, + GetIssueLink: issueLinkProvider, + GetIssueTitle: issueTitleProvider }, actionImpl: { - CopyToClipboard: copyToClipboard, EditWorkflowStatuses: editWorkflowStatuses }, resolver: { diff --git a/plugins/tracker-resources/src/issues.ts b/plugins/tracker-resources/src/issues.ts index 24efa234a0..c2f53cf798 100644 --- a/plugins/tracker-resources/src/issues.ts +++ b/plugins/tracker-resources/src/issues.ts @@ -1,5 +1,5 @@ import { Doc, DocumentUpdate, Ref, RelatedDocument, TxOperations } from '@hcengineering/core' -import { copyTextToClipboard, getClient } from '@hcengineering/presentation' +import { getClient } from '@hcengineering/presentation' import { Issue, Project, Sprint, Team, trackerId } from '@hcengineering/tracker' import { getCurrentLocation, getPanelURI, Location } from '@hcengineering/ui' import { workbenchId } from '@hcengineering/workbench' @@ -31,24 +31,18 @@ export function generateIssuePanelUri (issue: Issue): string { return getPanelURI(tracker.component.EditIssue, issue._id, issue._class, 'content') } -export async function copyToClipboard (object: Issue, ev: Event, { type }: { type: string }): Promise { +export async function issueIdProvider (doc: Doc): Promise { const client = getClient() - let text: string - switch (type) { - case 'id': - text = await getIssueTitle(client, object._id) - break - case 'title': - text = object.title - break - case 'link': - text = generateIssueShortLink(await getIssueTitle(client, object._id)) - break - default: - return - } + return await getIssueTitle(client, doc._id) +} - await copyTextToClipboard(text) +export async function issueTitleProvider (doc: Issue): Promise { + return await Promise.resolve(doc.title) +} + +export async function issueLinkProvider (doc: Doc): Promise { + const client = getClient() + return await getIssueTitle(client, doc._id).then(generateIssueShortLink) } export function generateIssueShortLink (issueId: string): string { diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts index 900a4bc0cb..16c4857b7b 100644 --- a/plugins/tracker-resources/src/plugin.ts +++ b/plugins/tracker-resources/src/plugin.ts @@ -301,6 +301,9 @@ export default mergeIds(trackerId, tracker, { IssueTemplatePresenter: '' as AnyComponent }, function: { - IssueTitleProvider: '' as Resource<(client: Client, ref: Ref) => Promise> + IssueTitleProvider: '' as Resource<(client: Client, ref: Ref) => Promise>, + GetIssueId: '' as Resource<(doc: Doc, props: Record) => Promise>, + GetIssueLink: '' as Resource<(doc: Doc, props: Record) => Promise>, + GetIssueTitle: '' as Resource<(doc: Doc, props: Record) => Promise> } }) diff --git a/plugins/view-resources/src/actionImpl.ts b/plugins/view-resources/src/actionImpl.ts index cecf962689..85ef092c82 100644 --- a/plugins/view-resources/src/actionImpl.ts +++ b/plugins/view-resources/src/actionImpl.ts @@ -18,6 +18,41 @@ import view from './plugin' import { FocusSelection, focusStore, previewDocument, SelectDirection, selectionStore } from './selection' import { deleteObject } from './utils' +/** + * Action to be used for copying text to clipboard. + * In Safari a request to write to the clipboard must be triggered during a user gesture. + * A call to clipboard.write or clipboard.writeText outside the scope of a user + * gesture(such as "click" or "touch" event handlers) will result in the immediate + * rejection of the promise returned by the API call. + * https://webkit.org/blog/10855/async-clipboard-api/ + * + * * Require props: + * - textProvider - a function that provides text to be copied. + * - props - additional text provider props. + */ +async function CopyTextToClipboard ( + doc: Doc, + evt: Event, + props: { + textProvider: Resource<(doc: Doc, props?: Record) => Promise> + props?: Record + } +): Promise { + const getText = await getResource(props.textProvider) + try { + // Safari specific behavior + // see https://bugs.webkit.org/show_bug.cgi?id=222262 + const clipboardItem = new ClipboardItem({ + 'text/plain': getText(doc, props.props) + }) + await navigator.clipboard.write([clipboardItem]) + } catch { + // Fallback to default clipboard API implementation + const text = await getText(doc, props.props) + await navigator.clipboard.writeText(text) + } +} + function Delete (object: Doc): void { showPopup( MessageBox, @@ -334,6 +369,7 @@ async function getPopupAlignment ( * @public */ export const actionImpl = { + CopyTextToClipboard, Delete, Move, MoveUp, diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 038e5272d7..1e8b2b6082 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -503,6 +503,10 @@ const view = plugin(viewId, { PositionElementAlignment: '' as Resource<(e?: Event) => PopupAlignment | undefined> }, actionImpl: { + CopyTextToClipboard: '' as ViewAction<{ + textProvider: Resource<(doc: Doc, props: Record) => Promise> + props?: Record + }>, UpdateDocument: '' as ViewAction<{ key: string value: any