mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-07 18:27:44 +02:00
Card version settings (#10843)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
@@ -162,6 +162,23 @@ export class Hierarchy {
|
||||
return Array.from(resultSet)
|
||||
}
|
||||
|
||||
getAllPossibleMixins (_class: Ref<Class<Doc>>, to: Ref<Class<Doc>> = core.class.Doc): Ref<Mixin<Doc>>[] {
|
||||
const result = new Set<Ref<Mixin<Doc>>>()
|
||||
let c = this.getClass(_class)
|
||||
|
||||
while (c._id !== to && c.extends !== undefined) {
|
||||
const _descendants = this.descendants.get(c._id)
|
||||
for (const d of _descendants ?? []) {
|
||||
if (this.isMixin(d) && this.getBaseClass(d) === c._id) {
|
||||
result.add(d)
|
||||
}
|
||||
}
|
||||
c = this.getClass(c.extends)
|
||||
}
|
||||
|
||||
return [...result]
|
||||
}
|
||||
|
||||
isMixin (_class: Ref<Class<Doc>>): boolean {
|
||||
const data = this.classifiers.get(_class)
|
||||
return data !== undefined && this._isMixin(data)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Class, Doc, PersonId, Ref } from './classes'
|
||||
import { Class, Doc, Mixin, PersonId, Ref } from './classes'
|
||||
|
||||
export interface VersionableDoc extends Doc {
|
||||
baseId?: Ref<Doc>
|
||||
@@ -10,4 +10,7 @@ export interface VersionableDoc extends Doc {
|
||||
|
||||
export interface VersionableClass extends Class<Doc> {
|
||||
enabled: boolean
|
||||
excludedProperties?: string[]
|
||||
excludedRelations?: string[] // ${associationId}_${a|b}
|
||||
excludeMixins?: Ref<Mixin<Doc>>[]
|
||||
}
|
||||
|
||||
@@ -447,4 +447,7 @@ export class TCollaborator extends TAttachedDoc implements Collaborator {
|
||||
@MMixin(core.mixin.VersionableClass, core.class.Class)
|
||||
export class TVersionableClass extends TClass implements VersionableClass {
|
||||
enabled!: boolean
|
||||
excludedProperties?: string[]
|
||||
excludedRelations?: string[] // ${associationId}_${a|b}
|
||||
excludeMixins?: Ref<Mixin<Doc>>[]
|
||||
}
|
||||
|
||||
@@ -600,6 +600,19 @@ export function defineFunctions (builder: Builder): void {
|
||||
process.function.StringFromNumber
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
process.class.ProcessFunction,
|
||||
core.space.Model,
|
||||
{
|
||||
of: core.class.TypeIdentifier,
|
||||
to: core.class.TypeString,
|
||||
category: 'attribute',
|
||||
label: process.string.TextFromIdentifier,
|
||||
type: 'convert'
|
||||
},
|
||||
process.function.StringFromIdentifier
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
process.class.ProcessFunction,
|
||||
core.space.Model,
|
||||
|
||||
@@ -207,6 +207,10 @@ export function createModel (builder: Builder): void {
|
||||
func: serverProcess.transform.DateFromNumber
|
||||
})
|
||||
|
||||
builder.mixin(process.function.StringFromIdentifier, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
|
||||
func: serverProcess.transform.StringFromIdentifier
|
||||
})
|
||||
|
||||
builder.mixin(process.function.NumberFromString, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
|
||||
func: serverProcess.transform.NumberFromString
|
||||
})
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
export let label: IntlString = ui.string.DropdownDefaultLabel
|
||||
export let params: Record<string, any> = {}
|
||||
export let items: DropdownIntlItem[]
|
||||
export let selected: DropdownIntlItem['id'] | undefined = undefined
|
||||
export let multiselect: boolean = false
|
||||
export let selected: DropdownIntlItem['id'] | Array<DropdownIntlItem['id']> | undefined = multiselect ? [] : undefined
|
||||
export let disabled: boolean = false
|
||||
export let kind: ButtonKind = 'regular'
|
||||
export let size: ButtonSize = 'small'
|
||||
@@ -48,9 +49,11 @@
|
||||
let container: HTMLElement
|
||||
let opened: boolean = false
|
||||
|
||||
$: selectedItem = items.find((x) => x.id === selected)
|
||||
$: if (shouldUpdateUndefined && selected === undefined && items[0] !== undefined) {
|
||||
selected = items[0].id
|
||||
$: selectedItem = multiselect
|
||||
? (items ?? []).filter((p) => (selected as Array<DropdownIntlItem['id']>)?.includes(p.id))
|
||||
: (items ?? []).find((x) => x.id === selected)
|
||||
$: if (shouldUpdateUndefined && selected === undefined && items?.[0] !== undefined) {
|
||||
selected = multiselect ? [items[0].id] : items[0].id
|
||||
dispatch('selected', selected)
|
||||
}
|
||||
|
||||
@@ -59,13 +62,24 @@
|
||||
function openPopup () {
|
||||
if (!opened) {
|
||||
opened = true
|
||||
showPopup(DropdownLabelsPopupIntl, { items, selected, params, withSearch }, container, (result) => {
|
||||
if (result) {
|
||||
selected = result
|
||||
dispatch('selected', result)
|
||||
showPopup(
|
||||
DropdownLabelsPopupIntl,
|
||||
{ items, selected, params, withSearch, multiselect },
|
||||
container,
|
||||
(result) => {
|
||||
if (result) {
|
||||
selected = result
|
||||
dispatch('selected', result)
|
||||
}
|
||||
opened = false
|
||||
},
|
||||
(result) => {
|
||||
if (result != null) {
|
||||
selected = result
|
||||
dispatch('selected', result)
|
||||
}
|
||||
}
|
||||
opened = false
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,10 +112,21 @@
|
||||
on:click={openPopup}
|
||||
>
|
||||
<span slot="content" class="overflow-label disabled flex-grow text-left mr-2">
|
||||
<Label
|
||||
label={selectedItem ? selectedItem.label : label}
|
||||
params={selectedItem ? (selectedItem.params ?? params) : params}
|
||||
/>
|
||||
{#if Array.isArray(selectedItem)}
|
||||
{#if selectedItem.length > 0}
|
||||
{#each selectedItem as item}
|
||||
<span class="step-row">
|
||||
<Label label={item.label} params={item.params ?? params} />
|
||||
</span>
|
||||
{/each}
|
||||
{:else}
|
||||
<Label {label} {params} />
|
||||
{/if}
|
||||
{:else if selectedItem}
|
||||
<Label label={selectedItem.label} params={selectedItem.params ?? params} />
|
||||
{:else}
|
||||
<Label {label} {params} />
|
||||
{/if}
|
||||
</span>
|
||||
<svelte:fragment slot="iconRight">
|
||||
<DropdownIcon
|
||||
@@ -111,3 +136,22 @@
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.step-row + .step-row {
|
||||
position: relative;
|
||||
margin-left: 0.75rem;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
top: 50%;
|
||||
left: -0.5rem;
|
||||
width: 0.25rem;
|
||||
height: 0.25rem;
|
||||
background-color: var(--dark-color);
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,12 +23,24 @@
|
||||
import ui from '../plugin'
|
||||
|
||||
export let items: DropdownIntlItem[]
|
||||
export let selected: DropdownIntlItem['id'] | undefined = undefined
|
||||
export let selected: DropdownIntlItem['id'] | Array<DropdownIntlItem['id']> | undefined = undefined
|
||||
export let multiselect: boolean = false
|
||||
export let params: Record<string, any> = {}
|
||||
export let withSearch: boolean = false
|
||||
export let searchPlaceholder: IntlString = ui.string.Search
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function isSelected (
|
||||
selected: DropdownIntlItem['id'] | Array<DropdownIntlItem['id']> | undefined,
|
||||
item: DropdownIntlItem
|
||||
): boolean {
|
||||
if (Array.isArray(selected)) {
|
||||
return selected.includes(item.id)
|
||||
} else {
|
||||
return item.id === selected
|
||||
}
|
||||
}
|
||||
let btns: HTMLButtonElement[] = []
|
||||
|
||||
const keyDown = (ev: KeyboardEvent, n?: number): void => {
|
||||
@@ -100,7 +112,18 @@
|
||||
keyDown(ev, i)
|
||||
}}
|
||||
on:click={() => {
|
||||
dispatch('close', item.id)
|
||||
if (multiselect && Array.isArray(selected)) {
|
||||
const index = selected.indexOf(item.id)
|
||||
if (index !== -1) {
|
||||
selected.splice(index, 1)
|
||||
selected = selected
|
||||
} else {
|
||||
selected = selected === undefined ? [item.id] : [...selected, item.id]
|
||||
}
|
||||
dispatch('update', selected)
|
||||
} else {
|
||||
dispatch('close', item.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex-grow caption-color nowrap flex-presenter flex-gap-2">
|
||||
@@ -110,7 +133,7 @@
|
||||
<Label label={item.label} params={item.params ?? params} />
|
||||
</div>
|
||||
<div class="check">
|
||||
{#if item.id === selected}<IconCheck size={'small'} />{/if}
|
||||
{#if isSelected(selected, item)}<IconCheck size={'small'} />{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Button, DropdownLabels, DropdownTextItem, getCurrentLocation, navigate, showPopup } from '@hcengineering/ui'
|
||||
import card from '../plugin'
|
||||
import NewVersionPopup from './NewVersionPopup.svelte'
|
||||
import { createNewVersion } from '../utils'
|
||||
|
||||
export let value: Card
|
||||
|
||||
@@ -70,10 +70,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
function newVersion (): void {
|
||||
showPopup(NewVersionPopup, {
|
||||
value
|
||||
})
|
||||
async function newVersion (): Promise<void> {
|
||||
const _id = await createNewVersion(value)
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[2] = cardId
|
||||
loc.path[3] = _id
|
||||
navigate(loc)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
<!--
|
||||
//
|
||||
// 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 { cardId, Card as CardType } from '@hcengineering/card'
|
||||
import core from '@hcengineering/core'
|
||||
import { Card, getClient } from '@hcengineering/presentation'
|
||||
import { getCurrentLocation, Label, navigate, Toggle } from '@hcengineering/ui'
|
||||
import plugin from '../plugin'
|
||||
import { createNewVersion } from '../utils'
|
||||
|
||||
export let value: CardType
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const ancestors = hierarchy.getAncestors(value._class)
|
||||
const mixins = hierarchy.findAllMixins(value)
|
||||
|
||||
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))
|
||||
|
||||
let relationsToCopy = new Set<string>([
|
||||
...relationsA.map((p) => `${p._id}_a`),
|
||||
...relationsB.map((p) => `${p._id}_b`)
|
||||
])
|
||||
|
||||
async function create (): Promise<void> {
|
||||
const _id = await createNewVersion(value, relationsToCopy)
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[2] = cardId
|
||||
loc.path[3] = _id
|
||||
navigate(loc)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card label={plugin.string.NewVersion} canSave={true} width={'small'} okAction={create} on:close>
|
||||
<div class="flex-col flex-gap-2">
|
||||
<Label label={plugin.string.NewVersionConfirmation} />
|
||||
{#if relationsA.length > 0 || relationsB.length > 0}
|
||||
<div>
|
||||
<Label label={plugin.string.RelationCopyDescr} />
|
||||
</div>
|
||||
<div class="grid">
|
||||
{#each relationsB as assoc}
|
||||
{@const id = `${assoc._id}_b`}
|
||||
<span>{assoc.nameB}</span>
|
||||
<Toggle
|
||||
on={relationsToCopy.has(id)}
|
||||
on:change={(e) => {
|
||||
if (e.detail === true) {
|
||||
relationsToCopy.add(id)
|
||||
} else {
|
||||
relationsToCopy.delete(id)
|
||||
}
|
||||
relationsToCopy = relationsToCopy
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{#each relationsA as assoc}
|
||||
{@const id = `${assoc._id}_a`}
|
||||
<span>{assoc.nameA}</span>
|
||||
<Toggle
|
||||
on={relationsToCopy.has(id)}
|
||||
on:change={(e) => {
|
||||
if (e.detail === true) {
|
||||
relationsToCopy.add(id)
|
||||
} else {
|
||||
relationsToCopy.delete(id)
|
||||
}
|
||||
relationsToCopy = relationsToCopy
|
||||
}}
|
||||
/>
|
||||
{/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>
|
||||
@@ -26,10 +26,13 @@
|
||||
getCurrentLocation,
|
||||
getPlatformColorDef,
|
||||
IconDelete,
|
||||
IconSettings,
|
||||
Label,
|
||||
ModernEditbox,
|
||||
navigate,
|
||||
showPopup,
|
||||
themeStore,
|
||||
Toggle,
|
||||
ToggleWithLabel
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
@@ -37,6 +40,7 @@
|
||||
import { exportModule } from '../../exporter'
|
||||
import card from '../../plugin'
|
||||
import { deleteMasterTag } from '../../utils'
|
||||
import VersioningSetting from './VersioningSetting.svelte'
|
||||
|
||||
export let masterTag: MasterTag
|
||||
|
||||
@@ -143,6 +147,12 @@
|
||||
})
|
||||
}
|
||||
|
||||
function versioningSetting (): void {
|
||||
showPopup(VersioningSetting, {
|
||||
masterTag: masterTag._id
|
||||
})
|
||||
}
|
||||
|
||||
let versioningEnabled = h.classHierarchyMixin(masterTag._id, core.mixin.VersionableClass)?.enabled
|
||||
$: versioningEnabled = h.classHierarchyMixin(masterTag._id, core.mixin.VersionableClass)?.enabled
|
||||
</script>
|
||||
@@ -191,15 +201,16 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if !h.isMixin(masterTag._id)}
|
||||
<div class="mx-2">
|
||||
<ToggleWithLabel
|
||||
label={card.string.Versioning}
|
||||
on={versioningEnabled}
|
||||
disabled={versioningEnabled}
|
||||
on:change={enableVersioning}
|
||||
/>
|
||||
<div class="mx-2 flex-between items-center">
|
||||
<Label label={card.string.Versioning} />
|
||||
<div class="flex items-center gap-1">
|
||||
{#if versioningEnabled}
|
||||
<ButtonIcon icon={setting.icon.Setting} size="extra-small" on:click={versioningSetting} />
|
||||
{/if}
|
||||
<Toggle on={versioningEnabled} disabled={versioningEnabled} on:change={enableVersioning} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-2">
|
||||
<div class="mx-2 pt-2">
|
||||
<ToggleWithLabel
|
||||
label={card.string.SingleColumn}
|
||||
on={masterTag.singleColumn}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
<!--
|
||||
//
|
||||
// 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 { Card, getClient } from '@hcengineering/presentation'
|
||||
import { DropdownLabels, DropdownLabelsIntl, Label, Toggle } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import card from '../../plugin'
|
||||
import setting from '@hcengineering/setting'
|
||||
|
||||
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, core.mixin.VersionableClass)
|
||||
|
||||
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
|
||||
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, core.mixin.VersionableClass, {
|
||||
excludedRelations: [...excludedRelations],
|
||||
excludedProperties: [...excludedProperties],
|
||||
excludeMixins: [...excludeMixins],
|
||||
enabled: true
|
||||
})
|
||||
} else {
|
||||
await client.createMixin(masterTag, card.class.MasterTag, core.space.Model, core.mixin.VersionableClass, {
|
||||
excludedRelations: [...excludedRelations],
|
||||
excludedProperties: [...excludedProperties],
|
||||
excludeMixins: [...excludeMixins],
|
||||
enabled: true
|
||||
})
|
||||
}
|
||||
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.NewVersion} 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}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#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>
|
||||
@@ -39,6 +39,7 @@ import core, {
|
||||
type Space,
|
||||
toRank,
|
||||
type TxOperations,
|
||||
type VersionableClass,
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import login from '@hcengineering/login'
|
||||
@@ -74,6 +75,7 @@ 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) {
|
||||
@@ -280,7 +282,7 @@ export async function createTypePermissions (masterTag: MasterTag | Tag): Promis
|
||||
async function cloneCard (
|
||||
origin: Card,
|
||||
overrideProps: Record<string, any>,
|
||||
relationToCopy: Set<string> | 'all',
|
||||
versionableClass?: VersionableClass,
|
||||
copyIds: boolean = false
|
||||
): Promise<Ref<Card>> {
|
||||
const client = getClient()
|
||||
@@ -292,8 +294,12 @@ async function cloneCard (
|
||||
const skipClasses = copyIds
|
||||
? [core.class.TypeCollaborativeDoc]
|
||||
: [core.class.TypeCollaborativeDoc, core.class.TypeIdentifier]
|
||||
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)) {
|
||||
continue
|
||||
}
|
||||
if (attr.type._class === core.class.Collection) {
|
||||
;(props as any)[key] = 0
|
||||
} else if (!skipClasses.includes(attr.type._class)) {
|
||||
@@ -303,19 +309,25 @@ async function cloneCard (
|
||||
for (const [k, v] of Object.entries(overrideProps)) {
|
||||
;(props as any)[k] = v
|
||||
}
|
||||
props.rank = makeRank(origin.rank, undefined)
|
||||
const targetId = generateId<Card>()
|
||||
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)
|
||||
if (versionableClass?.excludedProperties?.includes('content') !== true) {
|
||||
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) {
|
||||
if (versionableClass?.excludeMixins?.includes(mixin) === true) {
|
||||
continue
|
||||
}
|
||||
const mixinAttrs = h.getOwnAttributes(mixin)
|
||||
const as = h.as(origin, mixin)
|
||||
const attributes: Partial<Data<Doc>> = {}
|
||||
@@ -326,7 +338,7 @@ async function cloneCard (
|
||||
}
|
||||
|
||||
for (const rel of relationsA) {
|
||||
if (relationToCopy === 'all' || relationToCopy.has(`${rel.association}_b`)) {
|
||||
if (versionableClass?.excludedRelations?.includes(`${rel.association}_b`) !== true) {
|
||||
await ops.createDoc(core.class.Relation, core.space.Workspace, {
|
||||
docA: targetId,
|
||||
docB: rel.docB,
|
||||
@@ -335,7 +347,7 @@ async function cloneCard (
|
||||
}
|
||||
}
|
||||
for (const rel of relationsB) {
|
||||
if (relationToCopy === 'all' || relationToCopy.has(`${rel.association}_a`)) {
|
||||
if (versionableClass?.excludedRelations?.includes(`${rel.association}_a`) !== true) {
|
||||
await ops.createDoc(core.class.Relation, core.space.Workspace, {
|
||||
docA: rel.docA,
|
||||
docB: targetId,
|
||||
@@ -345,25 +357,23 @@ async function cloneCard (
|
||||
}
|
||||
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)
|
||||
if (versionableClass?.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) {
|
||||
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()
|
||||
}
|
||||
await attachmentOps.commit()
|
||||
|
||||
return targetId
|
||||
}
|
||||
|
||||
export async function duplicateCard (origin: Card): Promise<void> {
|
||||
const targetId = await cloneCard(
|
||||
origin,
|
||||
{
|
||||
title: `${origin.title} (Copy)`
|
||||
},
|
||||
'all'
|
||||
)
|
||||
const targetId = await cloneCard(origin, {
|
||||
title: `${origin.title} (Copy)`
|
||||
})
|
||||
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[2] = cardId
|
||||
@@ -513,14 +523,17 @@ export async function cardFactory (props: Record<string, any> = {}): Promise<Ref
|
||||
return await createCard(_class, space, props.data, props.content)
|
||||
}
|
||||
|
||||
export async function createNewVersion (card: Card, relationsToCopy: Set<string>): Promise<Ref<Card>> {
|
||||
export async function createNewVersion (card: Card): Promise<Ref<Card>> {
|
||||
const client = getClient()
|
||||
const mixin = client.getHierarchy().classHierarchyMixin(card._class, core.mixin.VersionableClass)
|
||||
|
||||
return await cloneCard(
|
||||
card,
|
||||
{
|
||||
baseId: card.baseId,
|
||||
docCreatedBy: card.docCreatedBy ?? card.createdBy ?? card.modifiedBy
|
||||
},
|
||||
relationsToCopy,
|
||||
mixin,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -267,6 +267,7 @@ export default mergeIds(processId, process, {
|
||||
UnlockField: '' as IntlString,
|
||||
Export: '' as IntlString,
|
||||
Import: '' as IntlString,
|
||||
TextFromIdentifier: '' as IntlString,
|
||||
TextFromNumber: '' as IntlString,
|
||||
TextFromDate: '' as IntlString,
|
||||
TextFromCheckbox: '' as IntlString,
|
||||
|
||||
@@ -378,6 +378,7 @@ export default plugin(processId, {
|
||||
EmptyValue: '' as Ref<ProcessFunction>,
|
||||
EmptyArray: '' as Ref<ProcessFunction>,
|
||||
CurrentDate: '' as Ref<ProcessFunction>,
|
||||
StringFromIdentifier: '' as Ref<ProcessFunction>,
|
||||
StringFromNumber: '' as Ref<ProcessFunction>,
|
||||
StringFromDate: '' as Ref<ProcessFunction>,
|
||||
StringFromMarkup: '' as Ref<ProcessFunction>,
|
||||
|
||||
@@ -107,6 +107,7 @@ import {
|
||||
LastValue,
|
||||
LowerCase,
|
||||
MarkupFromString,
|
||||
StringFromIdentifier,
|
||||
Max,
|
||||
Min,
|
||||
Modulo,
|
||||
@@ -800,6 +801,7 @@ export default async () => ({
|
||||
Max,
|
||||
StringFromMarkup,
|
||||
MarkupFromString,
|
||||
StringFromIdentifier,
|
||||
StringFromEnum,
|
||||
EnumFromString
|
||||
},
|
||||
|
||||
@@ -582,6 +582,10 @@ export function MarkupFromString (value: string): string {
|
||||
return value
|
||||
}
|
||||
|
||||
export function StringFromIdentifier (value: string): string {
|
||||
return value
|
||||
}
|
||||
|
||||
export function StringFromEnum (value: string): string {
|
||||
if (value == null) return ''
|
||||
return String(value)
|
||||
|
||||
@@ -126,6 +126,7 @@ export default plugin(serverProcessId, {
|
||||
NumberFromString: '' as Resource<TransformFunc>,
|
||||
DateFromString: '' as Resource<TransformFunc>,
|
||||
MarkupFromString: '' as Resource<TransformFunc>,
|
||||
StringFromIdentifier: '' as Resource<TransformFunc>,
|
||||
YearFromDate: '' as Resource<TransformFunc>,
|
||||
MonthFromDate: '' as Resource<TransformFunc>,
|
||||
DayFromDate: '' as Resource<TransformFunc>,
|
||||
|
||||
Reference in New Issue
Block a user