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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -399,10 +399,7 @@
dispatch('close')
}}
>
<div class="flex-row-center">
<div class="mr-4">
<EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:remove={removeAvatar} on:done={onAvatarDone} />
</div>
<div class="flex-between">
<div class="flex-col">
<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'} />
@ -411,15 +408,28 @@
</div>
<EditBox placeholder={recruit.string.Location} bind:value={object.city} kind={'small-style'} maxWidth={'32rem'} />
</div>
<div class="ml-4">
<EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:remove={removeAvatar} on:done={onAvatarDone} />
</div>
</div>
{#if channels.length > 0}
<div class="ml-22"><ChannelsView value={channels} size={'small'} on:click /></div>
<ChannelsView value={channels} size={'small'} on:click />
{/if}
<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.Remote} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.remote} />
<Component
is={tags.component.TagsEditor}
is={tags.component.TagsDropdownEditor}
props={{
items: skills,
key,
@ -437,30 +447,41 @@
/>
</svelte:fragment>
<svelte:fragment slot="footer">
<Button
icon={contact.icon.SocialEdit}
kind={'transparent'}
on:click={(ev) =>
showPopup(contact.component.SocialEditor, { values: channels }, ev.target, (result) => {
if (result !== undefined) channels = result
})
}
/>
<Button
icon={!resume.uuid && loading ? Spinner : IconAttachment}
kind={'transparent'}
on:click={() => { inputFile.click() }}
/>
<input bind:this={inputFile} type="file" name="file" id="file" style="display: none" on:change={fileSelected} />
{#if resume.uuid}
<Button
icon={FileIcon}
kind={'link-bordered'}
on:click={() => {
showPopup(PDFViewer, { file: resume.uuid, name: resume.name }, 'right')
}}
><svelte:fragment slot="content">{resume.name}</svelte:fragment></Button>
{/if}
<div
class="flex-center resume"
class:solid={dragover || resume.uuid}
on:dragover|preventDefault={() => {
dragover = true
}}
on:dragleave={() => {
dragover = false
}}
on:drop|preventDefault|stopPropagation={drop}
>
{#if resume.uuid}
<Link
label={resume.name}
icon={FileIcon}
maxLenght={16}
on:click={() => {
showPopup(PDFViewer, { file: resume.uuid, name: resume.name }, 'right')
}}
/>
{:else}
{#if loading}
<Link label={'Uploading...'} icon={Spinner} disabled />
{: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}
<div class="flex-row-center error-color">
<IconInfo size={'small'} />
@ -472,3 +493,14 @@
{/if}
</svelte:fragment>
</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">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'var(--theme-caption-color)'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">

View File

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

View File

@ -57,11 +57,10 @@
</script>
<TagsEditor
{elements}
bind:elements
{key}
{items}
bind:items
targetClass={_class}
on:open={(evt) => addRef(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 { 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 { ShowMore, Label, CircleButton, Button, showPopup, Tooltip, IconAdd, IconClose } from '@anticrm/ui'
import { createEventDispatcher, afterUpdate } from 'svelte'
import tags from '../plugin'
import TagsPopup from './TagsPopup.svelte'
import TagItem from './TagItem.svelte'
export let items: TagReference[] = []
export let targetClass: Ref<Class<Doc>>
export let key: KeyedAttribute
export let showTitle = true
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()
@ -54,12 +48,18 @@
TagsPopup,
{
targetClass,
addRef,
removeTag,
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>
<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>
<div class="flex-row">
{#if showTitle}
<div class="flex-row-center">
<div class="title">
<Label label={key.attr.label} />
</div>
<div id='add-tag'>
<Tooltip label={tags.string.AddTagTooltip} props={{ word: keyLabel }}>
<CircleButton icon={IconAdd} size={'small'} selected on:click={addTag} />
</Tooltip>
</div>
</div>
{/if}
<ShowMore ignore={!showTitle}>
<div class:tags-container={showTitle} class:mt-4={showTitle}>
<div class="tag-items" class:tag-items-scroll={!showTitle}>
{#if items.length === 0}
{#if keyLabel}
<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 { translate } from '@anticrm/platform'
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 { createEventDispatcher } from 'svelte'
import tags from '../plugin'
@ -27,10 +27,9 @@
export let targetClass: Ref<Class<Doc>>
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 keyLabel: string = ''
export let hideAdd: boolean = false
let search: string = ''
let searchElement: HTMLInputElement
@ -60,10 +59,6 @@
async function createTagElement (): Promise<void> {
showPopup(CreateTagElement, { targetClass }, 'top')
}
async function addTag (element: TagElement): Promise<void> {
await addRef(element)
selected = [...selected, element._id]
}
const isSelected = (element: TagElement): boolean => {
if (selected.filter(p => p === element._id).length > 0) return true
@ -72,14 +67,14 @@
const checkSelected = (element: TagElement): void => {
if (isSelected(element)) {
selected = selected.filter(p => p !== element._id)
removeTag(element)
dispatch('update', { action: 'remove', tag: element })
} else {
selected.push(element._id)
addTag(element)
selected = [...selected, element._id]
dispatch('update', { action: 'add', tag: element })
}
objects = objects
categories = categories
dispatch('update', selected)
dispatch('update', { action: 'selected', selected: selected})
}
const toggleGroup = (ev: MouseEvent): void => {
const el: HTMLElement = ev.currentTarget as HTMLElement
@ -106,7 +101,7 @@
{#if search !== ''}<div class="icon"><Icon icon={IconClose} size={'inline'} /></div>{/if}
</div>
<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>

View File

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

View File

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