Id override (#10847)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2026-05-18 01:31:17 +05:00
committed by GitHub
parent f2a0c4a106
commit 372a4b0ca3
30 changed files with 594 additions and 84 deletions
+7 -1
View File
@@ -264,7 +264,13 @@ export function createActions (builder: Builder): void {
createAction(
builder,
{
action: card.actionImpl.DuplicateCard,
action: view.actionImpl.ShowPopup,
actionProps: {
component: card.component.DuplicateCard,
fillProps: {
_object: 'value'
}
},
label: card.string.Duplicate,
icon: card.icon.Duplicate,
input: 'focus',
+14 -4
View File
@@ -12,7 +12,6 @@
// limitations under the License.
import activity from '@hcengineering/activity'
import communication from '@hcengineering/communication'
import {
type CanCreateCardResource,
type Card,
@@ -23,6 +22,7 @@ import {
type CardViewDefaults,
type CreateCardExtension,
DOMAIN_CARD,
type DuplicateSetting,
type ExportExtension,
type ExportFunc,
type FavoriteCard,
@@ -34,6 +34,8 @@ import {
type Tag
} from '@hcengineering/card'
import chunter from '@hcengineering/chunter'
import communication from '@hcengineering/communication'
import converter from '@hcengineering/converter'
import core, {
AccountRole,
type Blobs,
@@ -46,6 +48,7 @@ import core, {
DOMAIN_SPACE,
IndexKind,
type MarkupBlobRef,
type Mixin as MixinType,
type MixinData,
type Rank,
type Ref,
@@ -77,7 +80,7 @@ import presentation from '@hcengineering/model-presentation'
import setting from '@hcengineering/model-setting'
import view, { type Viewlet } from '@hcengineering/model-view'
import workbench, { WidgetType } from '@hcengineering/model-workbench'
import converter from '@hcengineering/converter'
import notification from '@hcengineering/notification'
import { type Asset, getEmbeddedLabel, type IntlString, type Resource } from '@hcengineering/platform'
import time, { type ToDo } from '@hcengineering/time'
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
@@ -86,7 +89,6 @@ import { type BuildModelKey, type ViewOptionModel } from '@hcengineering/view'
import { createActions } from './actions'
import { defineActionPermissions, definePermissions } from './permissions'
import card from './plugin'
import notification from '@hcengineering/notification'
export { cardId } from '@hcengineering/card'
@@ -223,6 +225,13 @@ export class TExportExtension extends TDoc implements ExportExtension {
func!: Resource<ExportFunc>
}
@Mixin(card.mixin.DuplicateSetting, card.class.MasterTag)
export class TDuplicateSetting extends TMasterTag implements DuplicateSetting {
excludedProperties?: string[]
excludedRelations?: string[] // ${associationId}_${a|b}
excludeMixins?: Ref<MixinType<Doc>>[]
}
export * from './migration'
const showAllVersionsOption: ViewOptionModel = {
@@ -441,7 +450,8 @@ export function createModel (builder: Builder): void {
TFavoriteCard,
TFavoriteType,
TCreateCardExtension,
TExportExtension
TExportExtension,
TDuplicateSetting
)
builder.createDoc(
+1 -2
View File
@@ -30,7 +30,6 @@ export default mergeIds(cardId, card, {
},
actionImpl: {
DeleteMasterTag: '' as ViewAction,
DuplicateCard: '' as ViewAction,
EditSpace: '' as ViewAction,
CreateChild: '' as ViewAction
},
@@ -39,7 +38,7 @@ export default mergeIds(cardId, card, {
SetParent: '' as Ref<Action<Doc, any>>,
UnsetParent: '' as Ref<Action<Doc, any>>,
PublicLink: '' as Ref<Action<Doc, any>>,
Duplicate: '' as Ref<Action>,
Duplicate: '' as Ref<Action<Doc, any>>,
CreateChild: '' as Ref<Action>
},
category: {
@@ -71,26 +71,14 @@
<div class="divider" />
{/if}
<div class="masterTag">
<MasterTagAttributes
bind:this={masterTagAttributes}
readonly={readonly || h.getAncestors(value._class).some((p) => value.readonlySections?.includes(p))}
{value}
{ignoreKeys}
fourRows={columns === 2}
/>
<MasterTagAttributes bind:this={masterTagAttributes} {readonly} {value} {ignoreKeys} fourRows={columns === 2} />
</div>
{#if mixins.length > 0}
<div class="divider" />
<Grid column={columns} columnGap={0} rowGap={0} alignItems={'start'}>
{#each mixins as tag, i (tag._id)}
<div class="tag" class:withoutBorder={Math.ceil((i + 1) / columns) === Math.ceil(mixins.length / columns)}>
<TagAttributes
bind:this={tagAttributes[i]}
readonly={readonly || value.readonlySections?.includes(tag._id)}
{tag}
{value}
{ignoreKeys}
/>
<TagAttributes bind:this={tagAttributes[i]} {readonly} {tag} {value} {ignoreKeys} />
</div>
{/each}
</Grid>
@@ -0,0 +1,203 @@
<!--
//
// Copyright © 2026 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
-->
<script lang="ts">
import core, { Doc, Mixin, Ref } from '@hcengineering/core'
import { Card, getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
import { DropdownLabelsIntl, Label, Toggle } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import card from '../plugin'
import { duplicateCard } from '../utils'
import { Card as CardType } from '@hcengineering/card'
export let value: CardType
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
const ancestors = hierarchy.getAncestors(value._class)
const mixins = hierarchy.findAllMixins(value)
const current = hierarchy.classHierarchyMixin(value._class, card.mixin.DuplicateSetting)
const mixinsItems = mixins.map((it) => {
const cl = hierarchy.getClass(it)
return { label: cl.label, id: it }
})
let excludedRelations = new Set<string>(current?.excludedRelations ?? [])
let excludedProperties = new Set<string>(current?.excludedProperties ?? [])
let excludeMixins = new Set<Ref<Mixin<Doc>>>(current?.excludeMixins ?? [])
$: selectedMixins = mixins.filter((it) => !excludeMixins.has(it))
const systemFields = [
'_class',
'id',
'createdOn',
'modifiedOn',
'modifiedBy',
'createdBy',
'createdOn',
'rank',
'title',
'space',
'version',
'icon',
'color',
'todos',
'comments'
]
const allProperties = hierarchy
.getAllAttributes(value._class, core.class.Doc)
.values()
.toArray()
.filter((it) => {
if (systemFields.includes(it.name)) return false
if (it.readonly === true || it.hidden === true) return false
if (it.type._class === core.class.TypeIdentifier) return false
return true
})
.map((p) => {
return { label: p.label, id: p.name }
})
$: selectedProperties = allProperties.filter((it) => !excludedProperties.has(it.id)).map((it) => it.id)
const targets = new Set([...ancestors, ...mixins])
const associations = client.getModel().findAllSync(core.class.Association, {})
const relationsA = associations.filter((it) => targets.has(it.classB))
const relationsB = associations.filter((it) => targets.has(it.classA))
async function save (): Promise<void> {
await duplicateCard(value, {
excludedProperties: [...excludedProperties],
excludedRelations: [...excludedRelations],
excludeMixins: [...excludeMixins]
})
dispatch('close')
}
function propertiesSelected (event: CustomEvent): void {
const selectedProperties = event.detail as string[]
allProperties.forEach((it) => {
if (!selectedProperties.includes(it.id)) {
excludedProperties.add(it.id)
} else {
excludedProperties.delete(it.id)
}
})
excludedProperties = excludedProperties
}
function mixinsSelected (event: CustomEvent): void {
const selectedMixins = event.detail as string[]
mixins.forEach((it) => {
if (!selectedMixins.includes(it)) {
excludeMixins.add(it)
} else {
excludeMixins.delete(it)
}
})
excludeMixins = excludeMixins
}
</script>
<Card label={card.string.Duplicate} canSave={true} width={'medium'} okAction={save} on:close>
<div class="flex-col flex-gap-2">
{#if allProperties.length > 0}
<div class="flex-between">
<Label label={setting.string.Properties} />
<div class="max-w-40">
<DropdownLabelsIntl
width="100%"
items={allProperties}
multiselect
selected={selectedProperties}
on:selected={propertiesSelected}
/>
</div>
</div>
{/if}
{#if mixins.length > 0}
<div class="flex-between">
<Label label={card.string.Tags} />
<div class="max-w-40">
<DropdownLabelsIntl
width="100%"
items={mixinsItems}
multiselect
selected={selectedMixins}
on:selected={mixinsSelected}
/>
</div>
</div>
{/if}
{#if relationsA.length > 0 || relationsB.length > 0}
<div class="divider" />
<div>
<Label label={card.string.RelationCopyDescr} />
</div>
<div class="grid">
{#each relationsB as assoc}
{@const id = `${assoc._id}_b`}
<span>{assoc.nameB}</span>
<Toggle
on={!excludedRelations.has(id)}
on:change={(e) => {
if (e.detail === true) {
excludedRelations.delete(id)
} else {
excludedRelations.add(id)
}
excludedRelations = excludedRelations
}}
/>
{/each}
{#each relationsA as assoc}
{@const id = `${assoc._id}_a`}
<span>{assoc.nameA}</span>
<Toggle
on={!excludedRelations.has(id)}
on:change={(e) => {
if (e.detail === true) {
excludedRelations.delete(id)
} else {
excludedRelations.add(id)
}
excludedRelations = excludedRelations
}}
/>
{/each}
</div>
{/if}
</div>
</Card>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 3fr 1fr;
grid-auto-rows: minmax(1rem, max-content);
justify-content: start;
width: 100%;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
height: min-content;
}
</style>
@@ -277,7 +277,7 @@
}}
/>
{#if !_readonly}
{#if !readonly}
<Button
icon={IconMoreH}
iconProps={{ size: 'medium' }}
@@ -57,6 +57,7 @@
}
$: isLocked = hierarchy.getAncestors(value._class).some((p) => value.readonlySections?.includes(p)) ?? false
$: _readonly = readonly || isLocked
$: canLock = canLockSection(value.space, $permissionsStore)
$: canUnlock = canUnlockSection(value.space, $permissionsStore)
@@ -85,6 +86,7 @@
icon={isLocked ? Lock : Unlock}
kind={'link'}
size={'medium'}
disabled={readonly}
showTooltip={{ label: isLocked ? card.string.UnLockSection : card.string.LockSection }}
on:click={toggleLock}
/>
@@ -120,8 +122,8 @@
</div>
</div>
<ExpandCollapse isExpanded={!isCollapsed}>
<CardAttributes object={value} _class={value._class} {readonly} {ignoreKeys} {fourRows} showCollaborators />
<MarkupProperties doc={value} {readonly} tag={undefined} />
<CardAttributes object={value} _class={value._class} readonly={_readonly} {ignoreKeys} {fourRows} showCollaborators />
<MarkupProperties doc={value} readonly={_readonly} tag={undefined} />
</ExpandCollapse>
<style lang="scss">
@@ -64,6 +64,7 @@
$: isLocked = value.readonlySections?.includes(tag._id) ?? false
$: canLock = canLockSection(value.space, $permissionsStore)
$: canUnlock = canUnlockSection(value.space, $permissionsStore)
$: _readonly = readonly || isLocked
async function toggleLock (ev: MouseEvent): Promise<void> {
ev.stopPropagation()
@@ -126,8 +127,8 @@
</div>
</div>
<ExpandCollapse isExpanded={!isCollapsed}>
<CardAttributes object={value} _class={tag._id} to={tag.extends} {readonly} {ignoreKeys} />
<MarkupProperties doc={value} {readonly} {tag} />
<CardAttributes object={value} _class={tag._id} to={tag.extends} readonly={_readonly} {ignoreKeys} />
<MarkupProperties doc={value} readonly={_readonly} {tag} />
</ExpandCollapse>
<style lang="scss">
@@ -0,0 +1,217 @@
<!--
//
// Copyright © 2026 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
-->
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import core, { Doc, Mixin, Ref } from '@hcengineering/core'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
import { DropdownLabelsIntl, Label, Toggle } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import card from '../../plugin'
export let masterTag: Ref<MasterTag>
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
const ancestors = hierarchy.getAncestors(masterTag)
const mixins = hierarchy.getAllPossibleMixins(masterTag)
const current = hierarchy.classHierarchyMixin(masterTag, card.mixin.DuplicateSetting)
const mixinsItems = mixins.map((it) => {
const cl = hierarchy.getClass(it)
return { label: cl.label, id: it }
})
let excludedRelations = new Set<string>(current?.excludedRelations ?? [])
let excludedProperties = new Set<string>(current?.excludedProperties ?? [])
let excludeMixins = new Set<Ref<Mixin<Doc>>>(current?.excludeMixins ?? [])
$: selectedMixins = mixins.filter((it) => !excludeMixins.has(it))
const systemFields = [
'_class',
'id',
'createdOn',
'modifiedOn',
'modifiedBy',
'createdBy',
'createdOn',
'rank',
'title',
'space',
'version',
'icon',
'color',
'todos',
'comments'
]
const allProperties = hierarchy
.getAllAttributes(masterTag, core.class.Doc)
.values()
.toArray()
.filter((it) => {
if (systemFields.includes(it.name)) return false
if (it.readonly === true || it.hidden === true) return false
if (it.type._class === core.class.TypeIdentifier) return false
return true
})
.map((p) => {
return { label: p.label, id: p.name }
})
$: selectedProperties = allProperties.filter((it) => !excludedProperties.has(it.id)).map((it) => it.id)
const targets = new Set([...ancestors, ...mixins])
const associations = client.getModel().findAllSync(core.class.Association, {})
const relationsA = associations.filter((it) => targets.has(it.classB))
const relationsB = associations.filter((it) => targets.has(it.classA))
async function save (): Promise<void> {
if (current?._id === masterTag) {
await client.updateMixin(masterTag, card.class.MasterTag, core.space.Model, card.mixin.DuplicateSetting, {
excludedRelations: [...excludedRelations],
excludedProperties: [...excludedProperties],
excludeMixins: [...excludeMixins]
})
} else {
await client.createMixin(masterTag, card.class.MasterTag, core.space.Model, card.mixin.DuplicateSetting, {
excludedRelations: [...excludedRelations],
excludedProperties: [...excludedProperties],
excludeMixins: [...excludeMixins]
})
}
dispatch('close')
}
function propertiesSelected (event: CustomEvent): void {
const selectedProperties = event.detail as string[]
allProperties.forEach((it) => {
if (!selectedProperties.includes(it.id)) {
excludedProperties.add(it.id)
} else {
excludedProperties.delete(it.id)
}
})
excludedProperties = excludedProperties
}
function mixinsSelected (event: CustomEvent): void {
const selectedMixins = event.detail as string[]
mixins.forEach((it) => {
if (!selectedMixins.includes(it)) {
excludeMixins.add(it)
} else {
excludeMixins.delete(it)
}
})
excludeMixins = excludeMixins
}
</script>
<Card
label={card.string.Duplicate}
okLabel={presentation.string.Save}
canSave={true}
width={'medium'}
okAction={save}
on:close
>
<div class="flex-col flex-gap-2">
{#if allProperties.length > 0}
<div class="flex-between">
<Label label={setting.string.Properties} />
<div class="max-w-40">
<DropdownLabelsIntl
width="100%"
items={allProperties}
multiselect
selected={selectedProperties}
on:selected={propertiesSelected}
/>
</div>
</div>
{/if}
{#if mixins.length > 0}
<div class="flex-between">
<Label label={card.string.Tags} />
<div class="max-w-40">
<DropdownLabelsIntl
width="100%"
items={mixinsItems}
multiselect
selected={selectedMixins}
on:selected={mixinsSelected}
/>
</div>
</div>
{/if}
{#if relationsA.length > 0 || relationsB.length > 0}
<div class="divider" />
<div>
<Label label={card.string.RelationCopyDescr} />
</div>
<div class="grid">
{#each relationsB as assoc}
{@const id = `${assoc._id}_b`}
<span>{assoc.nameB}</span>
<Toggle
on={!excludedRelations.has(id)}
on:change={(e) => {
if (e.detail === true) {
excludedRelations.delete(id)
} else {
excludedRelations.add(id)
}
excludedRelations = excludedRelations
}}
/>
{/each}
{#each relationsA as assoc}
{@const id = `${assoc._id}_a`}
<span>{assoc.nameA}</span>
<Toggle
on={!excludedRelations.has(id)}
on:change={(e) => {
if (e.detail === true) {
excludedRelations.delete(id)
} else {
excludedRelations.add(id)
}
excludedRelations = excludedRelations
}}
/>
{/each}
</div>
{/if}
</div>
</Card>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 3fr 1fr;
grid-auto-rows: minmax(1rem, max-content);
justify-content: start;
width: 100%;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
height: min-content;
}
</style>
@@ -41,6 +41,7 @@
import card from '../../plugin'
import { deleteMasterTag } from '../../utils'
import VersioningSetting from './VersioningSetting.svelte'
import DuplicateSetting from './DuplicateSetting.svelte'
export let masterTag: MasterTag
@@ -153,6 +154,12 @@
})
}
function duplicateSetting (): void {
showPopup(DuplicateSetting, {
masterTag: masterTag._id
})
}
let versioningEnabled = h.classHierarchyMixin(masterTag._id, core.mixin.VersionableClass)?.enabled
$: versioningEnabled = h.classHierarchyMixin(masterTag._id, core.mixin.VersionableClass)?.enabled
</script>
@@ -217,6 +224,12 @@
on:change={(e) => attributeUpdated('singleColumn', e.detail)}
/>
</div>
<div class="mx-2 flex-between items-center">
<Label label={card.string.Duplicate} />
<div class="flex items-center gap-1">
<ButtonIcon icon={setting.icon.Setting} size="extra-small" on:click={duplicateSetting} />
</div>
</div>
{/if}
</div>
@@ -17,11 +17,11 @@
<script lang="ts">
import { MasterTag } from '@hcengineering/card'
import core, { Doc, Mixin, Ref } from '@hcengineering/core'
import { Card, getClient } from '@hcengineering/presentation'
import { DropdownLabels, DropdownLabelsIntl, Label, Toggle } from '@hcengineering/ui'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
import { DropdownLabelsIntl, Label, Toggle } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import card from '../../plugin'
import setting from '@hcengineering/setting'
export let masterTag: Ref<MasterTag>
@@ -124,32 +124,43 @@
}
</script>
<Card label={card.string.NewVersion} canSave={true} width={'medium'} okAction={save} on:close>
<Card
label={card.string.NewVersion}
okLabel={presentation.string.Save}
canSave={true}
width={'medium'}
okAction={save}
on:close
>
<div class="flex-col flex-gap-2">
<div class="flex-between">
<Label label={setting.string.Properties} />
<div class="max-w-40">
<DropdownLabelsIntl
width="100%"
items={allProperties}
multiselect
selected={selectedProperties}
on:selected={propertiesSelected}
/>
{#if allProperties.length > 0}
<div class="flex-between">
<Label label={setting.string.Properties} />
<div class="max-w-40">
<DropdownLabelsIntl
width="100%"
items={allProperties}
multiselect
selected={selectedProperties}
on:selected={propertiesSelected}
/>
</div>
</div>
</div>
<div class="flex-between">
<Label label={card.string.Tags} />
<div class="max-w-40">
<DropdownLabelsIntl
width="100%"
items={mixinsItems}
multiselect
selected={selectedMixins}
on:selected={mixinsSelected}
/>
{/if}
{#if mixins.length > 0}
<div class="flex-between">
<Label label={card.string.Tags} />
<div class="max-w-40">
<DropdownLabelsIntl
width="100%"
items={mixinsItems}
multiselect
selected={selectedMixins}
on:selected={mixinsSelected}
/>
</div>
</div>
</div>
{/if}
{#if relationsA.length > 0 || relationsB.length > 0}
<div class="divider" />
<div>
+3 -3
View File
@@ -35,7 +35,6 @@ import {
getSpaceAccessPublicLink,
canGetSpaceAccessPublicLink,
cardFactory,
duplicateCard,
checkChildrenSectionVisibility,
createChildAction,
showAllVersions
@@ -69,6 +68,7 @@ import CardWidget from './components/CardWidget.svelte'
import CreateSpace from './components/navigator/CreateSpace.svelte'
import CardHeaderButton from './components/navigator/CardHeaderButton.svelte'
import MyCards from './components/navigator/MyCards.svelte'
import DuplicateCard from './components/DuplicateCard.svelte'
// Card Sections
import AttachmentsCardSection from './components/sections/AttachmentsSection.svelte'
@@ -141,7 +141,8 @@ export default async (): Promise<Resources> => ({
CreateSpace,
CardHeaderButton,
CreateRolePopup,
MyCards
MyCards,
DuplicateCard
},
sectionComponent: {
AttachmentsSection: AttachmentsCardSection,
@@ -161,7 +162,6 @@ export default async (): Promise<Resources> => ({
},
actionImpl: {
DeleteMasterTag: deleteMasterTag,
DuplicateCard: duplicateCard,
EditSpace: editSpace,
CreateChild: createChildAction
},
+1
View File
@@ -28,6 +28,7 @@ export default mergeIds(cardId, card, {
ManageMasterTagsTools: '' as AnyComponent,
MasterTags: '' as AnyComponent,
CreateTag: '' as AnyComponent,
DuplicateCard: '' as AnyComponent,
CardPresenter: '' as AnyComponent,
CardsPresenter: '' as AnyComponent,
FavoriteCardPresenter: '' as AnyComponent,
+24 -14
View File
@@ -34,12 +34,12 @@ import core, {
makeDocCollabId,
type Markup,
type MarkupBlobRef,
type Mixin,
type Ref,
type RelatedDocument,
type Space,
toRank,
type TxOperations,
type VersionableClass,
type WithLookup
} from '@hcengineering/core'
import login from '@hcengineering/login'
@@ -70,12 +70,12 @@ import workbench, { type LocationData, type Widget, type WidgetTab } from '@hcen
import { createWidgetTab } from '@hcengineering/workbench-resources'
import attachment from '@hcengineering/attachment'
import { makeRank } from '@hcengineering/rank'
import { writable } from 'svelte/store'
import CardSearchItem from './components/CardSearchItem.svelte'
import CreateSpace from './components/navigator/CreateSpace.svelte'
import card from './plugin'
import { type NavigatorConfig } from './types'
import { writable } from 'svelte/store'
import { makeRank } from '@hcengineering/rank'
export async function deleteMasterTag (tag: MasterTag | undefined, onDelete?: () => void): Promise<void> {
if (tag !== undefined) {
@@ -279,10 +279,16 @@ export async function createTypePermissions (masterTag: MasterTag | Tag): Promis
}
}
interface CopySettings {
excludedProperties?: string[]
excludedRelations?: string[] // ${associationId}_${a|b}
excludeMixins?: Array<Ref<Mixin<Doc>>>
}
async function cloneCard (
origin: Card,
overrideProps: Record<string, any>,
versionableClass?: VersionableClass,
config?: CopySettings,
copyIds: boolean = false
): Promise<Ref<Card>> {
const client = getClient()
@@ -297,7 +303,7 @@ async function cloneCard (
const systemFields = ['_class', 'id', 'createdOn', 'modifiedOn', 'modifiedBy', 'createdBy', 'createdOn', 'rank']
for (const [key, attr] of attrs) {
if (versionableClass?.excludedProperties?.includes(key) === true || systemFields.includes(key)) {
if (config?.excludedProperties?.includes(key) === true || systemFields.includes(key)) {
continue
}
if (attr.type._class === core.class.Collection) {
@@ -314,7 +320,7 @@ async function cloneCard (
const relationsA = await client.findAll(core.class.Relation, { docA: origin._id })
const relationsB = await client.findAll(core.class.Relation, { docB: origin._id })
if (versionableClass?.excludedProperties?.includes('content') !== true) {
if (config?.excludedProperties?.includes('content') !== true) {
const markup = await getMarkup(makeDocCollabId(origin, 'content'), origin.content)
if (!isEmptyMarkup(markup)) {
const collabId = makeCollabId(base, targetId, 'content')
@@ -325,7 +331,7 @@ async function cloneCard (
const ops = client.apply(`Duplicate_card_${origin._id}`)
await ops.createDoc(base, origin.space, props, targetId)
for (const mixin of mixins) {
if (versionableClass?.excludeMixins?.includes(mixin) === true) {
if (config?.excludeMixins?.includes(mixin) === true) {
continue
}
const mixinAttrs = h.getOwnAttributes(mixin)
@@ -338,7 +344,7 @@ async function cloneCard (
}
for (const rel of relationsA) {
if (versionableClass?.excludedRelations?.includes(`${rel.association}_b`) !== true) {
if (config?.excludedRelations?.includes(`${rel.association}_b`) !== true) {
await ops.createDoc(core.class.Relation, core.space.Workspace, {
docA: targetId,
docB: rel.docB,
@@ -347,7 +353,7 @@ async function cloneCard (
}
}
for (const rel of relationsB) {
if (versionableClass?.excludedRelations?.includes(`${rel.association}_a`) !== true) {
if (config?.excludedRelations?.includes(`${rel.association}_a`) !== true) {
await ops.createDoc(core.class.Relation, core.space.Workspace, {
docA: rel.docA,
docB: targetId,
@@ -357,7 +363,7 @@ async function cloneCard (
}
await ops.commit()
if (versionableClass?.excludedProperties?.includes('attachments') !== true) {
if (config?.excludedProperties?.includes('attachments') !== true) {
const attachments = await client.findAll(attachment.class.Attachment, { attachedTo: origin._id })
const attachmentOps = client.apply(`Duplicate_attachments_${origin._id}`)
for (const att of attachments) {
@@ -370,10 +376,14 @@ async function cloneCard (
return targetId
}
export async function duplicateCard (origin: Card): Promise<void> {
const targetId = await cloneCard(origin, {
title: `${origin.title} (Copy)`
})
export async function duplicateCard (origin: Card, config?: CopySettings): Promise<void> {
const targetId = await cloneCard(
origin,
{
title: `${origin.title} (Copy)`
},
config
)
const loc = getCurrentLocation()
loc.path[2] = cardId
+8 -1
View File
@@ -48,6 +48,12 @@ export interface Role extends BaseRole {
types: Ref<MasterTag | Tag>[]
}
export interface DuplicateSetting extends Class<MasterTag> {
excludedProperties?: string[]
excludedRelations?: string[] // ${associationId}_${a|b}
excludeMixins?: Ref<Mixin<Doc>>[]
}
export interface Card extends Doc, IconProps, VersionableDoc {
_class: Ref<MasterTag>
title: string
@@ -157,7 +163,8 @@ const cardPlugin = plugin(cardId, {
},
mixin: {
CardViewDefaults: '' as Ref<Mixin<CardViewDefaults>>,
CreateCardExtension: '' as Ref<Mixin<CreateCardExtension>>
CreateCardExtension: '' as Ref<Mixin<CreateCardExtension>>,
DuplicateSetting: '' as Ref<Mixin<DuplicateSetting>>
},
space: {
Default: '' as Ref<CardSpace>
+2 -1
View File
@@ -237,6 +237,7 @@
"TwoFactorAuthEnabled": "Dvoufaktorové ověřování je povoleno",
"TwoFactorAuthDisabled": "Dvoufaktorové ověřování je zakázáno",
"ShowQRCode": "Zobrazit QR kód",
"EnterVerificationCode": "Zadejte ověřovací kód"
"EnterVerificationCode": "Zadejte ověřovací kód",
"OverrideAttribute": "Přepsat atribut"
}
}
+2 -1
View File
@@ -239,6 +239,7 @@
"TwoFactorAuthEnabled": "Zweistufige Authentifizierung ist aktiviert",
"TwoFactorAuthDisabled": "Zweistufige Authentifizierung ist deaktiviert",
"ShowQRCode": "QR-Code anzeigen",
"EnterVerificationCode": "Verifizierungscode eingeben"
"EnterVerificationCode": "Verifizierungscode eingeben",
"OverrideAttribute": "Überschreibattribut"
}
}
+2 -1
View File
@@ -239,6 +239,7 @@
"TwoFactorAuthEnabled": "Two-factor authentication is enabled",
"TwoFactorAuthDisabled": "Two-factor authentication is disabled",
"ShowQRCode": "Show QR code",
"EnterVerificationCode": "Enter verification code"
"EnterVerificationCode": "Enter verification code",
"OverrideAttribute": "Override attribute"
}
}
+2 -1
View File
@@ -230,6 +230,7 @@
"TwoFactorAuthEnabled": "La autenticación de dos factores está habilitada",
"TwoFactorAuthDisabled": "La autenticación de dos factores está deshabilitada",
"ShowQRCode": "Mostrar código QR",
"EnterVerificationCode": "Introducir código de verificación"
"EnterVerificationCode": "Introducir código de verificación",
"OverrideAttribute": "Sobreescribir atributo"
}
}
+2 -1
View File
@@ -239,6 +239,7 @@
"TwoFactorAuthEnabled": "L'authentification à deux facteurs est activée",
"TwoFactorAuthDisabled": "L'authentification à deux facteurs est désactivée",
"ShowQRCode": "Afficher le code QR",
"EnterVerificationCode": "Entrer le code de vérification"
"EnterVerificationCode": "Entrer le code de vérification",
"OverrideAttribute": "Surcharger l'attribut"
}
}
+2 -1
View File
@@ -239,6 +239,7 @@
"TwoFactorAuthEnabled": "L'autenticazione a due fattori è abilitata",
"TwoFactorAuthDisabled": "L'autenticazione a due fattori è disabilitata",
"ShowQRCode": "Mostra codice QR",
"EnterVerificationCode": "Inserisci codice di verifica"
"EnterVerificationCode": "Inserisci codice di verifica",
"OverrideAttribute": "Sovrascrivi attributo"
}
}
+2 -1
View File
@@ -239,6 +239,7 @@
"TwoFactorAuthEnabled": "二要素認証は有効です",
"TwoFactorAuthDisabled": "二要素認証は無効です",
"ShowQRCode": "QRコードを表示",
"EnterVerificationCode": "確認コードを入力"
"EnterVerificationCode": "確認コードを入力",
"OverrideAttribute": "属性を上書き"
}
}
+2 -1
View File
@@ -239,6 +239,7 @@
"TwoFactorAuthEnabled": "2단계 인증이 활성화됨",
"TwoFactorAuthDisabled": "2단계 인증이 비활성화됨",
"ShowQRCode": "QR 코드 표시",
"EnterVerificationCode": "인증 코드 입력"
"EnterVerificationCode": "인증 코드 입력",
"OverrideAttribute": "속성 재정의"
}
}
+2 -1
View File
@@ -230,6 +230,7 @@
"TwoFactorAuthEnabled": "Autenticação de dois fatores está ativada",
"TwoFactorAuthDisabled": "Autenticação de dois fatores está desativada",
"ShowQRCode": "Mostrar código QR",
"EnterVerificationCode": "Inserir código de verificação"
"EnterVerificationCode": "Inserir código de verificação",
"OverrideAttribute": "Sobrescrever atributo"
}
}
+2 -1
View File
@@ -230,6 +230,7 @@
"TwoFactorAuthEnabled": "Autenticação de dois fatores está ativada",
"TwoFactorAuthDisabled": "Autenticação de dois fatores está desativada",
"ShowQRCode": "Mostrar código QR",
"EnterVerificationCode": "Inserir código de verificação"
"EnterVerificationCode": "Inserir código de verificação",
"OverrideAttribute": "Sobrescrever atributo"
}
}
+2 -1
View File
@@ -239,6 +239,7 @@
"TwoFactorAuthEnabled": "Двухфакторная аутентификация включена",
"TwoFactorAuthDisabled": "Двухфакторная аутентификация отключена",
"ShowQRCode": "Показать QR-код",
"EnterVerificationCode": "Введите код подтверждения"
"EnterVerificationCode": "Введите код подтверждения",
"OverrideAttribute": "Переопределить атрибут"
}
}
+2 -1
View File
@@ -239,6 +239,7 @@
"TwoFactorAuthEnabled": "İki faktörlü kimlik doğrulama etkin",
"TwoFactorAuthDisabled": "İki faktörlü kimlik doğrulama devre dışı",
"ShowQRCode": "QR kodu göster",
"EnterVerificationCode": "Doğrulama kodunu gir"
"EnterVerificationCode": "Doğrulama kodunu gir",
"OverrideAttribute": "Özniteliği geçersiz kıl"
}
}
+2 -1
View File
@@ -239,6 +239,7 @@
"TwoFactorAuthEnabled": "双因素认证已启用",
"TwoFactorAuthDisabled": "双因素认证已禁用",
"ShowQRCode": "显示QR码",
"EnterVerificationCode": "输入验证码"
"EnterVerificationCode": "输入验证码",
"OverrideAttribute": "覆盖属性"
}
}
@@ -31,6 +31,7 @@
import {
Action,
AnySvelteComponent,
IconCopy,
IconDelete,
IconEdit,
Menu,
@@ -42,6 +43,7 @@
import ClassAttributeRow from './ClassAttributeRow.svelte'
import { makeRank } from '@hcengineering/rank'
import EditAttribute from './EditAttribute.svelte'
import { TypeIdentifier } from '@hcengineering/model'
export let _class: Ref<Class<Doc>>
export let ofClass: Ref<Class<Doc>> | undefined = undefined
@@ -97,6 +99,23 @@
showPopup(EditAttribute, { attribute, exist }, 'top', update)
}
export async function overrideAttribute (source: AnyAttribute): Promise<void> {
const newSeq = await client.createDoc(core.class.CustomSequence, core.space.Workspace, {
prefix: '',
sequence: 0,
attachedTo: core.class.CustomSequence
})
const _id = await client.createDoc(core.class.Attribute, core.space.Model, {
...source,
type: TypeIdentifier(newSeq),
attributeOf: _class
})
const attribute = await client.findOne(core.class.Attribute, _id)
if (attribute !== undefined) {
showPopup(EditAttribute, { attribute, exist: true }, 'top', update)
}
}
export async function removeAttribute (attribute: AnyAttribute, exist: boolean): Promise<void> {
showPopup(
MessageBox,
@@ -125,6 +144,15 @@
}
]
if (attribute.isCustom === true) {
if (attribute.attributeOf !== _class && attribute.type._class === core.class.TypeIdentifier) {
actions.push({
label: settings.string.OverrideAttribute,
icon: IconCopy,
action: async () => {
await overrideAttribute(attribute)
}
})
}
actions.push({
label: presentation.string.Remove,
icon: IconDelete,
+2 -1
View File
@@ -166,6 +166,7 @@ export default mergeIds(settingId, setting, {
ShowInTitle: '' as IntlString,
SpaceMembersOnly: '' as IntlString,
LastOwnerLeaveTitle: '' as IntlString,
LastOwnerLeaveMessage: '' as IntlString
LastOwnerLeaveMessage: '' as IntlString,
OverrideAttribute: '' as IntlString
}
})