diff --git a/models/setting/src/index.ts b/models/setting/src/index.ts index 606f6b56d0..62356f0a82 100644 --- a/models/setting/src/index.ts +++ b/models/setting/src/index.ts @@ -302,6 +302,10 @@ export function createModel (builder: Builder): void { editor: setting.component.EnumTypeEditor }) + builder.mixin(core.class.ArrOf, core.class.Class, view.mixin.ObjectEditor, { + editor: setting.component.ArrayEditor + }) + builder.mixin(core.class.Class, core.class.Class, view.mixin.IgnoreActions, { actions: [view.action.Delete] }) diff --git a/models/setting/src/plugin.ts b/models/setting/src/plugin.ts index 44945b2373..987572a17b 100644 --- a/models/setting/src/plugin.ts +++ b/models/setting/src/plugin.ts @@ -40,7 +40,8 @@ export default mergeIds(settingId, setting, { RefEditor: '' as AnyComponent, EnumTypeEditor: '' as AnyComponent, Owners: '' as AnyComponent, - CreateMixin: '' as AnyComponent + CreateMixin: '' as AnyComponent, + ArrayEditor: '' as AnyComponent }, category: { Settings: '' as Ref<ActionCategory> diff --git a/models/view/src/index.ts b/models/view/src/index.ts index 5778485794..eec97f03ff 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -383,6 +383,9 @@ export function createModel (builder: Builder): void { builder.mixin(core.class.Class, core.class.Class, view.mixin.ObjectPresenter, { presenter: view.component.ClassPresenter }) + builder.mixin(core.class.EnumOf, core.class.Class, view.mixin.ArrayEditor, { + inlineEditor: view.component.EnumArrayEditor + }) classPresenter(builder, core.class.TypeRelatedDocument, view.component.ObjectPresenter) diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index afc7f13e10..ebcc6fc221 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -60,6 +60,7 @@ export default mergeIds(viewId, view, { ClassPresenter: '' as AnyComponent, ClassRefPresenter: '' as AnyComponent, EnumEditor: '' as AnyComponent, + EnumArrayEditor: '' as AnyComponent, HTMLEditor: '' as AnyComponent, MarkupEditor: '' as AnyComponent, MarkupEditorPopup: '' as AnyComponent, diff --git a/packages/ui/src/components/DropdownLabels.svelte b/packages/ui/src/components/DropdownLabels.svelte index 976e7a4dd2..aaffcf6055 100644 --- a/packages/ui/src/components/DropdownLabels.svelte +++ b/packages/ui/src/components/DropdownLabels.svelte @@ -27,7 +27,8 @@ export let label: IntlString export let placeholder: IntlString | undefined = ui.string.SearchDots export let items: DropdownTextItem[] - export let selected: DropdownTextItem['id'] | undefined = undefined + export let multiselect = false + export let selected: DropdownTextItem['id'] | DropdownTextItem['id'][] | undefined = multiselect ? [] : undefined export let kind: ButtonKind = 'no-border' export let size: ButtonSize = 'small' @@ -41,13 +42,10 @@ let container: HTMLElement let opened: boolean = false - let isDisabled = false - $: isDisabled = items.length === 0 - let selectedItem = items.find((x) => x.id === selected) - $: selectedItem = items.find((x) => x.id === selected) + $: selectedItem = multiselect ? items.filter((p) => selected?.includes(p.id)) : items.find((x) => x.id === selected) $: if (autoSelect && selected === undefined && items[0] !== undefined) { - selected = items[0].id + selected = multiselect ? [items[0].id] : items[0].id } const dispatch = createEventDispatcher() @@ -66,19 +64,42 @@ on:click={() => { if (!opened) { opened = true - showPopup(DropdownLabelsPopup, { placeholder, items, selected }, container, (result) => { - if (result) { - selected = result - dispatch('selected', result) + showPopup( + DropdownLabelsPopup, + { placeholder, items, multiselect, selected }, + container, + (result) => { + if (result) { + selected = result + dispatch('selected', result) + } + opened = false + mgr?.setFocusPos(focusIndex) + }, + (result) => { + if (result) { + selected = result + dispatch('selected', result) + } } - opened = false - mgr?.setFocusPos(focusIndex) - }) + ) } }} > <span slot="content" class="overflow-label disabled" class:content-color={selectedItem === undefined}> - {#if selectedItem}{selectedItem.label}{:else}<Label label={label ?? ui.string.NotSelected} />{/if} + {#if Array.isArray(selectedItem)} + {#if selectedItem.length > 0} + {#each selectedItem as seleceted, i} + <span class:ml-1={i !== 0}>{seleceted.label}</span> + {/each} + {:else} + <Label label={label ?? ui.string.NotSelected} /> + {/if} + {:else if selectedItem} + {selectedItem.label} + {:else} + <Label label={label ?? ui.string.NotSelected} /> + {/if} </span> </Button> </div> diff --git a/packages/ui/src/components/DropdownLabelsPopup.svelte b/packages/ui/src/components/DropdownLabelsPopup.svelte index 7a1cd2068a..24f63a2007 100644 --- a/packages/ui/src/components/DropdownLabelsPopup.svelte +++ b/packages/ui/src/components/DropdownLabelsPopup.svelte @@ -24,7 +24,8 @@ export let placeholder: IntlString = plugin.string.SearchDots export let items: DropdownTextItem[] - export let selected: DropdownTextItem['id'] | undefined = undefined + export let selected: DropdownTextItem['id'] | DropdownTextItem['id'][] | undefined = undefined + export let multiselect: boolean = false let search: string = '' let phTraslate: string = '' @@ -45,8 +46,18 @@ async function handleSelection (evt: Event | undefined, selection: number): Promise<void> { const item = objects[selection] - - 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) + } } function onKeydown (key: KeyboardEvent): void { @@ -71,6 +82,17 @@ dispatch('close') } } + + function isSelected ( + selected: DropdownTextItem['id'] | DropdownTextItem['id'][] | undefined, + item: DropdownTextItem + ): boolean { + if (Array.isArray(selected)) { + return selected.includes(item.id) + } else { + return item.id === selected + } + } </script> <div @@ -99,11 +121,22 @@ <button class="menu-item flex-between w-full" 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 lines-limit-2">{item.label}</div> - {#if item.id === selected} + {#if isSelected(selected, item)} <div class="check-right"><CheckBox checked primary /></div> {/if} </button> diff --git a/plugins/setting-resources/src/components/typeEditors/ArrayEditor.svelte b/plugins/setting-resources/src/components/typeEditors/ArrayEditor.svelte new file mode 100644 index 0000000000..c70f869223 --- /dev/null +++ b/plugins/setting-resources/src/components/typeEditors/ArrayEditor.svelte @@ -0,0 +1,89 @@ +<!-- +// Copyright © 2022 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, { ArrOf, Class, Doc, Ref, Type } from '@hcengineering/core' + import { ArrOf as createArrOf } from '@hcengineering/model' + import { getClient } from '@hcengineering/presentation' + import { AnyComponent, Component, DropdownLabelsIntl, Label } from '@hcengineering/ui' + import view from '@hcengineering/view' + import { createEventDispatcher } from 'svelte' + import setting from '../../plugin' + + export let type: ArrOf<Doc> | undefined + export let editable: boolean = true + + const dispatch = createEventDispatcher() + const client = getClient() + const hierarchy = client.getHierarchy() + + const descendants = hierarchy.getDescendants(core.class.Type) + + const types: Class<Type<Doc>>[] = descendants + .map((p) => hierarchy.getClass(p)) + .filter((p) => { + return ( + hierarchy.hasMixin(p, view.mixin.ArrayEditor) && + hierarchy.hasMixin(p, view.mixin.ObjectEditor) && + p.label !== undefined + ) + }) + + let refClass: Ref<Doc> | undefined = type !== undefined ? hierarchy.getClass(type.of._class)._id : undefined + + $: selected = types.find((p) => p._id === refClass) + + const handleChange = (e: any) => { + const type = e.detail?.type + const res = { type: createArrOf(type) } + dispatch('change', res) + } + + function getComponent (selected: Class<Type<Doc>>): AnyComponent { + const editor = hierarchy.as(selected, view.mixin.ObjectEditor) + return editor.editor + } +</script> + +<div class="flex-col"> + <div class="flex-row-center flex-grow"> + <Label label={setting.string.Type} /> + <div class="ml-4"> + {#if editable} + <DropdownLabelsIntl + label={core.string.Class} + items={types.map((p) => { + return { id: p._id, label: p.label } + })} + width="8rem" + bind:selected={refClass} + /> + {:else if selected} + <Label label={selected.label} /> + {/if} + </div> + </div> + {#if selected} + <div class="flex mt-4"> + <Component + is={getComponent(selected)} + props={{ + type: type?.of, + editable + }} + on:change={handleChange} + /> + </div> + {/if} +</div> diff --git a/plugins/setting-resources/src/index.ts b/plugins/setting-resources/src/index.ts index d1ab450c60..52b660542f 100644 --- a/plugins/setting-resources/src/index.ts +++ b/plugins/setting-resources/src/index.ts @@ -38,6 +38,7 @@ import DateTypeEditor from './components/typeEditors/DateTypeEditor.svelte' import EnumTypeEditor from './components/typeEditors/EnumTypeEditor.svelte' import HyperlinkTypeEditor from './components/typeEditors/HyperlinkTypeEditor.svelte' import NumberTypeEditor from './components/typeEditors/NumberTypeEditor.svelte' +import ArrayEditor from './components/typeEditors/ArrayEditor.svelte' import RefEditor from './components/typeEditors/RefEditor.svelte' import StringTypeEditor from './components/typeEditors/StringTypeEditor.svelte' import WorkspaceSettings from './components/WorkspaceSettings.svelte' @@ -90,6 +91,7 @@ export default async (): Promise<Resources> => ({ RefEditor, DateTypeEditor, EnumTypeEditor, + ArrayEditor, EditEnum, EnumSetting, Owners, diff --git a/plugins/view-resources/src/components/EnumArrayEditor.svelte b/plugins/view-resources/src/components/EnumArrayEditor.svelte new file mode 100644 index 0000000000..0b75afc9c5 --- /dev/null +++ b/plugins/view-resources/src/components/EnumArrayEditor.svelte @@ -0,0 +1,59 @@ +<!-- +// Copyright © 2022 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, { ArrOf, EnumOf } from '@hcengineering/core' + import type { IntlString } from '@hcengineering/platform' + import { createQuery } from '@hcengineering/presentation' + import { DropdownLabels, DropdownTextItem } from '@hcengineering/ui' + + export let label: IntlString + export let value: string[] = [] + export let type: ArrOf<string> + export let onChange: (value: string[]) => void + + let items: DropdownTextItem[] = [] + + const query = createQuery() + + query.query( + core.class.Enum, + { + _id: (type.of as EnumOf).of + }, + (res) => { + items = + res[0]?.enumValues?.map((p) => { + return { id: p, label: p } + }) ?? [] + }, + { limit: 1 } + ) +</script> + +<DropdownLabels + selected={value ?? []} + {items} + {label} + useFlexGrow={true} + justify={'left'} + size={'large'} + kind={'link'} + width={'100%'} + multiselect + autoSelect={false} + on:selected={(e) => { + onChange(e.detail) + }} +/> diff --git a/plugins/view-resources/src/components/StringPresenter.svelte b/plugins/view-resources/src/components/StringPresenter.svelte index 3dcc732f90..dbc7c002ba 100644 --- a/plugins/view-resources/src/components/StringPresenter.svelte +++ b/plugins/view-resources/src/components/StringPresenter.svelte @@ -14,7 +14,15 @@ // limitations under the License. --> <script lang="ts"> - export let value: string + export let value: string | string[] </script> -<span class="lines-limit-2 select-text">{value}</span> +<span class="lines-limit-2 select-text"> + {#if Array.isArray(value)} + {#each value as str, i} + <span class:ml-1={i !== 0}>{str}</span> + {/each} + {:else} + {value} + {/if} +</span> diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index a16de02b8b..40ca2d25e7 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -63,6 +63,7 @@ import UpDownNavigator from './components/UpDownNavigator.svelte' import ValueSelector from './components/ValueSelector.svelte' import ViewletSettingButton from './components/ViewletSettingButton.svelte' import SpaceRefPresenter from './components/SpaceRefPresenter.svelte' +import EnumArrayEditor from './components/EnumArrayEditor.svelte' import { afterResult, @@ -174,7 +175,8 @@ export default async (): Promise<Resources> => ({ ListView, GrowPresenter, IndexedDocumentPreview, - SpaceRefPresenter + SpaceRefPresenter, + EnumArrayEditor }, popup: { PositionElementAlignment