Add TagsDropdownEditor. Fix layout. (#1422)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2022-04-17 07:05:10 +03:00 committed by GitHub
parent f0cca26e9b
commit e5187e7d4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 261 additions and 97 deletions

View File

@ -88,6 +88,7 @@ table {
--modal-padding: 1.5rem; --modal-padding: 1.5rem;
} }
p { user-select: text; }
p:first-child { margin-block-start: 0; } // First and last padding p:first-child { margin-block-start: 0; } // First and last padding
p:last-child { margin-block-end: 0; } p:last-child { margin-block-end: 0; }
@ -109,8 +110,8 @@ p:last-child { margin-block-end: 0; }
.inline-flex { display: inline-flex; } .inline-flex { display: inline-flex; }
.flex-grow { flex-grow: 1; } .flex-grow { flex-grow: 1; }
.flex-no-shrink { flex-shrink: 0; } .flex-no-shrink { flex-shrink: 0; }
.flex-wrap { flex-wrap: wrap; } .flex-wrap { flex-wrap: wrap !important; }
.flex-nowrap { flex-wrap: nowrap; } .flex-nowrap { flex-wrap: nowrap !important; }
.flex-baseline { .flex-baseline {
display: inline-flex; display: inline-flex;
align-items: baseline; align-items: baseline;
@ -446,6 +447,7 @@ a.no-line {
.cursor-default { cursor: default; } .cursor-default { cursor: default; }
.pointer-events-none { pointer-events: none; } .pointer-events-none { pointer-events: none; }
.select-text { user-select: text; }
/* Text */ /* Text */

View File

@ -188,7 +188,7 @@
align-items: center; align-items: center;
padding: .75rem; padding: .75rem;
height: auto; height: auto;
border-top: 1px solid var(--button-bg-color); border-top: 1px solid var(--divider-color);
&.reverse { flex-direction: row-reverse; } &.reverse { flex-direction: row-reverse; }
&__error { &__error {

View File

@ -80,12 +80,6 @@
overflow: hidden; overflow: hidden;
&:checked + .checkSVG { &:checked + .checkSVG {
& .back {
fill: var(--theme-bg-check);
&.primary {
fill: var(--primary-bg-color);
}
}
& .check { & .check {
visibility: visible; visibility: visible;
fill: var(--theme-button-bg-enabled); fill: var(--theme-button-bg-enabled);

View File

@ -50,16 +50,16 @@
.icon { .icon {
margin-right: .25rem; margin-right: .25rem;
color: var(--theme-content-color); color: var(--content-color);
} }
&:hover .icon { color: var(--theme-caption-color); } &:hover .icon { color: var(--accent-color); }
&:active .icon { color: var(--theme-content-accent-color); } &:active .icon { color: var(--caption-color); }
} }
.disabled { .disabled {
cursor: not-allowed; cursor: not-allowed;
color: var(--theme-content-trans-color); color: var(--dark-color);
.icon { color: var(--theme-content-trans-color); } .icon { color: var(--dark-color); }
&:hover .icon { color: var(--theme-content-trans-color); } &:hover .icon { color: var(--dark-color); }
&:active .icon { color: var(--theme-content-trans-color); } &:active .icon { color: var(--dark-color); }
} }
</style> </style>

View File

@ -66,7 +66,7 @@
{/if} {/if}
<div class="flex-col h-full min-h-0" class:background-bg-accent={!transparent}> <div class="flex-col h-full min-h-0" class:background-bg-accent={!transparent}>
<Scroller> <Scroller>
<div class="p-10"> <div class="p-10 select-text">
{#if txes} {#if txes}
<Grid column={1} rowGap={1.5}> <Grid column={1} rowGap={1.5}>
{#each txes as tx (tx.tx._id)} {#each txes as tx (tx.tx._id)}

View File

@ -47,7 +47,7 @@
<style lang="scss"> <style lang="scss">
.content { .content {
padding: 1rem; padding: 1rem;
color: var(--theme-caption-color); color: var(--accent-color);
background: var(--theme-bg-accent-color); background: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-color); border: 1px solid var(--theme-bg-accent-color);
border-radius: .75rem; border-radius: .75rem;

View File

@ -399,10 +399,7 @@
dispatch('close') dispatch('close')
}} }}
> >
<div class="flex-row-center"> <div class="flex-between">
<div class="mr-4">
<EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:remove={removeAvatar} on:done={onAvatarDone} />
</div>
<div class="flex-col"> <div class="flex-col">
<EditBox placeholder={recruit.string.PersonFirstNamePlaceholder} bind:value={firstName} kind={'large-style'} maxWidth={'32rem'} focus /> <EditBox placeholder={recruit.string.PersonFirstNamePlaceholder} bind:value={firstName} kind={'large-style'} maxWidth={'32rem'} focus />
<EditBox placeholder={recruit.string.PersonLastNamePlaceholder} bind:value={lastName} kind={'large-style'} maxWidth={'32rem'} /> <EditBox placeholder={recruit.string.PersonLastNamePlaceholder} bind:value={lastName} kind={'large-style'} maxWidth={'32rem'} />
@ -411,15 +408,28 @@
</div> </div>
<EditBox placeholder={recruit.string.Location} bind:value={object.city} kind={'small-style'} maxWidth={'32rem'} /> <EditBox placeholder={recruit.string.Location} bind:value={object.city} kind={'small-style'} maxWidth={'32rem'} />
</div> </div>
<div class="ml-4">
<EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:remove={removeAvatar} on:done={onAvatarDone} />
</div>
</div> </div>
{#if channels.length > 0} {#if channels.length > 0}
<div class="ml-22"><ChannelsView value={channels} size={'small'} on:click /></div> <ChannelsView value={channels} size={'small'} on:click />
{/if} {/if}
<svelte:fragment slot="pool"> <svelte:fragment slot="pool">
<Button
icon={contact.icon.SocialEdit}
kind={'no-border'}
size={'small'}
on:click={(ev) =>
showPopup(contact.component.SocialEditor, { values: channels }, ev.target, (result) => {
if (result !== undefined) channels = result
})
}
/>
<YesNo label={recruit.string.Onsite} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.onsite} /> <YesNo label={recruit.string.Onsite} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.onsite} />
<YesNo label={recruit.string.Remote} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.remote} /> <YesNo label={recruit.string.Remote} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.remote} />
<Component <Component
is={tags.component.TagsEditor} is={tags.component.TagsDropdownEditor}
props={{ props={{
items: skills, items: skills,
key, key,
@ -437,30 +447,41 @@
/> />
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="footer"> <svelte:fragment slot="footer">
<Button <div
icon={contact.icon.SocialEdit} class="flex-center resume"
kind={'transparent'} class:solid={dragover || resume.uuid}
on:click={(ev) => on:dragover|preventDefault={() => {
showPopup(contact.component.SocialEditor, { values: channels }, ev.target, (result) => { dragover = true
if (result !== undefined) channels = result }}
}) on:dragleave={() => {
} dragover = false
/> }}
<Button on:drop|preventDefault|stopPropagation={drop}
icon={!resume.uuid && loading ? Spinner : IconAttachment} >
kind={'transparent'} {#if resume.uuid}
on:click={() => { inputFile.click() }} <Link
/> label={resume.name}
<input bind:this={inputFile} type="file" name="file" id="file" style="display: none" on:change={fileSelected} /> icon={FileIcon}
{#if resume.uuid} maxLenght={16}
<Button on:click={() => {
icon={FileIcon} showPopup(PDFViewer, { file: resume.uuid, name: resume.name }, 'right')
kind={'link-bordered'} }}
on:click={() => { />
showPopup(PDFViewer, { file: resume.uuid, name: resume.name }, 'right') {:else}
}} {#if loading}
><svelte:fragment slot="content">{resume.name}</svelte:fragment></Button> <Link label={'Uploading...'} icon={Spinner} disabled />
{/if} {:else}
<Link
label={'Add or drop resume'}
icon={FileUpload}
on:click={() => {
inputFile.click()
}}
/>
{/if}
<input bind:this={inputFile} type="file" name="file" id="file" style="display: none" on:change={fileSelected} />
{/if}
</div>
{#if matches.length > 0} {#if matches.length > 0}
<div class="flex-row-center error-color"> <div class="flex-row-center error-color">
<IconInfo size={'small'} /> <IconInfo size={'small'} />
@ -472,3 +493,14 @@
{/if} {/if}
</svelte:fragment> </svelte:fragment>
</Card> </Card>
<style lang="scss">
.resume {
padding: .5rem .75rem;
background: var(--accent-bg-color);
border: 1px dashed var(--divider-color);
border-radius: .5rem;
&.solid { border-style: solid; }
}
</style>

View File

@ -16,7 +16,7 @@
<script lang="ts"> <script lang="ts">
export let size: 'small' | 'medium' | 'large' export let size: 'small' | 'medium' | 'large'
const fill: string = 'var(--theme-caption-color)' const fill: string = 'currentColor'
</script> </script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> <svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">

View File

@ -41,7 +41,7 @@
> >
{name} {name}
{#if action} {#if action}
<div class="ml-2"> <div class="ml-1">
<ActionIcon <ActionIcon
icon={action} icon={action}
size={'small'} size={'small'}

View File

@ -57,11 +57,10 @@
</script> </script>
<TagsEditor <TagsEditor
{elements} bind:elements
{key} {key}
{items} bind:items
targetClass={_class} targetClass={_class}
on:open={(evt) => addRef(evt.detail)} on:open={(evt) => addRef(evt.detail)}
on:delete={(evt) => removeTag(evt.detail)} on:delete={(evt) => removeTag(evt.detail)}
countLabel={key.attr.label}
/> />

View File

@ -0,0 +1,93 @@
<!--
// 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 type { AttachedDoc, Class, Collection, Doc, Ref } from '@anticrm/core'
import { IntlString, translate } from '@anticrm/platform'
import { KeyedAttribute } from '@anticrm/presentation'
import { TagElement, TagReference } from '@anticrm/tags'
import type { ButtonKind, ButtonSize, TooltipAlignment } from '@anticrm/ui'
import { Button, showPopup, Tooltip } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import tags from '../plugin'
import TagsPopup from './TagsPopup.svelte'
export let items: TagReference[] = []
export let targetClass: Ref<Class<Doc>>
export let key: KeyedAttribute
export let elements: Map<Ref<TagElement>, TagElement>
export let countLabel: IntlString
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = undefined
export let labelDirection: TooltipAlignment | undefined = undefined
const dispatch = createEventDispatcher()
let keyLabel: string = ''
$: itemLabel = (key.attr.type as Collection<AttachedDoc>).itemLabel
$: translate(itemLabel ?? key.attr.label, {}).then((v) => {
keyLabel = v
})
async function addRef (tag: TagElement): Promise<void> {
dispatch('open', tag)
}
async function addTag (evt: Event): Promise<void> {
showPopup(
TagsPopup,
{
targetClass,
selected: items.map((it) => it.tag),
keyLabel
},
evt.target as HTMLElement,
() => { },
(result) => {
if (result != undefined) {
if (result.action === 'add') addRef(result.tag)
else if (result.action === 'remove') removeTag(items.filter(it => it.tag === result.tag._id)[0]._id)
}
}
)
}
async function removeTag (id: Ref<TagReference>): Promise<void> {
dispatch('delete', id)
}
</script>
<Tooltip label={key.attr.label} direction={labelDirection}>
<Button
icon={tags.icon.Tags}
label={items.length > 0 ? undefined : key.attr.label}
width={width ?? 'min-content'}
{kind} {size} {justify}
on:click={addTag}
>
<svelte:fragment slot="content">
{#if items.length > 0}
<div class="flex-row-center flex-nowrap">
{#await translate(countLabel, { count: items.length }) then text}
{text}
{/await}
</div>
{/if}
</svelte:fragment>
</Button>
</Tooltip>

View File

@ -18,23 +18,17 @@
import { KeyedAttribute } from '@anticrm/presentation' import { KeyedAttribute } from '@anticrm/presentation'
import { TagElement, TagReference } from '@anticrm/tags' import { TagElement, TagReference } from '@anticrm/tags'
import type { ButtonKind, ButtonSize, TooltipAlignment } from '@anticrm/ui' import type { ButtonKind, ButtonSize, TooltipAlignment } from '@anticrm/ui'
import { Button, showPopup, Tooltip } from '@anticrm/ui' import { ShowMore, Label, CircleButton, Button, showPopup, Tooltip, IconAdd, IconClose } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher, afterUpdate } from 'svelte'
import tags from '../plugin' import tags from '../plugin'
import TagsPopup from './TagsPopup.svelte' import TagsPopup from './TagsPopup.svelte'
import TagItem from './TagItem.svelte'
export let items: TagReference[] = [] export let items: TagReference[] = []
export let targetClass: Ref<Class<Doc>> export let targetClass: Ref<Class<Doc>>
export let key: KeyedAttribute export let key: KeyedAttribute
export let showTitle = true export let showTitle = true
export let elements: Map<Ref<TagElement>, TagElement> export let elements: Map<Ref<TagElement>, TagElement>
export let countLabel: IntlString
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = undefined
export let labelDirection: TooltipAlignment | undefined = undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -54,12 +48,18 @@
TagsPopup, TagsPopup,
{ {
targetClass, targetClass,
addRef,
removeTag,
selected: items.map((it) => it.tag), selected: items.map((it) => it.tag),
keyLabel keyLabel,
hideAdd: true
}, },
evt.target as HTMLElement evt.target as HTMLElement,
() => { },
(result) => {
if (result != undefined) {
if (result.action === 'add') addRef(result.tag)
else if (result.action === 'remove') removeTag(items.filter(it => it.tag === result.tag._id)[0]._id)
}
}
) )
} }
@ -68,22 +68,68 @@
} }
</script> </script>
<Tooltip label={key.attr.label} direction={labelDirection}> <div class="flex-row">
<Button {#if showTitle}
icon={tags.icon.Tags} <div class="flex-row-center">
label={items.length > 0 ? undefined : key.attr.label} <div class="title">
width={width ?? 'min-content'} <Label label={key.attr.label} />
{kind} {size} {justify} </div>
on:click={addTag} <div id='add-tag'>
> <Tooltip label={tags.string.AddTagTooltip} props={{ word: keyLabel }}>
<svelte:fragment slot="content"> <CircleButton icon={IconAdd} size={'small'} selected on:click={addTag} />
{#if items.length > 0} </Tooltip>
<div class="flex-row-center flex-nowrap"> </div>
{#await translate(countLabel, { count: items.length }) then text} </div>
{text} {/if}
{/await} <ShowMore ignore={!showTitle}>
</div> <div class:tags-container={showTitle} class:mt-4={showTitle}>
{/if} <div class="tag-items" class:tag-items-scroll={!showTitle}>
</svelte:fragment> {#if items.length === 0}
</Button> {#if keyLabel}
</Tooltip> <div class="flex flex-grow title-center">
<Label label={tags.string.NoItems} params={{ word: keyLabel }} />
</div>
{/if}
{/if}
{#each items as tag}
<TagItem
{tag}
element={elements.get(tag.tag)}
action={IconClose}
on:action={() => {
removeTag(tag._id)
}}
/>
{/each}
</div>
</div>
</ShowMore>
</div>
<style lang="scss">
.title {
margin-right: 0.75rem;
font-weight: 500;
font-size: 1.25rem;
color: var(--theme-caption-color);
}
.tags-container {
padding: 1rem;
color: var(--theme-caption-color);
background: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-color);
border-radius: 0.75rem;
}
.tag-items {
flex-grow: 1;
display: flex;
flex-wrap: wrap;
}
.tag-items-scroll {
overflow-y: scroll;
max-height: 10rem;
}
.title-center {
align-items: center;
}
</style>

View File

@ -17,7 +17,7 @@
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import { translate } from '@anticrm/platform' import { translate } from '@anticrm/platform'
import presentation, { createQuery, getClient } from '@anticrm/presentation' import presentation, { createQuery, getClient } from '@anticrm/presentation'
import { TagCategory, TagElement } from '@anticrm/tags' import { TagCategory, TagElement, TagReference } from '@anticrm/tags'
import { CheckBox, Button, Icon, IconAdd, IconClose, Label, showPopup, getPlatformColor } from '@anticrm/ui' import { CheckBox, Button, Icon, IconAdd, IconClose, Label, showPopup, getPlatformColor } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import tags from '../plugin' import tags from '../plugin'
@ -27,10 +27,9 @@
export let targetClass: Ref<Class<Doc>> export let targetClass: Ref<Class<Doc>>
export let placeholder: IntlString = presentation.string.Search export let placeholder: IntlString = presentation.string.Search
export let addRef: (tag: TagElement) => Promise<void>
export let removeTag: (tag: TagElement) => Promise<void>
export let selected: Ref<TagElement>[] = [] export let selected: Ref<TagElement>[] = []
export let keyLabel: string = '' export let keyLabel: string = ''
export let hideAdd: boolean = false
let search: string = '' let search: string = ''
let searchElement: HTMLInputElement let searchElement: HTMLInputElement
@ -60,10 +59,6 @@
async function createTagElement (): Promise<void> { async function createTagElement (): Promise<void> {
showPopup(CreateTagElement, { targetClass }, 'top') showPopup(CreateTagElement, { targetClass }, 'top')
} }
async function addTag (element: TagElement): Promise<void> {
await addRef(element)
selected = [...selected, element._id]
}
const isSelected = (element: TagElement): boolean => { const isSelected = (element: TagElement): boolean => {
if (selected.filter(p => p === element._id).length > 0) return true if (selected.filter(p => p === element._id).length > 0) return true
@ -72,14 +67,14 @@
const checkSelected = (element: TagElement): void => { const checkSelected = (element: TagElement): void => {
if (isSelected(element)) { if (isSelected(element)) {
selected = selected.filter(p => p !== element._id) selected = selected.filter(p => p !== element._id)
removeTag(element) dispatch('update', { action: 'remove', tag: element })
} else { } else {
selected.push(element._id) selected = [...selected, element._id]
addTag(element) dispatch('update', { action: 'add', tag: element })
} }
objects = objects objects = objects
categories = categories categories = categories
dispatch('update', selected) dispatch('update', { action: 'selected', selected: selected})
} }
const toggleGroup = (ev: MouseEvent): void => { const toggleGroup = (ev: MouseEvent): void => {
const el: HTMLElement = ev.currentTarget as HTMLElement const el: HTMLElement = ev.currentTarget as HTMLElement
@ -106,7 +101,7 @@
{#if search !== ''}<div class="icon"><Icon icon={IconClose} size={'inline'} /></div>{/if} {#if search !== ''}<div class="icon"><Icon icon={IconClose} size={'inline'} /></div>{/if}
</div> </div>
<Button kind={'transparent'} size={'small'} icon={show ? IconView : IconViewHide} on:click={() => show = !show} /> <Button kind={'transparent'} size={'small'} icon={show ? IconView : IconViewHide} on:click={() => show = !show} />
<Button kind={'transparent'} size={'small'} icon={IconAdd} on:click={createTagElement} /> {#if !hideAdd}<Button kind={'transparent'} size={'small'} icon={IconAdd} on:click={createTagElement} />{/if}
</div> </div>
</div> </div>
</div> </div>

View File

@ -21,6 +21,7 @@ import TagsPresenter from './components/TagsPresenter.svelte'
import TagsItemPresenter from './components/TagsItemPresenter.svelte' import TagsItemPresenter from './components/TagsItemPresenter.svelte'
import TagsView from './components/TagsView.svelte' import TagsView from './components/TagsView.svelte'
import TagsEditor from './components/TagsEditor.svelte' import TagsEditor from './components/TagsEditor.svelte'
import TagsDropdownEditor from './components/TagsDropdownEditor.svelte'
import CategoryPresenter from './components/CategoryPresenter.svelte' import CategoryPresenter from './components/CategoryPresenter.svelte'
import tags from './plugin' import tags from './plugin'
import TagsCategoryBar from './components/CategoryBar.svelte' import TagsCategoryBar from './components/CategoryBar.svelte'
@ -33,6 +34,7 @@ export default async (): Promise<Resources> => ({
TagsPresenter, TagsPresenter,
TagsView, TagsView,
TagsEditor, TagsEditor,
TagsDropdownEditor,
TagsItemPresenter, TagsItemPresenter,
CategoryPresenter, CategoryPresenter,
TagsCategoryBar TagsCategoryBar

View File

@ -79,6 +79,7 @@ const tagsPlugin = plugin(tagsId, {
component: { component: {
TagsView: '' as AnyComponent, TagsView: '' as AnyComponent,
TagsEditor: '' as AnyComponent, TagsEditor: '' as AnyComponent,
TagsDropdownEditor: '' as AnyComponent,
TagsCategoryBar: '' as AnyComponent TagsCategoryBar: '' as AnyComponent
}, },
category: { category: {