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">
|
||||
.list-container {
|
||||
background-color: var(--board-card-bg-color);
|
||||
border-radius: 0.25rem;
|
||||
user-select: none;
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -54,6 +54,8 @@
|
||||
"Maintainer": "Maintainer",
|
||||
"Owner": "Владелец",
|
||||
"Role": "Роль",
|
||||
"FailedToSave": "Не удалось обновить пароль"
|
||||
"FailedToSave": "Не удалось обновить пароль",
|
||||
"ImportEnum": "Загрузить значения справочника",
|
||||
"ImportEnumCopy": "Загрузить значения справочника из буфера обмена"
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
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,
|
||||
Owner: '' 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