diff --git a/foundations/core/packages/core/src/hierarchy.ts b/foundations/core/packages/core/src/hierarchy.ts index 9b8fc3665c..32d055c7a4 100644 --- a/foundations/core/packages/core/src/hierarchy.ts +++ b/foundations/core/packages/core/src/hierarchy.ts @@ -162,6 +162,23 @@ export class Hierarchy { return Array.from(resultSet) } + getAllPossibleMixins (_class: Ref>, to: Ref> = core.class.Doc): Ref>[] { + const result = new Set>>() + 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>): boolean { const data = this.classifiers.get(_class) return data !== undefined && this._isMixin(data) diff --git a/foundations/core/packages/core/src/versioning.ts b/foundations/core/packages/core/src/versioning.ts index 34db2fd7bb..6ab64893a5 100644 --- a/foundations/core/packages/core/src/versioning.ts +++ b/foundations/core/packages/core/src/versioning.ts @@ -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 @@ -10,4 +10,7 @@ export interface VersionableDoc extends Doc { export interface VersionableClass extends Class { enabled: boolean + excludedProperties?: string[] + excludedRelations?: string[] // ${associationId}_${a|b} + excludeMixins?: Ref>[] } diff --git a/models/core/src/core.ts b/models/core/src/core.ts index 02e69d551b..f0feb13166 100644 --- a/models/core/src/core.ts +++ b/models/core/src/core.ts @@ -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>[] } diff --git a/models/process/src/functions.ts b/models/process/src/functions.ts index ba7c6f932f..11d982c12c 100644 --- a/models/process/src/functions.ts +++ b/models/process/src/functions.ts @@ -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, diff --git a/models/server-process/src/index.ts b/models/server-process/src/index.ts index dafae6227b..334528e3c0 100644 --- a/models/server-process/src/index.ts +++ b/models/server-process/src/index.ts @@ -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 }) diff --git a/packages/ui/src/components/DropdownLabelsIntl.svelte b/packages/ui/src/components/DropdownLabelsIntl.svelte index ceafeacb97..47d8fecde3 100644 --- a/packages/ui/src/components/DropdownLabelsIntl.svelte +++ b/packages/ui/src/components/DropdownLabelsIntl.svelte @@ -30,7 +30,8 @@ export let label: IntlString = ui.string.DropdownDefaultLabel export let params: Record = {} export let items: DropdownIntlItem[] - export let selected: DropdownIntlItem['id'] | undefined = undefined + export let multiselect: boolean = false + export let selected: DropdownIntlItem['id'] | Array | 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)?.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} > - + + diff --git a/packages/ui/src/components/DropdownLabelsPopupIntl.svelte b/packages/ui/src/components/DropdownLabelsPopupIntl.svelte index b9ed3eea96..013de21455 100644 --- a/packages/ui/src/components/DropdownLabelsPopupIntl.svelte +++ b/packages/ui/src/components/DropdownLabelsPopupIntl.svelte @@ -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 | undefined = undefined + export let multiselect: boolean = false export let params: Record = {} export let withSearch: boolean = false export let searchPlaceholder: IntlString = ui.string.Search const dispatch = createEventDispatcher() + + function isSelected ( + selected: DropdownIntlItem['id'] | Array | 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) + } }} >
@@ -110,7 +133,7 @@
- {#if item.id === selected}{/if} + {#if isSelected(selected, item)}{/if}
{/each} diff --git a/plugins/card-resources/src/components/CardVersionSelector.svelte b/plugins/card-resources/src/components/CardVersionSelector.svelte index 9c579f9eca..1e7d165c5c 100644 --- a/plugins/card-resources/src/components/CardVersionSelector.svelte +++ b/plugins/card-resources/src/components/CardVersionSelector.svelte @@ -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 { + const _id = await createNewVersion(value) + const loc = getCurrentLocation() + loc.path[2] = cardId + loc.path[3] = _id + navigate(loc) } diff --git a/plugins/card-resources/src/components/NewVersionPopup.svelte b/plugins/card-resources/src/components/NewVersionPopup.svelte deleted file mode 100644 index 36963c8ed2..0000000000 --- a/plugins/card-resources/src/components/NewVersionPopup.svelte +++ /dev/null @@ -1,106 +0,0 @@ - - - - -
-
-
- - diff --git a/plugins/card-resources/src/components/settings/GeneralSection.svelte b/plugins/card-resources/src/components/settings/GeneralSection.svelte index 9f4a04a05f..38551c8102 100644 --- a/plugins/card-resources/src/components/settings/GeneralSection.svelte +++ b/plugins/card-resources/src/components/settings/GeneralSection.svelte @@ -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 @@ -191,15 +201,16 @@ {/if} {#if !h.isMixin(masterTag._id)} -
- +
+
-
+
+ + + +
+
+
+
+
+ {#if relationsA.length > 0 || relationsB.length > 0} +
+
+
+
+ {#each relationsB as assoc} + {@const id = `${assoc._id}_b`} + {assoc.nameB} + { + if (e.detail === true) { + excludedRelations.delete(id) + } else { + excludedRelations.add(id) + } + excludedRelations = excludedRelations + }} + /> + {/each} + {#each relationsA as assoc} + {@const id = `${assoc._id}_a`} + {assoc.nameA} + { + if (e.detail === true) { + excludedRelations.delete(id) + } else { + excludedRelations.add(id) + } + excludedRelations = excludedRelations + }} + /> + {/each} +
+ {/if} +
+ + + diff --git a/plugins/card-resources/src/utils.ts b/plugins/card-resources/src/utils.ts index 66f6afc06a..3af45cbf1c 100644 --- a/plugins/card-resources/src/utils.ts +++ b/plugins/card-resources/src/utils.ts @@ -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 { if (tag !== undefined) { @@ -280,7 +282,7 @@ export async function createTypePermissions (masterTag: MasterTag | Tag): Promis async function cloneCard ( origin: Card, overrideProps: Record, - relationToCopy: Set | 'all', + versionableClass?: VersionableClass, copyIds: boolean = false ): Promise> { 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() 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> = {} @@ -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 { - 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 = {}): Promise): Promise> { +export async function createNewVersion (card: Card): Promise> { + 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 ) } diff --git a/plugins/process-resources/src/plugin.ts b/plugins/process-resources/src/plugin.ts index 55d58db6b6..de5dcd33bc 100644 --- a/plugins/process-resources/src/plugin.ts +++ b/plugins/process-resources/src/plugin.ts @@ -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, diff --git a/plugins/process/src/index.ts b/plugins/process/src/index.ts index dab168fdab..c1147efca7 100644 --- a/plugins/process/src/index.ts +++ b/plugins/process/src/index.ts @@ -378,6 +378,7 @@ export default plugin(processId, { EmptyValue: '' as Ref, EmptyArray: '' as Ref, CurrentDate: '' as Ref, + StringFromIdentifier: '' as Ref, StringFromNumber: '' as Ref, StringFromDate: '' as Ref, StringFromMarkup: '' as Ref, diff --git a/server-plugins/process-resources/src/index.ts b/server-plugins/process-resources/src/index.ts index 238d71812d..ad53f2be09 100644 --- a/server-plugins/process-resources/src/index.ts +++ b/server-plugins/process-resources/src/index.ts @@ -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 }, diff --git a/server-plugins/process-resources/src/transform.ts b/server-plugins/process-resources/src/transform.ts index 31806a2af8..66e412efce 100644 --- a/server-plugins/process-resources/src/transform.ts +++ b/server-plugins/process-resources/src/transform.ts @@ -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) diff --git a/server-plugins/process/src/index.ts b/server-plugins/process/src/index.ts index 83318be440..895ae7b1d9 100644 --- a/server-plugins/process/src/index.ts +++ b/server-plugins/process/src/index.ts @@ -126,6 +126,7 @@ export default plugin(serverProcessId, { NumberFromString: '' as Resource, DateFromString: '' as Resource, MarkupFromString: '' as Resource, + StringFromIdentifier: '' as Resource, YearFromDate: '' as Resource, MonthFromDate: '' as Resource, DayFromDate: '' as Resource,