diff --git a/models/card/src/actions.ts b/models/card/src/actions.ts index 4b2f99ed79e..1328d1fba8e 100644 --- a/models/card/src/actions.ts +++ b/models/card/src/actions.ts @@ -243,4 +243,22 @@ export function createActions (builder: Builder): void { group: 'edit' } }) + + createAction( + builder, + { + action: card.actionImpl.DuplicateCard, + label: card.string.Duplicate, + icon: card.icon.Duplicate, + input: 'focus', + category: card.category.Card, + target: card.class.Card, + context: { + mode: ['context', 'browser'], + application: card.app.Card, + group: 'associate' + } + }, + card.action.Duplicate + ) } diff --git a/models/card/src/plugin.ts b/models/card/src/plugin.ts index e1afdb54a4e..404b4eccf41 100644 --- a/models/card/src/plugin.ts +++ b/models/card/src/plugin.ts @@ -29,13 +29,15 @@ export default mergeIds(cardId, card, { }, actionImpl: { DeleteMasterTag: '' as ViewAction, + DuplicateCard: '' as ViewAction, EditSpace: '' as ViewAction }, action: { DeleteMasterTag: '' as Ref, SetParent: '' as Ref>, UnsetParent: '' as Ref>, - PublicLink: '' as Ref> + PublicLink: '' as Ref>, + Duplicate: '' as Ref }, string: { CreateCardPersmissionDescription: '' as IntlString, diff --git a/plugins/card-assets/assets/icons.svg b/plugins/card-assets/assets/icons.svg index a8e221223da..632d511d18c 100644 --- a/plugins/card-assets/assets/icons.svg +++ b/plugins/card-assets/assets/icons.svg @@ -69,4 +69,8 @@ + + + + diff --git a/plugins/card-assets/lang/cs.json b/plugins/card-assets/lang/cs.json index 4c24c5d8a55..81edcaad8ee 100644 --- a/plugins/card-assets/lang/cs.json +++ b/plugins/card-assets/lang/cs.json @@ -59,6 +59,7 @@ "ForbidUpdateCard": "Zakázat aktualizaci karty", "ForbidCreateCardPermission": "Zakázat vytváření karet", "ForbidAddTagPermission": "Zakázat přidávání štítků", - "ForbidRemoveTag": "Zakázat odebírání štítků" + "ForbidRemoveTag": "Zakázat odebírání štítků", + "Duplicate": "Duplikovat" } } diff --git a/plugins/card-assets/lang/de.json b/plugins/card-assets/lang/de.json index 5c2566945c1..8c1490243f4 100644 --- a/plugins/card-assets/lang/de.json +++ b/plugins/card-assets/lang/de.json @@ -59,6 +59,7 @@ "ForbidUpdateCard": "Aktualisieren der Karte verbieten", "ForbidCreateCardPermission": "Erstellen von Karten verbieten", "ForbidAddTagPermission": "Hinzufügen von Tags verbieten", - "ForbidRemoveTag": "Entfernen von Tags verbieten" + "ForbidRemoveTag": "Entfernen von Tags verbieten", + "Duplicate": "Duplizieren" } } diff --git a/plugins/card-assets/lang/en.json b/plugins/card-assets/lang/en.json index efa1c32e31c..fae5ff889e9 100644 --- a/plugins/card-assets/lang/en.json +++ b/plugins/card-assets/lang/en.json @@ -59,6 +59,7 @@ "ForbidUpdateCard": "Forbid Update Card", "ForbidCreateCardPermission": "Forbid Create Card", "ForbidAddTagPermission": "Forbid Add Tag", - "ForbidRemoveTag": "Forbid Remove Tag" + "ForbidRemoveTag": "Forbid Remove Tag", + "Duplicate": "Duplicate" } } diff --git a/plugins/card-assets/lang/es.json b/plugins/card-assets/lang/es.json index b1ca654420a..d9cfaac9516 100644 --- a/plugins/card-assets/lang/es.json +++ b/plugins/card-assets/lang/es.json @@ -59,6 +59,7 @@ "ForbidUpdateCard": "Prohibir actualizar tarjeta", "ForbidCreateCardPermission": "Prohibir crear tarjeta", "ForbidAddTagPermission": "Prohibir añadir etiqueta", - "ForbidRemoveTag": "Prohibir eliminar etiqueta" + "ForbidRemoveTag": "Prohibir eliminar etiqueta", + "Duplicate": "Duplicar" } } diff --git a/plugins/card-assets/lang/fr.json b/plugins/card-assets/lang/fr.json index 7bd8d571d81..b8d1bec7985 100644 --- a/plugins/card-assets/lang/fr.json +++ b/plugins/card-assets/lang/fr.json @@ -59,6 +59,7 @@ "ForbidUpdateCard": "Interdire la mise à jour de la fiche", "ForbidCreateCardPermission": "Interdire la création de fiche", "ForbidAddTagPermission": "Interdire l'ajout d'étiquette", - "ForbidRemoveTag": "Interdire la suppression d'étiquette" + "ForbidRemoveTag": "Interdire la suppression d'étiquette", + "Duplicate": "Dupliquer" } } diff --git a/plugins/card-assets/lang/it.json b/plugins/card-assets/lang/it.json index 37005d91fa0..12dc6563a91 100644 --- a/plugins/card-assets/lang/it.json +++ b/plugins/card-assets/lang/it.json @@ -59,6 +59,7 @@ "ForbidUpdateCard": "Vieta aggiornamento scheda", "ForbidCreateCardPermission": "Vieta creazione scheda", "ForbidAddTagPermission": "Vieta aggiunta tag", - "ForbidRemoveTag": "Vieta rimozione tag" + "ForbidRemoveTag": "Vieta rimozione tag", + "Duplicate": "Duplicato" } } diff --git a/plugins/card-assets/lang/ja.json b/plugins/card-assets/lang/ja.json index a482f56239b..44a599cf600 100644 --- a/plugins/card-assets/lang/ja.json +++ b/plugins/card-assets/lang/ja.json @@ -59,6 +59,7 @@ "ForbidUpdateCard": "カード更新を禁止", "ForbidCreateCardPermission": "カード作成を禁止", "ForbidAddTagPermission": "タグ追加を禁止", - "ForbidRemoveTag": "タグ削除を禁止" + "ForbidRemoveTag": "タグ削除を禁止", + "Duplicate": "複製" } } diff --git a/plugins/card-assets/lang/pt.json b/plugins/card-assets/lang/pt.json index 1e5e7ad0fd2..dbd5e82de83 100644 --- a/plugins/card-assets/lang/pt.json +++ b/plugins/card-assets/lang/pt.json @@ -59,6 +59,7 @@ "ForbidUpdateCard": "Proibir atualização de cartão", "ForbidCreateCardPermission": "Proibir criação de cartão", "ForbidAddTagPermission": "Proibir adicionar etiqueta", - "ForbidRemoveTag": "Proibir remover etiqueta" + "ForbidRemoveTag": "Proibir remover etiqueta", + "Duplicate": "Duplicar" } } diff --git a/plugins/card-assets/lang/ru.json b/plugins/card-assets/lang/ru.json index 4c34a99c158..89c9dcf7575 100644 --- a/plugins/card-assets/lang/ru.json +++ b/plugins/card-assets/lang/ru.json @@ -59,6 +59,7 @@ "ForbidUpdateCard": "Запретить обновление карточки", "ForbidCreateCardPermission": "Запретить создание карточки", "ForbidAddTagPermission": "Запретить добавление тега", - "ForbidRemoveTag": "Запретить удаление тега" + "ForbidRemoveTag": "Запретить удаление тега", + "Duplicate": "Дублировать" } } diff --git a/plugins/card-assets/lang/tr.json b/plugins/card-assets/lang/tr.json index 576372ac598..23960f3e302 100644 --- a/plugins/card-assets/lang/tr.json +++ b/plugins/card-assets/lang/tr.json @@ -59,6 +59,7 @@ "ForbidUpdateCard": "Kart güncellemeyi yasakla", "ForbidCreateCardPermission": "Kart oluşturmayı yasakla", "ForbidAddTagPermission": "Etiket eklemeyi yasakla", - "ForbidRemoveTag": "Etiket kaldırmayı yasakla" + "ForbidRemoveTag": "Etiket kaldırmayı yasakla", + "Duplicate": "Çoğalt" } } diff --git a/plugins/card-assets/lang/zh.json b/plugins/card-assets/lang/zh.json index 2d418261d0a..870dc7c9f14 100644 --- a/plugins/card-assets/lang/zh.json +++ b/plugins/card-assets/lang/zh.json @@ -59,6 +59,7 @@ "ForbidUpdateCard": "禁止更新卡片", "ForbidCreateCardPermission": "禁止创建卡片", "ForbidAddTagPermission": "禁止添加标签", - "ForbidRemoveTag": "禁止移除标签" + "ForbidRemoveTag": "禁止移除标签", + "Duplicate": "复制" } } diff --git a/plugins/card-assets/src/index.ts b/plugins/card-assets/src/index.ts index d244ef58e4e..45bad82d03d 100644 --- a/plugins/card-assets/src/index.ts +++ b/plugins/card-assets/src/index.ts @@ -28,5 +28,6 @@ loadMetadata(card.icon, { Space: `${icons}#space`, Expand: `${icons}#expand`, Feed: `${icons}#feed`, - All: `${icons}#all` + All: `${icons}#all`, + Duplicate: `${icons}#duplicate` }) diff --git a/plugins/card-resources/src/index.ts b/plugins/card-resources/src/index.ts index 6ff30c3a082..6020da67543 100644 --- a/plugins/card-resources/src/index.ts +++ b/plugins/card-resources/src/index.ts @@ -33,7 +33,8 @@ import { checkRelationsSectionVisibility, getSpaceAccessPublicLink, canGetSpaceAccessPublicLink, - cardFactory + cardFactory, + duplicateCard } from './utils' import ManageMasterTagsContent from './components/settings/ManageMasterTagsContent.svelte' import ManageMasterTagsTools from './components/settings/ManageMasterTagsTools.svelte' @@ -144,6 +145,7 @@ export default async (): Promise => ({ }, actionImpl: { DeleteMasterTag: deleteMasterTag, + DuplicateCard: duplicateCard, EditSpace: editSpace }, function: { diff --git a/plugins/card-resources/src/plugin.ts b/plugins/card-resources/src/plugin.ts index 27e6c65bac1..b86ba50f5c9 100644 --- a/plugins/card-resources/src/plugin.ts +++ b/plugins/card-resources/src/plugin.ts @@ -125,6 +125,7 @@ export default mergeIds(cardId, card, { CardTitle: '' as IntlString, CardContent: '' as IntlString, Post: '' as IntlString, - ShowLess: '' as IntlString + ShowLess: '' as IntlString, + Duplicate: '' as IntlString } }) diff --git a/plugins/card-resources/src/utils.ts b/plugins/card-resources/src/utils.ts index 10263b71373..d5bb4f5164b 100644 --- a/plugins/card-resources/src/utils.ts +++ b/plugins/card-resources/src/utils.ts @@ -11,6 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { type AccountClient, getClient as getAccountClientRaw } from '@hcengineering/account-client' +import { Analytics } from '@hcengineering/analytics' import { type Card, CardEvents, cardId, type CardSpace, type MasterTag } from '@hcengineering/card' import core, { AccountRole, @@ -25,6 +27,7 @@ import core, { hasAccountRole, type Hierarchy, makeCollabId, + makeDocCollabId, type Markup, type MarkupBlobRef, type Ref, @@ -35,14 +38,17 @@ import core, { type WithLookup } from '@hcengineering/core' import login from '@hcengineering/login' +import { getMetadata, translate } from '@hcengineering/platform' import presentation, { createMarkup, getClient, + getMarkup, IconWithEmoji, MessageBox, type ObjectSearchResult } from '@hcengineering/presentation' -import { type AccountClient, getClient as getAccountClientRaw } from '@hcengineering/account-client' +import { makeRank } from '@hcengineering/rank' +import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text' import { getCurrentLocation, getCurrentResolvedLocation, @@ -50,18 +56,16 @@ import { type IconComponent, type IconProps, type Location, + navigate, type ResolvedLocation, showPopup } from '@hcengineering/ui' -import view, { encodeObjectURI, canCopyLink } from '@hcengineering/view' +import view, { canCopyLink, encodeObjectURI } from '@hcengineering/view' import { accessDeniedStore } from '@hcengineering/view-resources' import workbench, { type LocationData, type Widget, type WidgetTab } from '@hcengineering/workbench' -import { translate, getMetadata } from '@hcengineering/platform' -import { makeRank } from '@hcengineering/rank' -import { Analytics } from '@hcengineering/analytics' import { createWidgetTab } from '@hcengineering/workbench-resources' -import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text' +import attachment from '@hcengineering/attachment' import CardSearchItem from './components/CardSearchItem.svelte' import CreateSpace from './components/navigator/CreateSpace.svelte' import card from './plugin' @@ -92,6 +96,82 @@ export async function deleteMasterTag (tag: MasterTag | undefined, onDelete?: () } } +export async function duplicateCard (origin: Card): Promise { + const client = getClient() + const h = client.getHierarchy() + const props: Partial> = {} + const base = h.getBaseClass(origin._class) + const mixins = h.findAllMixins(origin) + const attrs = h.getAllAttributes(base, core.class.Doc) + + for (const [key, attr] of attrs) { + if (attr.readonly !== true && attr.hidden !== true) { + if (attr.type._class === core.class.Collection) { + ;(props as any)[key] = 0 + } else if ( + attr.type._class !== core.class.TypeCollaborativeDoc && + attr.type._class !== core.class.TypeIdentifier + ) { + ;(props as any)[key] = (origin as any)[key] + } + } + } + props.title = `${origin.title} (Copy)` + const targetId = generateId() + const relationsA = await client.findAll(core.class.Relation, { docA: origin._id }) + const relationsB = await client.findAll(core.class.Relation, { docB: origin._id }) + + const markup = await getMarkup(makeDocCollabId(origin, 'content'), origin.content) + if (!isEmptyMarkup(markup)) { + const collabId = makeCollabId(base, targetId, 'content') + props.content = await createMarkup(collabId, markup) + } + + const ops = client.apply(`Duplicate_card_${origin._id}`) + await ops.createDoc(base, origin.space, props, targetId) + for (const mixin of mixins) { + const mixinAttrs = h.getOwnAttributes(mixin) + const as = h.as(origin, mixin) + const attributes: Partial> = {} + for (const [key, attr] of mixinAttrs) { + if (attr.readonly !== true && attr.hidden !== true) { + ;(attributes as any)[key] = (as as any)[key] + } + } + await ops.createMixin(targetId, base, origin.space, mixin, attributes) + } + + for (const rel of relationsA) { + await ops.createDoc(core.class.Relation, origin.space, { + docA: targetId, + docB: rel.docB, + association: rel.association + }) + } + for (const rel of relationsB) { + await ops.createDoc(core.class.Relation, origin.space, { + docA: rel.docA, + docB: targetId, + association: rel.association + }) + } + await ops.commit() + + const attachments = await client.findAll(attachment.class.Attachment, { attachedTo: origin._id }) + const attachmentOps = client.apply(`Duplicate_attachments_${origin._id}`) + for (const att of attachments) { + const { _id, modifiedBy, modifiedOn, attachedTo, attachedToClass, collection, space, ...props } = att + await attachmentOps.addCollection(attachment.class.Attachment, origin.space, targetId, base, 'attachments', props) + } + await attachmentOps.commit() + + const loc = getCurrentLocation() + loc.path[2] = cardId + loc.path[3] = targetId + loc.path.length = 4 + navigate(loc) +} + export async function resolveLocation (loc: Location): Promise { if (loc.path[2] !== cardId) { return undefined diff --git a/plugins/card/src/index.ts b/plugins/card/src/index.ts index fb83fca1d3d..52dc389ab47 100644 --- a/plugins/card/src/index.ts +++ b/plugins/card/src/index.ts @@ -169,7 +169,8 @@ const cardPlugin = plugin(cardId, { Space: '' as Asset, Expand: '' as Asset, Feed: '' as Asset, - All: '' as Asset + All: '' as Asset, + Duplicate: '' as Asset }, extensions: { EditCardExtension: '' as ComponentExtensionId