Fix TSK-294 Allow to Bulk operations over enums (#2241)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-07-19 09:06:15 +07:00 committed by GitHub
parent 09d8275da7
commit 0e0b8ab766
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 312 additions and 40 deletions

View File

@ -76,7 +76,6 @@
<style lang="scss">
.list-container {
background-color: var(--board-card-bg-color);
border-radius: 0.25rem;
user-select: none;

View File

@ -54,6 +54,8 @@
"Maintainer": "Maintainer",
"Owner": "Owner",
"Role": "Role",
"FailedToSave": "Failed to update password"
"FailedToSave": "Failed to update password",
"ImportEnum": "Import enum values",
"ImportEnumCopy": "Copy enum values from clipboard"
}
}

View File

@ -54,6 +54,8 @@
"Maintainer": "Maintainer",
"Owner": "Владелец",
"Role": "Роль",
"FailedToSave": "Не удалось обновить пароль"
"FailedToSave": "Не удалось обновить пароль",
"ImportEnum": "Загрузить значения справочника",
"ImportEnumCopy": "Загрузить значения справочника из буфера обмена"
}
}

View File

@ -14,10 +14,12 @@
-->
<script lang="ts">
import core, { Enum } from '@anticrm/core'
import presentation, { Card, getClient } from '@anticrm/presentation'
import { ActionIcon, EditBox, IconCheck, IconDelete } from '@anticrm/ui'
import presentation, { Card, getClient, MessageBox } from '@anticrm/presentation'
import { ActionIcon, EditBox, IconAdd, IconAttachment, IconDelete, Label, ListView, showPopup } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import setting from '../plugin'
import Copy from './icons/Copy.svelte'
import view from '@anticrm/view-resources/src/plugin'
export let value: Enum | undefined
let name: string = value?.name ?? ''
@ -25,6 +27,8 @@
const client = getClient()
const dispatch = createEventDispatcher()
let list: ListView
async function save (): Promise<void> {
if (value === undefined) {
await client.createDoc(core.class.Enum, core.space.Model, {
@ -41,7 +45,9 @@
}
function add () {
if (newValue.trim().length === 0) return
newValue = newValue.trim()
if (newValue.length === 0) return
if (values.includes(newValue)) return
values.push(newValue)
values = values
newValue = ''
@ -52,8 +58,110 @@
}
let newValue = ''
let inputFile: HTMLInputElement
function processText (text: string): void {
const newValues = text.split('\n').map((it) => it.trim())
for (const v of newValues) {
if (!values.includes(v)) {
values.push(v)
}
}
values = values
newValue = ''
}
async function processFile (file: File): Promise<void> {
const text = await file.text()
processText(text)
}
function fileSelected () {
const list = inputFile.files
if (list === null || list.length === 0) return
for (let index = 0; index < list.length; index++) {
const file = list.item(index)
if (file !== null) {
processFile(file)
}
}
inputFile.value = ''
}
function fileDrop (e: DragEvent) {
const list = e.dataTransfer?.files
if (list === undefined || list.length === 0) return
for (let index = 0; index < list.length; index++) {
const file = list.item(index)
if (file !== null) {
processFile(file)
}
}
}
function pasteAction (evt: ClipboardEvent): void {
const items = evt.clipboardData?.items ?? []
for (const index in items) {
const item = items[index]
if (item.kind === 'file') {
const blob = item.getAsFile()
if (blob !== null) {
processFile(blob)
}
}
}
}
async function handleClipboard (): Promise<void> {
const text = await navigator.clipboard.readText()
processText(text)
}
let dragover = false
let selection: number = 0
function onKeydown (key: KeyboardEvent): void {
if (key.code === 'ArrowUp') {
key.stopPropagation()
key.preventDefault()
list.select(selection - 1)
}
if (key.code === 'ArrowDown') {
key.stopPropagation()
key.preventDefault()
list.select(selection + 1)
}
}
$: filtered = newValue.length > 0 ? values.filter((it) => it.includes(newValue)) : values
function onDelete () {
showPopup(
MessageBox,
{
label: view.string.DeleteObject,
message: view.string.DeleteObjectConfirm,
params: { count: filtered.length }
},
undefined,
(result?: boolean) => {
if (result === true) {
values = values.filter((it) => !filtered.includes(it))
newValue = ''
}
}
)
}
</script>
<svelte:window on:paste={pasteAction} />
<input
bind:this={inputFile}
multiple
type="file"
name="file"
id="file"
style="display: none"
on:change={fileSelected}
/>
<Card
label={core.string.Enum}
okLabel={presentation.string.Save}
@ -63,27 +171,76 @@
dispatch('close')
}}
>
<div class="mb-2"><EditBox bind:value={name} placeholder={core.string.Name} maxWidth="13rem" /></div>
<div class="flex-between mb-4">
<EditBox
placeholder={setting.string.NewValue}
kind="large-style"
bind:value={newValue}
maxWidth="13rem"
/><ActionIcon icon={IconCheck} label={presentation.string.Add} action={add} size={'small'} />
</div>
<div class="flex-row">
{#each values as value}
<div class="flex-between mb-2">
{value}<ActionIcon
<div on:keydown={onKeydown}>
<div class="mb-2">
<EditBox bind:value={name} placeholder={core.string.Name} maxWidth="13rem" />
</div>
<div class="flex-between mb-4">
<EditBox placeholder={presentation.string.Search} kind="large-style" bind:value={newValue} maxWidth="13rem" />
<div class="flex gap-2">
<ActionIcon icon={IconAdd} label={presentation.string.Add} action={add} size={'small'} />
<ActionIcon
icon={Copy}
label={setting.string.ImportEnumCopy}
action={() => {
handleClipboard()
}}
size={'small'}
/>
<ActionIcon
icon={IconDelete}
label={setting.string.Delete}
action={() => {
remove(value)
onDelete()
}}
size={'small'}
/>
</div>
{/each}
</div>
<div class="scroll">
<div class="box flex max-h-125">
<ListView bind:this={list} count={filtered.length} bind:selection>
<svelte:fragment slot="item" let:item>
{@const value = filtered[item]}
<div class="flex-between flex-nowrap mb-2">
<span class="overflow-label">{value}</span>
<ActionIcon
icon={IconDelete}
label={setting.string.Delete}
action={() => {
remove(value)
}}
size={'small'}
/>
</div>
</svelte:fragment>
</ListView>
{#if filtered.length === 0}
<Label label={presentation.string.NoMatchesFound} />
{/if}
</div>
</div>
</div>
<svelte:fragment slot="footer">
<div
class="resume flex gap-2"
class:solid={dragover}
on:dragover|preventDefault={() => {
dragover = true
}}
on:dragleave={() => {
dragover = false
}}
on:drop|preventDefault|stopPropagation={fileDrop}
>
<ActionIcon
icon={IconAttachment}
label={setting.string.ImportEnum}
action={() => {
inputFile.click()
}}
size={'small'}
/>
</div>
</svelte:fragment>
</Card>

View File

@ -14,9 +14,11 @@
-->
<script lang="ts">
import { Enum } from '@anticrm/core'
import { ActionIcon, EditBox, IconCheck, IconDelete } from '@anticrm/ui'
import { getClient } from '@anticrm/presentation'
import presentation, { getClient, MessageBox } from '@anticrm/presentation'
import { ActionIcon, EditBox, IconAdd, IconAttachment, IconDelete, Label, ListView, showPopup } from '@anticrm/ui'
import setting from '../plugin'
import Copy from './icons/Copy.svelte'
import view from '@anticrm/view-resources/src/plugin'
export let value: Enum
@ -42,32 +44,130 @@
add()
}
}
$: filtered = newValue.length > 0 ? value.enumValues.filter((it) => it.includes(newValue)) : value.enumValues
async function handleClipboard (): Promise<void> {
const text = await navigator.clipboard.readText()
processText(text)
}
async function processText (text: string): Promise<void> {
const newValues = text.split('\n').map((it) => it.trim())
for (const v of newValues) {
if (!value.enumValues.includes(v)) {
await client.update(value, {
$push: { enumValues: v }
})
}
}
newValue = ''
}
let inputFile: HTMLInputElement
async function processFile (file: File): Promise<void> {
const text = await file.text()
processText(text)
}
function fileSelected () {
const list = inputFile.files
if (list === null || list.length === 0) return
for (let index = 0; index < list.length; index++) {
const file = list.item(index)
if (file !== null) {
processFile(file)
}
}
inputFile.value = ''
}
function onDelete () {
showPopup(
MessageBox,
{
label: view.string.DeleteObject,
message: view.string.DeleteObjectConfirm,
params: { count: filtered.length }
},
undefined,
(result?: boolean) => {
if (result === true) {
client.update(value, {
$pull: { enumValues: { $in: filtered } }
})
newValue = ''
}
}
)
}
</script>
<input
bind:this={inputFile}
multiple
type="file"
name="file"
id="file"
style="display: none"
on:change={fileSelected}
/>
<div class="flex-grow">
<div class="flex-between mb-4">
<EditBox
placeholder={setting.string.NewValue}
placeholder={presentation.string.Search}
on:keydown={handleKeydown}
kind="large-style"
bind:value={newValue}
maxWidth="18rem"
/>
<ActionIcon icon={IconCheck} label={setting.string.Add} action={add} size={'small'} />
<div class="flex gap-2">
<ActionIcon icon={IconAdd} label={setting.string.Add} action={add} size={'small'} />
<ActionIcon
icon={IconAttachment}
label={setting.string.ImportEnum}
action={() => {
inputFile.click()
}}
size={'small'}
/>
<ActionIcon
icon={Copy}
label={setting.string.ImportEnumCopy}
action={() => {
handleClipboard()
}}
size={'small'}
/>
<ActionIcon
icon={IconDelete}
label={setting.string.Delete}
action={() => {
onDelete()
}}
size={'small'}
/>
</div>
</div>
<div class="overflow-y-auto flex-row">
{#each value.enumValues as value}
<div class="flex-between mb-2">
{value}
<ActionIcon
icon={IconDelete}
label={setting.string.Delete}
action={() => {
remove(value)
}}
size={'small'}
/>
</div>
{/each}
<div class="scroll">
<div class="box">
<ListView count={filtered.length}>
<svelte:fragment slot="item" let:item>
{@const evalue = filtered[item]}
<div class="flex-between flex-nowrap mb-2">
<span class="overflow-label">{evalue}</span>
<ActionIcon
icon={IconDelete}
label={setting.string.Delete}
action={() => {
remove(evalue)
}}
size={'small'}
/>
</div>
</svelte:fragment>
</ListView>
{#if filtered.length === 0}
<Label label={presentation.string.NoMatchesFound} />
{/if}
</div>
</div>
</div>

View File

@ -0,0 +1,10 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path
d="M11.5,1H6.6C5.3,1,4.2,2,4.1,3.3C2.9,3.5,2,4.6,2,5.8v6.7C2,13.9,3.1,15,4.5,15h4.9c1.3,0,2.4-1,2.5-2.3 c1.2-0.2,2.1-1.2,2.1-2.5V3.5C14,2.1,12.9,1,11.5,1z M9.4,14H4.5C3.7,14,3,13.3,3,12.5V5.8c0-0.7,0.5-1.3,1.1-1.4v5.8 c0,1.4,1.1,2.5,2.5,2.5h4.3C10.8,13.4,10.2,14,9.4,14z M13,10.2c0,0.8-0.7,1.5-1.5,1.5H6.6c-0.8,0-1.5-0.7-1.5-1.5V3.5 C5.1,2.7,5.8,2,6.6,2h4.9C12.3,2,13,2.7,13,3.5V10.2z"
/>
</svg>

View File

@ -48,6 +48,8 @@ export default mergeIds(settingId, setting, {
Maintainer: '' as IntlString,
Owner: '' as IntlString,
Role: '' as IntlString,
FailedToSave: '' as IntlString
FailedToSave: '' as IntlString,
ImportEnum: '' as IntlString,
ImportEnumCopy: '' as IntlString
}
})