Cards fixes (#8003)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2025-02-14 01:41:25 +07:00
committed by GitHub
parent 617a64713b
commit 9d87ce7ede
96 changed files with 2358 additions and 436 deletions
@@ -23,20 +23,24 @@
DropdownIntlItem,
DropdownLabelsIntl,
EditBox,
Label
Label,
NestedDropdown
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import card from '@hcengineering/card'
import { createEventDispatcher } from 'svelte'
import setting from '../plugin'
export let association: Association | Data<Association>
export let kind: ButtonKind = 'regular'
export let size: ButtonSize = 'medium'
export let _classes: Ref<Class<Doc>>[] = [core.class.Doc]
export let exclude: Ref<Class<Doc>>[] = [card.class.Card]
const client = getClient()
const hierarchy = client.getHierarchy()
const descendants = hierarchy.getDescendants(core.class.Doc)
const descendants = new Set(_classes.map((p) => hierarchy.getDescendants(p)).reduce((a, b) => a.concat(b)))
const viewlets = new Set(
client
.getModel()
@@ -44,43 +48,69 @@
.map((p) => p.attachTo)
)
function filterClasses (descendants: Ref<Class<Doc>>[], viewlets: Set<Ref<Class<Doc>>>): DropdownIntlItem[] {
const result = []
function filterClasses (
descendants: Set<Ref<Class<Doc>>>,
viewlets: Set<Ref<Class<Doc>>>,
exclude: Ref<Class<Doc>>[]
): [DropdownIntlItem, DropdownIntlItem[]][] {
const ignore = new Set<Ref<Class<Doc>>>()
const added = new Set<Ref<Class<Doc>>>()
for (const _id of descendants) {
if (added.has(_id)) continue
const _class = hierarchy.getClass(_id)
if (_class.extends !== undefined && added.has(_class.extends)) {
added.add(_id)
result.push({ id: _id, label: _class.label })
const base = new Map<Ref<Class<Doc>>, Class<Doc>[]>()
for (const excluded of exclude) {
const desc = hierarchy.getDescendants(excluded)
for (const _id of desc) {
ignore.add(_id)
}
}
for (const _id of descendants) {
if (added.has(_id) || ignore.has(_id)) continue
const _class = hierarchy.getClass(_id)
if (_class.label === undefined) continue
if (viewlets.has(hierarchy.getBaseClass(_id))) {
added.add(_id)
result.push({ id: _id, label: _class.label })
const descendants = hierarchy.getDescendants(_id)
const toAdd: Class<Doc>[] = []
for (const desc of descendants) {
if (added.has(desc) || ignore.has(desc)) continue
const _class = hierarchy.getClass(desc)
if (_class.label === undefined) continue
added.add(desc)
toAdd.push(_class)
}
base.set(_id, toAdd)
}
}
const result: [DropdownIntlItem, DropdownIntlItem[]][] = []
for (const [key, value] of base) {
try {
const clazz = hierarchy.getClass(key)
result.push([{ id: key, label: clazz.label }, value.map((it) => ({ id: it._id, label: it.label }))])
} catch {}
}
return result
}
const classes = filterClasses(descendants, viewlets)
const classes = filterClasses(descendants, viewlets, exclude)
let classA = classes.find((p) => p.id === association.classA) ?? classes[0]
let classB = classes.find((p) => p.id === association.classB) ?? classes[0]
let classARef = classA?.id
let classBRef = classB?.id
let classARef: Ref<Class<Doc>> | undefined = undefined
let classBRef: Ref<Class<Doc>> | undefined = undefined
let nameA = association.nameA
let nameB = association.nameB
$: classA = isEmptyClass(classARef) ? undefined : hierarchy.getClass(classARef as Ref<Class<Doc>>)
$: classB = isEmptyClass(classBRef) ? undefined : hierarchy.getClass(classBRef as Ref<Class<Doc>>)
function isEmptyClass (ref: Ref<Class<Doc>> | undefined): boolean {
return ref === undefined || ref !== ''
}
$: editable = !isAssociation(association)
$: fill(association)
function fill (association: Association | Data<Association>): void {
classA = classes.find((p) => p.id === association.classA) ?? classes[0]
classB = classes.find((p) => p.id === association.classB) ?? classes[0]
classBRef = classB?.id
classARef = classA?.id
classBRef = !isEmptyClass(association.classB) ? association.classB : undefined
classARef = !isEmptyClass(association.classA) ? association.classA : undefined
nameA = association.nameA
nameB = association.nameB
}
@@ -102,8 +132,8 @@
})
} else {
await client.createDoc(core.class.Association, core.space.Model, {
classA: classARef as Ref<Class<Doc>>,
classB: classBRef as Ref<Class<Doc>>,
classA: classARef,
classB: classBRef,
type: mode,
nameA,
nameB
@@ -143,13 +173,11 @@
</div>
<div>
{#if editable}
<DropdownLabelsIntl
label={core.string.Class}
<NestedDropdown
items={classes}
width="8rem"
bind:selected={classARef}
{kind}
{size}
on:selected={(e) => {
classARef = e.detail
}}
/>
{:else if classA}
<Label label={classA.label} />
@@ -184,13 +212,11 @@
</div>
<div>
{#if editable}
<DropdownLabelsIntl
label={core.string.Class}
<NestedDropdown
items={classes}
width="8rem"
bind:selected={classBRef}
{kind}
{size}
on:selected={(e) => {
classBRef = e.detail
}}
/>
{:else if classB}
<Label label={classB.label} />
@@ -53,11 +53,6 @@
<button class="hulyTableAttr-content__row-dragMenu" on:click|stopPropagation={clickMore}>
<IconMoreV2 size={'small'} />
</button>
{#if attribute.isCustom}
<div class="hulyChip-item font-medium-12">
<Label label={settings.string.Custom} />
</div>
{/if}
{#if attribute.icon !== undefined}
<div class="hulyTableAttr-content__row-icon">
<Icon icon={attribute.icon} size={'small'} />
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import core, { AnyAttribute, Class, ClassifierKind, Doc, Ref, Space } from '@hcengineering/core'
import core, { AnyAttribute, Class, Doc, Ref, Space } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
@@ -22,28 +22,29 @@
ButtonIcon,
IconAdd,
IconEdit,
Label,
getEventPositionElement,
showPopup,
IconSettings,
Label,
ModernButton,
deviceWidths,
getEventPositionElement,
resizeObserver,
deviceWidths
showPopup
} from '@hcengineering/ui'
import { ObjectPresenter } from '@hcengineering/view-resources'
import { onDestroy } from 'svelte'
import settings from '../plugin'
import CreateAttribute from './CreateAttribute.svelte'
import { clearSettingsStore, settingsStore } from '../store'
import ClassAttributesList from './ClassAttributesList.svelte'
import CreateAttribute from './CreateAttribute.svelte'
import EditAttribute from './EditAttribute.svelte'
import EditClassLabel from './EditClassLabel.svelte'
import { settingsStore, clearSettingsStore } from '../store'
import TypesPopup from './typeEditors/TypesPopup.svelte'
import { onDestroy } from 'svelte'
export let _class: Ref<Class<Doc>>
export let ofClass: Ref<Class<Doc>> | undefined = undefined
export let showHierarchy: boolean = false
export let showTitle: boolean = !showHierarchy
export let showHeader: boolean = true
export let disabled: boolean = true
export let attributeMapper:
| {
@@ -101,16 +102,18 @@
showPopup(EditClassLabel, { clazz }, getEventPositionElement(evt))
}
const classUpdated = (_clazz: Ref<Class<Doc>>): void => {
const classUpdated = (_clazz: Ref<Class<Doc>>, to: Ref<Class<Doc>>): void => {
selected = undefined
classes = client
.getHierarchy()
const h = client.getHierarchy()
const toAncestors = new Set(h.getAncestors(to))
classes = h
.getAncestors(_class)
.map((it) => client.getHierarchy().getClass(it))
.filter((it) => {
return (
!it.hidden &&
it.label !== undefined &&
!toAncestors.has(it._id) &&
it._id !== core.class.Doc &&
it._id !== core.class.AttachedDoc &&
it._id !== _class
@@ -118,7 +121,7 @@
})
clazzHierarchy = client.getHierarchy().getClass(_class)
}
$: classUpdated(_class)
$: classUpdated(_class, ofClass ?? core.class.Doc)
settingsStore.subscribe((value) => {
if ((value.id === undefined && selected !== undefined) || (value.id !== undefined && value.id !== selected?._id)) {
@@ -163,14 +166,18 @@
}}
>
<div class="hulyTableAttr-header font-medium-12" class:withButton={showHierarchy}>
{#if showHierarchy}
<ModernButton icon={IconSettings} kind={'secondary'} size={'small'} {disabled} hasMenu>
<Label label={settings.string.ClassColon} />
<ObjectPresenter _class={clazzHierarchy._class} objectId={clazzHierarchy._id} value={clazzHierarchy} />
</ModernButton>
{#if showHeader}
{#if showHierarchy}
<ModernButton icon={IconSettings} kind={'secondary'} size={'small'} {disabled} hasMenu>
<Label label={settings.string.ClassColon} />
<ObjectPresenter _class={clazzHierarchy._class} objectId={clazzHierarchy._id} value={clazzHierarchy} />
</ModernButton>
{:else}
<IconSettings size={'small'} />
<span><Label label={settings.string.ClassProperties} /></span>
{/if}
{:else}
<IconSettings size={'small'} />
<span><Label label={settings.string.ClassProperties} /></span>
<div></div>
{/if}
<ButtonIcon
kind={'primary'}
@@ -138,7 +138,6 @@
/>
<ModernEditbox bind:value={name} label={core.string.Name} size={'large'} kind={'ghost'} autoFocus />
</div>
<ModernEditbox bind:value={name} label={core.string.Name} size={'large'} kind={'ghost'} autoFocus />
</div>
<div class="hulyModal-content__settingsSet">
<div class="hulyModal-content__settingsSet-line">
@@ -0,0 +1,175 @@
<!--
// Copyright © 2025 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 cardPlugin from '@hcengineering/card'
import core, { Class, Doc, Ref } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import { DropdownIntlItem, DropdownLabelsIntl, EditBox, Label } from '@hcengineering/ui'
import NestedDropdown from '@hcengineering/ui/src/components/NestedDropdown.svelte'
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import setting from '../plugin'
export let _classes: Ref<Class<Doc>>[] = [core.class.Doc]
export let exclude: Ref<Class<Doc>>[] = [cardPlugin.class.Card]
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
const descendants = new Set(_classes.map((p) => hierarchy.getDescendants(p)).reduce((a, b) => a.concat(b)))
const viewlets = new Set(
client
.getModel()
.findAllSync(view.class.Viewlet, { descriptor: view.viewlet.Table })
.map((p) => p.attachTo)
)
function filterClasses (
descendants: Set<Ref<Class<Doc>>>,
viewlets: Set<Ref<Class<Doc>>>,
exclude: Ref<Class<Doc>>[]
): [DropdownIntlItem, DropdownIntlItem[]][] {
const ignore = new Set<Ref<Class<Doc>>>()
const added = new Set<Ref<Class<Doc>>>()
const base = new Map<Ref<Class<Doc>>, Class<Doc>[]>()
for (const excluded of exclude) {
const desc = hierarchy.getDescendants(excluded)
for (const _id of desc) {
ignore.add(_id)
}
}
for (const _id of descendants) {
if (added.has(_id) || ignore.has(_id)) continue
const _class = hierarchy.getClass(_id)
if (_class.label === undefined) continue
if (viewlets.has(hierarchy.getBaseClass(_id))) {
added.add(_id)
const descendants = hierarchy.getDescendants(_id)
const toAdd: Class<Doc>[] = []
for (const desc of descendants) {
if (added.has(desc) || ignore.has(desc)) continue
const _class = hierarchy.getClass(desc)
if (_class.label === undefined) continue
added.add(desc)
toAdd.push(_class)
}
base.set(_id, toAdd)
}
}
const result: [DropdownIntlItem, DropdownIntlItem[]][] = []
for (const [key, value] of base) {
try {
const clazz = hierarchy.getClass(key)
result.push([{ id: key, label: clazz.label }, value.map((it) => ({ id: it._id, label: it.label }))])
} catch {}
}
return result
}
const classes = filterClasses(descendants, viewlets, exclude)
let classARef: Ref<Class<Doc>> | undefined = undefined
let classBRef: Ref<Class<Doc>> | undefined = undefined
let nameA: string = ''
let nameB: string = ''
async function save (): Promise<void> {
if (classBRef === undefined || classARef === undefined) {
return
}
await client.createDoc(core.class.Association, core.space.Model, {
classA: classARef,
classB: classBRef,
type: mode,
nameA,
nameB
})
dispatch('close')
}
const items: DropdownIntlItem[] = [
{
id: '1:1',
label: getEmbeddedLabel('1:1')
},
{
id: '1:N',
label: getEmbeddedLabel('1:N')
},
{
id: 'N:N',
label: getEmbeddedLabel('N:N')
}
]
let mode: '1:1' | '1:N' | 'N:N' = 'N:N' as '1:1' | '1:N' | 'N:N'
</script>
<Card
label={core.string.AddRelation}
okLabel={presentation.string.Create}
canSave={nameB.trim() !== '' && nameA.trim() !== '' && classARef !== undefined && classBRef !== undefined}
okAction={save}
on:close
>
<div class="flex-between flex-gap-4">
<div class="flex-col p-4 flex-gap-2">
<div class="flex-col-center">A</div>
<div>
<EditBox bind:value={nameA} placeholder={core.string.Name} kind={'default'} />
</div>
<div>
<NestedDropdown
items={classes}
on:selected={(e) => {
classARef = e.detail
}}
/>
</div>
</div>
<div class="flex-col p-4 flex-gap-2">
<span class="label">
<Label label={setting.string.Type} />
</span>
<DropdownLabelsIntl
selected={mode}
{items}
label={setting.string.Type}
on:selected={(res) => {
mode = res.detail
}}
/>
</div>
<div class="flex-col p-4 flex-gap-2">
<div class="flex-col-center">B</div>
<div>
<EditBox bind:value={nameB} placeholder={core.string.Name} kind={'default'} />
</div>
<div>
<NestedDropdown
items={classes}
on:selected={(e) => {
classBRef = e.detail
}}
/>
</div>
</div>
</div>
</Card>
@@ -29,10 +29,10 @@
showPopup,
themeStore
} from '@hcengineering/ui'
import { IconPicker } from '@hcengineering/view-resources'
import view from '@hcengineering/view-resources/src/plugin'
import setting from '../plugin'
import { clearSettingsStore } from '../store'
import { IconPicker, iconsLibrary } from '@hcengineering/view-resources'
export let attribute: AnyAttribute
export let exist: boolean
@@ -166,11 +166,6 @@
{/if}
</svelte:fragment>
<div class="hulyModal-content__titleGroup">
{#if attribute.isCustom}
<div class="hulyChip-item font-medium-12">
<Label label={setting.string.Custom} />
</div>
{/if}
<div class="flex items-center">
<ButtonIcon
icon={icon ?? setting.icon.Enums}
@@ -0,0 +1,112 @@
<!--
// Copyright © 2025 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, { Association } from '@hcengineering/core'
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import presentation, { getClient, MessageBox } from '@hcengineering/presentation'
import { Button, DropdownIntlItem, EditBox, IconDelete, Label, Modal, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import setting from '../plugin'
import { clearSettingsStore } from '../store'
export let association: Association
const client = getClient()
const hierarchy = client.getHierarchy()
const classA = hierarchy.getClass(association.classA)
const classB = hierarchy.getClass(association.classB)
let nameA = association.nameA
let nameB = association.nameB
async function save (): Promise<void> {
await client.diffUpdate(association, {
nameA,
nameB
})
clearSettingsStore()
}
const items: DropdownIntlItem[] = [
{
id: '1:1',
label: getEmbeddedLabel('1:1')
},
{
id: '1:N',
label: getEmbeddedLabel('1:N')
},
{
id: 'N:N',
label: getEmbeddedLabel('N:N')
}
]
const label = items.find((item) => item.id === association?.type)?.label ?? ('' as IntlString)
async function remove (): Promise<void> {
showPopup(MessageBox, {
label: view.string.DeleteObject,
message: view.string.DeleteObjectConfirm,
params: { count: 1 },
dangerous: true,
action: async () => {
await client.remove(association)
clearSettingsStore()
}
})
}
</script>
<Modal
label={core.string.Relation}
type={'type-aside'}
okLabel={presentation.string.Save}
okAction={save}
canSave={true}
onCancel={clearSettingsStore}
>
<svelte:fragment slot="actions">
<Button icon={IconDelete} kind={'dangerous'} on:click={remove} />
</svelte:fragment>
<div class="hulyModal-content__settingsSet">
<div class="flex flex-gap-4">
<div class="flex-col p-4 flex-gap-2">
<div class="flex-col-center">A</div>
<div>
<EditBox bind:value={nameA} placeholder={core.string.Name} kind={'default'} />
</div>
<div>
<Label label={classA.label} />
</div>
</div>
<div class="flex-col p-4 flex-gap-2">
<span class="label">
<Label label={setting.string.Type} />
</span>
<Label {label} />
</div>
<div class="flex-col p-4 flex-gap-2">
<div class="flex-col-center">B</div>
<div>
<EditBox bind:value={nameB} placeholder={core.string.Name} kind={'default'} />
</div>
<div>
<Label label={classB.label} />
</div>
</div>
</div>
</div>
</Modal>
@@ -1,22 +1,28 @@
<script lang="ts">
import core, { Association, Class, Data, Doc, Ref } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform'
import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
import {
Button,
Header,
Breadcrumb,
Separator,
Button,
defineSeparators,
twoPanelsSeparators,
Header,
IconDelete,
showPopup
Separator,
showPopup,
twoPanelsSeparators
} from '@hcengineering/ui'
import settings from '../plugin'
import view from '@hcengineering/view-resources/src/plugin'
import settings from '../plugin'
import card from '@hcengineering/card'
import AssociationEditor from './AssociationEditor.svelte'
export let _classes: Ref<Class<Doc>>[] = [core.class.Doc]
export let exclude: Ref<Class<Doc>>[] = [card.class.Card]
const query = createQuery()
const client = getClient()
const hierarchy = client.getHierarchy()
let selected: Association | Data<Association> | undefined
@@ -26,6 +32,37 @@
associations = res
})
$: filtered = filterAssociations(associations, _classes, exclude)
function filterAssociations (
associations: Association[],
_classes: Ref<Class<Doc>>[],
exclude: Ref<Class<Doc>>[]
): Association[] {
_classes = _classes ?? [core.class.Doc]
exclude = exclude ?? [card.class.Card]
const res: Association[] = []
const descendants = new Set(_classes.map((p) => hierarchy.getDescendants(p)).reduce((a, b) => a.concat(b)))
const excluded = new Set()
for (const _class of exclude) {
const desc = hierarchy.getDescendants(_class)
for (const _id of desc) {
excluded.add(_id)
}
}
for (const association of associations) {
if (
descendants.has(association.classA) &&
descendants.has(association.classB) &&
!excluded.has(association.classA) &&
!excluded.has(association.classB)
) {
res.push(association)
}
}
return res
}
function createRelation (): void {
selected = {
classA: '' as Ref<Class<Doc>>,
@@ -40,6 +77,19 @@
return (data as Association)?._id !== undefined
}
function getClassLabel (_class: Ref<Class<Doc>>): IntlString | undefined {
const _classLabel = client.getModel().findObject(_class)
return _classLabel?.label
}
async function getLabel (association: Association): Promise<string> {
const aLabel = getClassLabel(association.classA)
const bLabel = getClassLabel(association.classB)
const aClass = aLabel !== undefined ? await translate(aLabel, {}) : undefined
const bClass = bLabel !== undefined ? await translate(bLabel, {}) : undefined
return `${association.nameA} ${aClass !== undefined ? '(' + aClass + ')' : ''} - ${association.nameB} ${bClass !== undefined ? '(' + bClass + ')' : ''}`
}
defineSeparators('workspaceSettings', twoPanelsSeparators)
async function remove (val: Association | Data<Association> | undefined): Promise<void> {
@@ -82,7 +132,7 @@
</div>
<div class="flex-col overflow-y-auto">
{#each associations as association (association._id)}
{#each filtered as association (association._id)}
<button
class="list-item"
class:selected={selected === association}
@@ -90,7 +140,9 @@
selected = association
}}
>
<span class="font-regular-14 overflow-label">{association.nameA} - {association.nameB}</span>
{#await getLabel(association) then label}
<span class="font-regular-14 overflow-label">{label}</span>
{/await}
</button>
{/each}
</div>
@@ -98,6 +150,8 @@
<Separator name={'workspaceSettings'} index={0} color={'var(--theme-divider-color)'} />
{#if selected !== undefined}
<AssociationEditor
{exclude}
{_classes}
association={selected}
on:close={() => {
selected = undefined
@@ -103,5 +103,5 @@
{:else if kind === 'content' && category === undefined}
<div class="hulyComponent" />
{:else if category}
<Component is={category.component} props={{ kind: 'content' }} on:change />
<Component is={category.component} props={{ kind: 'content', ...category.props }} on:change />
{/if}