Multiselect enums (#2573)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-02-01 17:24:21 +06:00 committed by GitHub
parent f2d83afb18
commit 6b05833c8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 246 additions and 23 deletions

View File

@ -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]
})

View File

@ -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>

View File

@ -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)

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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)
}}
/>

View File

@ -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>

View File

@ -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