mirror of
https://github.com/hcengineering/platform.git
synced 2025-01-03 17:05:16 +03:00
Fix TSK-294 Allow to Bulk operations over enums (#2241)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
09d8275da7
commit
0e0b8ab766
@ -76,7 +76,6 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.list-container {
|
.list-container {
|
||||||
background-color: var(--board-card-bg-color);
|
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
@ -54,6 +54,8 @@
|
|||||||
"Maintainer": "Maintainer",
|
"Maintainer": "Maintainer",
|
||||||
"Owner": "Owner",
|
"Owner": "Owner",
|
||||||
"Role": "Role",
|
"Role": "Role",
|
||||||
"FailedToSave": "Failed to update password"
|
"FailedToSave": "Failed to update password",
|
||||||
|
"ImportEnum": "Import enum values",
|
||||||
|
"ImportEnumCopy": "Copy enum values from clipboard"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -54,6 +54,8 @@
|
|||||||
"Maintainer": "Maintainer",
|
"Maintainer": "Maintainer",
|
||||||
"Owner": "Владелец",
|
"Owner": "Владелец",
|
||||||
"Role": "Роль",
|
"Role": "Роль",
|
||||||
"FailedToSave": "Не удалось обновить пароль"
|
"FailedToSave": "Не удалось обновить пароль",
|
||||||
|
"ImportEnum": "Загрузить значения справочника",
|
||||||
|
"ImportEnumCopy": "Загрузить значения справочника из буфера обмена"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,10 +14,12 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import core, { Enum } from '@anticrm/core'
|
import core, { Enum } from '@anticrm/core'
|
||||||
import presentation, { Card, getClient } from '@anticrm/presentation'
|
import presentation, { Card, getClient, MessageBox } from '@anticrm/presentation'
|
||||||
import { ActionIcon, EditBox, IconCheck, IconDelete } from '@anticrm/ui'
|
import { ActionIcon, EditBox, IconAdd, IconAttachment, IconDelete, Label, ListView, showPopup } from '@anticrm/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import setting from '../plugin'
|
import setting from '../plugin'
|
||||||
|
import Copy from './icons/Copy.svelte'
|
||||||
|
import view from '@anticrm/view-resources/src/plugin'
|
||||||
|
|
||||||
export let value: Enum | undefined
|
export let value: Enum | undefined
|
||||||
let name: string = value?.name ?? ''
|
let name: string = value?.name ?? ''
|
||||||
@ -25,6 +27,8 @@
|
|||||||
const client = getClient()
|
const client = getClient()
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let list: ListView
|
||||||
|
|
||||||
async function save (): Promise<void> {
|
async function save (): Promise<void> {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
await client.createDoc(core.class.Enum, core.space.Model, {
|
await client.createDoc(core.class.Enum, core.space.Model, {
|
||||||
@ -41,7 +45,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function add () {
|
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.push(newValue)
|
||||||
values = values
|
values = values
|
||||||
newValue = ''
|
newValue = ''
|
||||||
@ -52,8 +58,110 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let newValue = ''
|
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>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:paste={pasteAction} />
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:this={inputFile}
|
||||||
|
multiple
|
||||||
|
type="file"
|
||||||
|
name="file"
|
||||||
|
id="file"
|
||||||
|
style="display: none"
|
||||||
|
on:change={fileSelected}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
label={core.string.Enum}
|
label={core.string.Enum}
|
||||||
okLabel={presentation.string.Save}
|
okLabel={presentation.string.Save}
|
||||||
@ -63,27 +171,76 @@
|
|||||||
dispatch('close')
|
dispatch('close')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="mb-2"><EditBox bind:value={name} placeholder={core.string.Name} maxWidth="13rem" /></div>
|
<div on:keydown={onKeydown}>
|
||||||
<div class="flex-between mb-4">
|
<div class="mb-2">
|
||||||
<EditBox
|
<EditBox bind:value={name} placeholder={core.string.Name} maxWidth="13rem" />
|
||||||
placeholder={setting.string.NewValue}
|
</div>
|
||||||
kind="large-style"
|
<div class="flex-between mb-4">
|
||||||
bind:value={newValue}
|
<EditBox placeholder={presentation.string.Search} kind="large-style" bind:value={newValue} maxWidth="13rem" />
|
||||||
maxWidth="13rem"
|
<div class="flex gap-2">
|
||||||
/><ActionIcon icon={IconCheck} label={presentation.string.Add} action={add} size={'small'} />
|
<ActionIcon icon={IconAdd} label={presentation.string.Add} action={add} size={'small'} />
|
||||||
</div>
|
<ActionIcon
|
||||||
<div class="flex-row">
|
icon={Copy}
|
||||||
{#each values as value}
|
label={setting.string.ImportEnumCopy}
|
||||||
<div class="flex-between mb-2">
|
action={() => {
|
||||||
{value}<ActionIcon
|
handleClipboard()
|
||||||
|
}}
|
||||||
|
size={'small'}
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
icon={IconDelete}
|
icon={IconDelete}
|
||||||
label={setting.string.Delete}
|
label={setting.string.Delete}
|
||||||
action={() => {
|
action={() => {
|
||||||
remove(value)
|
onDelete()
|
||||||
}}
|
}}
|
||||||
size={'small'}
|
size={'small'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</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>
|
</Card>
|
||||||
|
@ -14,9 +14,11 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Enum } from '@anticrm/core'
|
import { Enum } from '@anticrm/core'
|
||||||
import { ActionIcon, EditBox, IconCheck, IconDelete } from '@anticrm/ui'
|
import presentation, { getClient, MessageBox } from '@anticrm/presentation'
|
||||||
import { getClient } from '@anticrm/presentation'
|
import { ActionIcon, EditBox, IconAdd, IconAttachment, IconDelete, Label, ListView, showPopup } from '@anticrm/ui'
|
||||||
import setting from '../plugin'
|
import setting from '../plugin'
|
||||||
|
import Copy from './icons/Copy.svelte'
|
||||||
|
import view from '@anticrm/view-resources/src/plugin'
|
||||||
|
|
||||||
export let value: Enum
|
export let value: Enum
|
||||||
|
|
||||||
@ -42,32 +44,130 @@
|
|||||||
add()
|
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>
|
</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-grow">
|
||||||
<div class="flex-between mb-4">
|
<div class="flex-between mb-4">
|
||||||
<EditBox
|
<EditBox
|
||||||
placeholder={setting.string.NewValue}
|
placeholder={presentation.string.Search}
|
||||||
on:keydown={handleKeydown}
|
on:keydown={handleKeydown}
|
||||||
kind="large-style"
|
kind="large-style"
|
||||||
bind:value={newValue}
|
bind:value={newValue}
|
||||||
maxWidth="18rem"
|
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>
|
||||||
<div class="overflow-y-auto flex-row">
|
<div class="scroll">
|
||||||
{#each value.enumValues as value}
|
<div class="box">
|
||||||
<div class="flex-between mb-2">
|
<ListView count={filtered.length}>
|
||||||
{value}
|
<svelte:fragment slot="item" let:item>
|
||||||
<ActionIcon
|
{@const evalue = filtered[item]}
|
||||||
icon={IconDelete}
|
<div class="flex-between flex-nowrap mb-2">
|
||||||
label={setting.string.Delete}
|
<span class="overflow-label">{evalue}</span>
|
||||||
action={() => {
|
<ActionIcon
|
||||||
remove(value)
|
icon={IconDelete}
|
||||||
}}
|
label={setting.string.Delete}
|
||||||
size={'small'}
|
action={() => {
|
||||||
/>
|
remove(evalue)
|
||||||
</div>
|
}}
|
||||||
{/each}
|
size={'small'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</svelte:fragment>
|
||||||
|
</ListView>
|
||||||
|
{#if filtered.length === 0}
|
||||||
|
<Label label={presentation.string.NoMatchesFound} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
10
plugins/setting-resources/src/components/icons/Copy.svelte
Normal file
10
plugins/setting-resources/src/components/icons/Copy.svelte
Normal 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>
|
@ -48,6 +48,8 @@ export default mergeIds(settingId, setting, {
|
|||||||
Maintainer: '' as IntlString,
|
Maintainer: '' as IntlString,
|
||||||
Owner: '' as IntlString,
|
Owner: '' as IntlString,
|
||||||
Role: '' as IntlString,
|
Role: '' as IntlString,
|
||||||
FailedToSave: '' as IntlString
|
FailedToSave: '' as IntlString,
|
||||||
|
ImportEnum: '' as IntlString,
|
||||||
|
ImportEnumCopy: '' as IntlString
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user