Update TagEditor layout (#1420)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2022-04-16 06:06:32 +03:00 committed by GitHub
parent 5dee8d9072
commit f0cca26e9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 391 additions and 241 deletions

View File

@ -15,6 +15,6 @@
"Search": "Search...", "Search": "Search...",
"Unassigned": "Unassigned", "Unassigned": "Unassigned",
"CreateMore": "Create more", "CreateMore": "Create more",
"NumberMembers": "{lenght, plural, =0 {no members} =1 {1 member} other {# members}}" "NumberMembers": "{count, plural, =0 {no members} =1 {1 member} other {# members}}"
} }
} }

View File

@ -15,6 +15,6 @@
"Search": "Поиск...", "Search": "Поиск...",
"Unassigned": "Не назначен", "Unassigned": "Не назначен",
"CreateMore": "Создать еще", "CreateMore": "Создать еще",
"NumberMembers": "{lenght, plural, =0 {нет участников} =1 {1 участик} other {# участника}}" "NumberMembers": "{count, plural, =0 {нет участников} =1 {1 участик} other {# участника}}"
} }
} }

View File

@ -79,7 +79,7 @@
{#if persons.length > 0} {#if persons.length > 0}
<div class="flex-row-center flex-nowrap"> <div class="flex-row-center flex-nowrap">
<CombineAvatars {_class} bind:items size={'inline'} /> <CombineAvatars {_class} bind:items size={'inline'} />
{#await translate(presentation.string.NumberMembers, { lenght: persons.length }) then text} {#await translate(presentation.string.NumberMembers, { count: persons.length }) then text}
<span class="ml-1-5">{text}</span> <span class="ml-1-5">{text}</span>
{/await} {/await}
</div> </div>

View File

@ -296,6 +296,7 @@ p:last-child { margin-block-end: 0; }
.ml-12 { margin-left: 3rem; } .ml-12 { margin-left: 3rem; }
.ml-22 { margin-left: 5.5rem; } .ml-22 { margin-left: 5.5rem; }
.mr-1 { margin-right: .25rem; } .mr-1 { margin-right: .25rem; }
.mr-1-5 { margin-right: .375rem; }
.mr-2 { margin-right: .5rem; } .mr-2 { margin-right: .5rem; }
.mr-3 { margin-right: .75rem; } .mr-3 { margin-right: .75rem; }
.mr-4 { margin-right: 1rem; } .mr-4 { margin-right: 1rem; }
@ -461,6 +462,7 @@ a.no-line {
color: var(--theme-content-trans-color); color: var(--theme-content-trans-color);
user-select: none; user-select: none;
} }
.text-xs { font-size: .625rem; }
.text-sm { font-size: .75rem; } .text-sm { font-size: .75rem; }
.text-md { font-size: .8125rem; } .text-md { font-size: .8125rem; }
.text-lg { font-size: 1.125rem; } .text-lg { font-size: 1.125rem; }

View File

@ -18,18 +18,20 @@
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: min-content; width: fit-content;
min-width: 10rem; min-width: 10rem;
max-width: 45rem; max-width: 15rem;
max-height: 20rem; max-height: 22rem;
background: var(--popup-bg-color); background: var(--popup-bg-color);
border-radius: .5rem; border-radius: .5rem;
box-shadow: var(--popup-shadow); box-shadow: var(--popup-shadow);
will-change: transform; will-change: transform;
&.maxHeight { height: 22rem; }
.header { .header {
border-bottom: 1px solid var(--popup-divider); border-bottom: 1px solid var(--popup-divider);
&.no-border { border-bottom-color: transparent; }
input { input {
margin: 0; margin: 0;
padding: .625rem .75rem; padding: .625rem .75rem;
@ -59,25 +61,33 @@
.menu-item { .menu-item {
flex-shrink: 0; flex-shrink: 0;
justify-content: start; justify-content: start;
padding: 0 .75rem; padding: .25rem .75rem;
height: 2rem; min-height: 2rem;
text-align: left; text-align: left;
color: var(--caption-color); color: var(--caption-color);
cursor: pointer; cursor: pointer;
&.high { height: 3rem; } &.high { height: 3rem; }
.icon {
.icon, .color, .tag {
flex-shrink: 0;
margin-right: .75rem; margin-right: .75rem;
}
.icon {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
} }
.color { .color {
margin-right: .75rem;
width: .875rem; width: .875rem;
height: .875rem; height: .875rem;
border: 1px solid rgba(0, 0, 0, .1); border: 1px solid rgba(0, 0, 0, .1);
border-radius: .25rem; border-radius: .25rem;
} }
.tag {
width: .5rem;
height: .5rem;
border-radius: 50%;
}
.label { .label {
flex-grow: 1; flex-grow: 1;
min-width: 0; min-width: 0;
@ -89,11 +99,50 @@
display: flex; display: flex;
align-items: center; align-items: center;
margin-right: .75rem; margin-right: .75rem;
height: 2rem;
} }
.check-right { margin: 0 0 0 2rem; } .check-right { margin: 0 0 0 2rem; }
&:hover { background-color: var(--popup-bg-hover); } &:hover { background-color: var(--popup-bg-hover); }
} }
.sticky-wrapper {
display: flex;
flex-direction: column;
&:not(:first-child) { margin-top: 1px; }
}
.menu-group {
overflow: hidden;
display: flex;
flex-direction: column;
height: 0;
transition: height .5s ease;
&__header {
position: sticky;
top: 0;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: .125rem .25rem;
min-height: 1.5rem;
font-weight: 500;
font-size: .75rem;
text-align: left;
color: var(--accent-color);
background-color: var(--button-bg-color);
cursor: pointer;
.icon {
width: .25rem;
transform-origin: 40% 50%;
transform: rotate(0deg);
transition: transform .15s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
&.show .icon { transform: rotate(90deg); }
&:hover { color: var(--caption-color); }
&.show + .menu-group { height: auto; }
}
}
} }
.antiPopup { .antiPopup {

View File

@ -17,7 +17,7 @@
import { AnySvelteComponent } from '../types' import { AnySvelteComponent } from '../types'
export let icon: Asset | AnySvelteComponent export let icon: Asset | AnySvelteComponent
export let size: 'x-small' | 'small' | 'medium' | 'large' | 'full' export let size: 'inline' | 'x-small' | 'small' | 'medium' | 'large' | 'full'
export let fill = 'currentColor' export let fill = 'currentColor'
export let filled: boolean = false export let filled: boolean = false

View File

@ -31,6 +31,7 @@
let modalHTML: HTMLElement let modalHTML: HTMLElement
let componentInstance: any let componentInstance: any
let show: boolean = false let show: boolean = false
let height: number
function _update (result: any): void { function _update (result: any): void {
if (onUpdate !== undefined) onUpdate(result) if (onUpdate !== undefined) onUpdate(result)
@ -61,11 +62,12 @@
} }
} }
afterUpdate(() => fitPopup()) afterUpdate(() => fitPopup())
$: if (height) fitPopup()
</script> </script>
<svelte:window on:resize={fitPopup} on:keydown={handleKeydown} /> <svelte:window on:resize={fitPopup} on:keydown={handleKeydown} />
<div class="popup" bind:this={modalHTML} style={`z-index: ${zIndex + 1};`}> <div class="popup" bind:this={modalHTML} bind:clientHeight={height} style={`z-index: ${zIndex + 1};`}>
<svelte:component bind:this={componentInstance} this={is} {...props} on:update={(ev) => _update(ev.detail)} on:close={(ev) => _close(ev.detail)} /> <svelte:component bind:this={componentInstance} this={is} {...props} on:update={(ev) => _update(ev.detail)} on:close={(ev) => _close(ev.detail)} />
</div> </div>
<div class="modal-overlay" class:antiOverlay={show} style={`z-index: ${zIndex};`} on:click={() => escapeClose()} /> <div class="modal-overlay" class:antiOverlay={show} style={`z-index: ${zIndex};`} on:click={() => escapeClose()} />

View File

@ -86,7 +86,8 @@
"Participants": "Participants", "Participants": "Participants",
"NoParticipants": "No participants added", "NoParticipants": "No participants added",
"PersonsLabel": "{name}", "PersonsLabel": "{name}",
"AddDescription": "Add description" "AddDescription": "Add description",
"NumberSkills": "{count, plural, =0 {no skills} =1 {1 skill} other {# skills}}"
}, },
"status": { "status": {
"CandidateRequired": "Please select candidate", "CandidateRequired": "Please select candidate",

View File

@ -87,7 +87,8 @@
"Participants": "Участники", "Participants": "Участники",
"NoParticipants": "Участники не добавлены", "NoParticipants": "Участники не добавлены",
"PersonsLabel": "{name}", "PersonsLabel": "{name}",
"AddDescription": "Add description" "AddDescription": "Add description",
"NumberSkills": "{count, plural, =0 {нет навыков} =1 {1 навык} =2 {2 навыка} other {# навыков}}"
}, },
"status": { "status": {
"CandidateRequired": "Пожалуйста выберите кандидата", "CandidateRequired": "Пожалуйста выберите кандидата",

View File

@ -231,22 +231,22 @@
</Card> </Card>
<style lang="scss"> <style lang="scss">
.card { // .card {
align-self: stretch; // align-self: stretch;
width: calc(50% - 3rem); // width: calc(50% - 3rem);
min-height: 16rem; // min-height: 16rem;
&.empty { // &.empty {
display: flex; // display: flex;
justify-content: center; // justify-content: center;
align-items: center; // align-items: center;
font-size: .75rem; // font-size: .75rem;
color: var(--dark-color); // color: var(--dark-color);
border: 1px solid var(--divider-color); // border: 1px solid var(--divider-color);
border-radius: .25rem; // border-radius: .25rem;
} // }
} // }
.arrows { width: 4rem; } // .arrows { width: 4rem; }
.color { .color {
margin-right: .375rem; margin-right: .375rem;
width: .875rem; width: .875rem;

View File

@ -415,12 +415,19 @@
{#if channels.length > 0} {#if channels.length > 0}
<div class="ml-22"><ChannelsView value={channels} size={'small'} on:click /></div> <div class="ml-22"><ChannelsView value={channels} size={'small'} on:click /></div>
{/if} {/if}
<div class="flex-col"> <svelte:fragment slot="pool">
<span class="text-sm fs-bold content-accent-color"><Label label={recruit.string.SkillsLabel} /></span> <YesNo label={recruit.string.Onsite} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.onsite} />
<div class="flex-grow"> <YesNo label={recruit.string.Remote} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.remote} />
<Component <Component
is={tags.component.TagsEditor} is={tags.component.TagsEditor}
props={{ items: skills, key, targetClass: recruit.mixin.Candidate, showTitle: false, elements }} props={{
items: skills,
key,
targetClass: recruit.mixin.Candidate,
showTitle: false,
elements,
countLabel: recruit.string.NumberSkills
}}
on:open={(evt) => { on:open={(evt) => {
addTagRef(evt.detail) addTagRef(evt.detail)
}} }}
@ -428,16 +435,6 @@
skills = skills.filter((it) => it._id !== evt.detail) skills = skills.filter((it) => it._id !== evt.detail)
}} }}
/> />
</div>
</div>
<svelte:fragment slot="pool">
<div class="flex-between w-full">
<span class="ml-2 content-color overflow-label"><Label label={recruit.string.WorkLocationPreferences} /></span>
<div class="buttons-group small-gap">
<YesNo label={recruit.string.Onsite} bind:value={object.onsite} />
<YesNo label={recruit.string.Remote} bind:value={object.remote} />
</div>
</div>
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="footer"> <svelte:fragment slot="footer">
<Button <Button

View File

@ -15,19 +15,23 @@
<script lang="ts"> <script lang="ts">
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import { Label } from '@anticrm/ui' import type { TooltipAlignment } from '@anticrm/ui'
import { Label, Tooltip } from '@anticrm/ui'
import recruit from '../plugin' import recruit from '../plugin'
export let label: IntlString export let label: IntlString
export let tooltip: IntlString
export let value: boolean | undefined export let value: boolean | undefined
export let disabled: boolean = false export let disabled: boolean = false
export let labelDirection: TooltipAlignment | undefined = undefined
</script> </script>
<button class="yesno-container" {disabled} class:yes={value === true} class:no={value === false} on:click={() => { <Tooltip direction={labelDirection} label={tooltip}>
<button class="yesno-container" {disabled} class:yes={value === true} class:no={value === false} on:click={() => {
if (value === true) value = false if (value === true) value = false
else if (value === false) value = undefined else if (value === false) value = undefined
else value = true else value = true
}}> }}>
<span class="overflow-label"> <span class="overflow-label">
<Label {label} /> <Label {label} />
</span> </span>
@ -43,7 +47,8 @@
{/if} {/if}
</svg> </svg>
</div> </div>
</button> </button>
</Tooltip>
<style lang="scss"> <style lang="scss">
.yesno-container { .yesno-container {

View File

@ -102,7 +102,8 @@ export default mergeIds(recruitId, recruit, {
StartDate: '' as IntlString, StartDate: '' as IntlString,
DueDate: '' as IntlString, DueDate: '' as IntlString,
CandidateReviews: '' as IntlString, CandidateReviews: '' as IntlString,
AddDescription: '' as IntlString AddDescription: '' as IntlString,
NumberSkills: '' as IntlString
}, },
space: { space: {
CandidatesPublic: '' as Ref<Space> CandidatesPublic: '' as Ref<Space>

View File

@ -16,7 +16,7 @@
import { Class, Doc, Ref, SortingOrder } from '@anticrm/core' import { Class, Doc, Ref, SortingOrder } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation' import { createQuery } from '@anticrm/presentation'
import { TagCategory, TagElement } from '@anticrm/tags' import { TagCategory, TagElement } from '@anticrm/tags'
import { getPlatformColorForText, Label } from '@anticrm/ui' import { getPlatformColorForText, Button } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import tags from '../plugin' import tags from '../plugin'
import { getTagStyle } from '../utils' import { getTagStyle } from '../utils'
@ -76,7 +76,7 @@
$: visibleCategories = categories.filter((it) => categoryKeys.includes(it._id)) $: visibleCategories = categories.filter((it) => categoryKeys.includes(it._id))
const selectItem = (ev: Event, item: TagCategory): void => { const selectItem = (item: TagCategory): void => {
if (category === item._id) { if (category === item._id) {
category = undefined category = undefined
} else { } else {
@ -87,28 +87,27 @@
</script> </script>
{#if visibleCategories.length > 0} {#if visibleCategories.length > 0}
<div class="flex-between mb-4 header"> <div class="flex-between header">
<div class="flex-row-center buttons"> <div class="flex-row-center buttons">
<div <Button
class="button flex-center" label={tags.string.AllCategories}
class:active={category === undefined} kind={'transparent'}
size={'large'}
on:click={() => { on:click={() => {
category = undefined category = undefined
dispatch('change', { category, elements: [] }) dispatch('change', { category, elements: [] })
}} }}
> />
<Label label={tags.string.AllCategories} />
</div>
</div> </div>
<div class="flex-row-center caption-color states"> <div class="flex-row-center caption-color states">
<div class="antiStatesBar mask-none {stepStyle}"> <div class="antiStatesBar mask-none {stepStyle}">
{#each visibleCategories as item, i (item._id)} {#each visibleCategories as item (item._id)}
<div <div
class="categoryElement flex-center" class="categoryElement flex-center"
label={item.label} label={item.label}
style={getTagStyle(getPlatformColorForText(item.label), item._id === category)} style={getTagStyle(getPlatformColorForText(item.label), item._id === category)}
on:click={(ev) => { on:click={() => {
if (item._id !== category) selectItem(ev, item) if (item._id !== category) selectItem(item)
}} }}
> >
{item.label} ({categoryCounts.get(item._id)?.length ?? ''}) {item.label} ({categoryCounts.get(item._id)?.length ?? ''})
@ -121,20 +120,20 @@
<style lang="scss"> <style lang="scss">
.categoryElement { .categoryElement {
padding: 0.5rem 0.75rem; padding: .375rem .75rem;
height: 2.5rem; // height: 2.5rem;
white-space: nowrap; white-space: nowrap;
border: 1px solid var(--theme-button-border-enabled); border: 1px solid var(--theme-button-border-enabled);
border-radius: 0.5rem; border-radius: .25rem;
cursor: pointer; cursor: pointer;
} }
.categoryElement + .categoryElement { .categoryElement + .categoryElement {
margin-left: 0.75rem; margin-left: .125rem;
} }
.header { .header {
margin-left: 2.5rem; padding: 0 1.75rem 1rem 2.5rem;
margin-right: 1.75rem; border-bottom: 1px solid var(--divider-color);
.buttons { .buttons {
padding: 0.125rem 0; padding: 0.125rem 0;
} }

View File

@ -56,16 +56,17 @@
<style lang="scss"> <style lang="scss">
.tag-item { .tag-item {
margin: .25rem; margin: .125rem;
padding: .5rem; padding: .125rem .25rem;
border-radius: .5rem; border-radius: .25rem;
font-weight: 500; font-weight: 500;
font-size: .625rem; font-size: .625rem;
text-transform: uppercase; text-transform: uppercase;
color: var(--theme-caption-color); color: var(--accent-color);
&:hover { color: var(--caption-color); }
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -63,4 +63,5 @@
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

@ -14,13 +14,13 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { AttachedDoc, Class, Collection, Doc, Ref } from '@anticrm/core' import type { AttachedDoc, Class, Collection, Doc, Ref } from '@anticrm/core'
import { translate } from '@anticrm/platform' import { IntlString, translate } from '@anticrm/platform'
import { KeyedAttribute } from '@anticrm/presentation' import { KeyedAttribute } from '@anticrm/presentation'
import { TagElement, TagReference } from '@anticrm/tags' import { TagElement, TagReference } from '@anticrm/tags'
import { CircleButton, IconAdd, IconClose, Label, ShowMore, showPopup, Tooltip } from '@anticrm/ui' import type { ButtonKind, ButtonSize, TooltipAlignment } from '@anticrm/ui'
import { Button, showPopup, Tooltip } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import tags from '../plugin' import tags from '../plugin'
import TagItem from './TagItem.svelte'
import TagsPopup from './TagsPopup.svelte' import TagsPopup from './TagsPopup.svelte'
export let items: TagReference[] = [] export let items: TagReference[] = []
@ -28,6 +28,13 @@
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()
@ -47,17 +54,12 @@
TagsPopup, TagsPopup,
{ {
targetClass, targetClass,
title: keyLabel,
addRef, addRef,
selected: items.map((it) => it.tag) removeTag,
selected: items.map((it) => it.tag),
keyLabel
}, },
evt.target as HTMLElement, evt.target as HTMLElement
async (result?: TagElement) => {
if (result === null || result === undefined) {
return
}
addRef(result)
}
) )
} }
@ -66,70 +68,22 @@
} }
</script> </script>
<div class="flex-row"> <Tooltip label={key.attr.label} direction={labelDirection}>
{#if showTitle} <Button
<div class="flex-row-center"> icon={tags.icon.Tags}
<div class="title"> label={items.length > 0 ? undefined : key.attr.label}
<Label label={key.attr.label} /> width={width ?? 'min-content'}
</div> {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> </div>
{/if} {/if}
<ShowMore ignore={!showTitle}> </svelte:fragment>
<div class:tags-container={showTitle} class:mt-4={showTitle}> </Button>
<div class="flex flex-reverse"> </Tooltip>
<div id='add-tag' class="ml-4">
<Tooltip label={tags.string.AddTagTooltip} props={{ word: keyLabel }}>
<CircleButton icon={IconAdd} size={'small'} selected on:click={addTag} />
</Tooltip>
</div>
<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>
</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

@ -15,27 +15,38 @@
<script lang="ts"> <script lang="ts">
import { Class, Doc, Ref } from '@anticrm/core' import { Class, Doc, Ref } from '@anticrm/core'
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import { createQuery } from '@anticrm/presentation' import { translate } from '@anticrm/platform'
import { TagElement } from '@anticrm/tags' import presentation, { createQuery, getClient } from '@anticrm/presentation'
import ui, { ActionIcon, Button, EditWithIcon, IconAdd, IconSearch, Label, showPopup } from '@anticrm/ui' import { TagCategory, TagElement } from '@anticrm/tags'
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'
import CreateTagElement from './CreateTagElement.svelte' import CreateTagElement from './CreateTagElement.svelte'
import TagItem from './TagItem.svelte' import IconView from './icons/View.svelte'
import IconViewHide from './icons/ViewHide.svelte'
export let targetClass: Ref<Class<Doc>> export let targetClass: Ref<Class<Doc>>
export let title: string export let placeholder: IntlString = presentation.string.Search
export let caption: IntlString = ui.string.Suggested
export let addRef: (tag: TagElement) => Promise<void> 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 = ''
let search: string = '' let search: string = ''
let searchElement: HTMLInputElement
let show: boolean = false
let objects: TagElement[] = [] let objects: TagElement[] = []
let available: TagElement[] = [] let categories: TagCategory[] = []
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const query = createQuery() const query = createQuery()
const client = getClient()
client.findAll(tags.class.TagCategory, { targetClass }).then((res) => { categories = res })
let phTraslate: string = ''
$: if (placeholder) translate(placeholder, {}).then(res => { phTraslate = res })
// TODO: Add $not: {$in: []} query // TODO: Add $not: {$in: []} query
$: query.query( $: query.query(
tags.class.TagElement, tags.class.TagElement,
@ -46,70 +57,141 @@
{ limit: 200 } { limit: 200 }
) )
$: available = objects.filter((it) => !selected.includes(it._id))
let anchor: HTMLElement
async function createTagElement (): Promise<void> { async function createTagElement (): Promise<void> {
showPopup(CreateTagElement, { targetClass, keyTitle: title }, anchor) showPopup(CreateTagElement, { targetClass }, 'top')
} }
async function addTag (element: TagElement): Promise<void> { async function addTag (element: TagElement): Promise<void> {
await addRef(element) await addRef(element)
selected = [...selected, element._id] selected = [...selected, element._id]
} }
const isSelected = (element: TagElement): boolean => {
if (selected.filter(p => p === element._id).length > 0) return true
return false
}
const checkSelected = (element: TagElement): void => {
if (isSelected(element)) {
selected = selected.filter(p => p !== element._id)
removeTag(element)
} else {
selected.push(element._id)
addTag(element)
}
objects = objects
categories = categories
dispatch('update', selected)
}
const toggleGroup = (ev: MouseEvent): void => {
const el: HTMLElement = ev.currentTarget as HTMLElement
el.classList.toggle('show')
}
const getCount = (cat: TagCategory): string => {
const count = objects.filter(el => el.category === cat._id).filter((it) => selected.includes(it._id)).length
if (count > 0) return count.toString()
return ''
}
</script> </script>
<div class="antiPopup antiPopup-withHeader antiPopup-withTitle"> <div class="selectPopup maxHeight">
<div class="ap-title"> <div class="header no-border">
<Label label={tags.string.AddTagTooltip} params={{ word: title }} /> <div class="flex-between flex-grow pr-2">
<div class="flex-grow">
<input bind:this={searchElement} type="text" bind:value={search} placeholder={phTraslate} style="width: 100%;" on:change/>
</div> </div>
<div class="ap-header"> <div class="buttons-group small-gap">
<EditWithIcon icon={IconSearch} bind:value={search} placeholder={tags.string.SearchCreate} focus> <div class="clear-btn" class:show={search !== ''} on:click={() => {
<svelte:fragment slot="extra"> search = ''
<div id='new-tag' class="ml-27" bind:this={anchor}> searchElement.focus()
<ActionIcon }}>
label={tags.string.AddNowTooltip} {#if search !== ''}<div class="icon"><Icon icon={IconClose} size={'inline'} /></div>{/if}
labelProps={{ word: title }}
icon={IconAdd}
action={createTagElement}
size={'small'}
/>
</div> </div>
</svelte:fragment> <Button kind={'transparent'} size={'small'} icon={show ? IconView : IconViewHide} on:click={() => show = !show} />
</EditWithIcon> <Button kind={'transparent'} size={'small'} icon={IconAdd} on:click={createTagElement} />
<div class="ap-caption">
<Label label={caption} />
</div> </div>
</div> </div>
<div class="ap-space" /> </div>
<div class="ap-scroll"> <div class="scroll">
<div class="flex flex-wrap" style={'max-height: 15rem;'}> <div class="box">
{#each available as element} {#each categories as cat}
<div {#if objects.filter(el => el.category === cat._id).length > 0}
class="hover-trans" <div class="sticky-wrapper">
on:click={() => { <button class="menu-group__header" class:show={search !== '' || show} on:click={toggleGroup}>
addTag(element) <div class="flex-row-center">
}} <span class="mr-1-5">{cat.label}</span>
> <div class="icon">
<div class="flex-between"> <svg fill="var(--content-color)" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
<TagItem <path d="M0,0L6,3L0,6Z" />
{element} </svg>
action={IconAdd}
on:action={() => {
addTag(element)
}}
/>
</div> </div>
</div> </div>
<div class="flex-row-center text-xs">
<span class="content-color mr-1">({objects.filter(el => el.category === cat._id).length})</span>
<span class="counter">{getCount(cat)}</span>
</div>
</button>
<div class="menu-group">
{#each objects.filter(el => el.category === cat._id) as element}
<button class="menu-item" on:click={() => {
checkSelected(element)
}}>
<div class="check pointer-events-none">
<CheckBox checked={isSelected(element)} primary />
</div>
<div class="tag" style="background-color: {getPlatformColor(element.color)};" />
{element.title}
</button>
{/each} {/each}
</div> </div>
</div> </div>
<div class="ap-footer"> {/if}
<Button {/each}
label={tags.string.CancelLabel} {#if objects.length === 0}
size={'small'} <div class="empty">
on:click={() => { <Label label={tags.string.NoItems} params={{ word: keyLabel }} />
dispatch('close') </div>
}} {/if}
/> </div>
</div> </div>
</div> </div>
<style lang="scss">
.clear-btn {
display: flex;
justify-content: center;
align-items: center;
width: .75rem;
height: .75rem;
border-radius: 50%;
.icon {
width: .625rem;
height: .625rem;
}
&.show {
color: var(--content-color);
background-color: var(--button-border-color);
cursor: pointer;
&:hover {
color: var(--accent-color);
background-color: var(--button-border-hover);
}
}
}
.counter {
padding-right: .125rem;
min-width: 1.5rem;
text-align: right;
font-size: .8125rem;
color: var(--caption-color);
}
.empty {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
font-size: .75rem;
color: var(--dark-color);
border-top: 1px solid var(--popup-divider);
}
</style>

View File

@ -42,7 +42,7 @@
} }
} }
function showCreateDialog (ev: Event) { function showCreateDialog () {
showPopup(CreateTagElement, { targetClass, keyTitle }, 'top') showPopup(CreateTagElement, { targetClass, keyTitle }, 'top')
} }
const opt: FindOptions<TagElement> = { const opt: FindOptions<TagElement> = {
@ -65,7 +65,7 @@
updateResultQuery(search, category) updateResultQuery(search, category)
}} }}
/> />
<Button icon={IconAdd} label={сreateItemLabel} kind={'primary'} on:click={(ev) => showCreateDialog(ev)} /> <Button icon={IconAdd} label={сreateItemLabel} kind={'primary'} on:click={showCreateDialog} />
</div> </div>
<CategoryBar <CategoryBar

View File

@ -0,0 +1,25 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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">
export let size: 'x-small' | 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12,8c-2.2,0-4,1.8-4,4c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4C16,9.8,14.2,8,12,8z M12,15c-1.7,0-3-1.3-3-3s1.3-3,3-3s3,1.3,3,3 S13.7,15,12,15z" />
<path d="M20.6,10.6l-0.4,0.3l-0.4,0.3c0.4,0.5,0.5,0.6,0.5,0.7s-0.1,0.3-0.5,0.7c-1.5,1.8-4.4,4.8-7.8,4.8c-3.4,0-6.3-3-7.8-4.8 c-0.4-0.5-0.5-0.6-0.5-0.7c0-0.2,0.1-0.3,0.5-0.7C5.7,9.5,8.6,6.5,12,6.5c3.4,0,6.3,3,7.8,4.8l0.4-0.3L20.6,10.6L20.6,10.6 C19,8.7,15.8,5.5,12,5.5c-3.8,0-7,3.2-8.6,5.1C3,11.1,2.7,11.5,2.7,12s0.3,0.9,0.7,1.4c1.6,1.9,4.8,5.1,8.6,5.1 c3.8,0,7-3.2,8.6-5.1c0.4-0.5,0.7-0.8,0.7-1.4S21,11.1,20.6,10.6z" />
</svg>

View File

@ -0,0 +1,26 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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">
export let size: 'x-small' | 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M8.7,9.8C8.2,10.4,8,11.2,8,12c0,2.2,1.8,4,4,4c0.8,0,1.6-0.2,2.2-0.7l-0.7-0.7C13,14.9,12.5,15,12,15c-1.7,0-3-1.3-3-3 c0-0.5,0.1-1,0.4-1.5L8.7,9.8z" />
<path d="M12,17.5c-1.7,0-3.3-0.7-4.6-1.7c-1.4-1-2.5-2.2-3.2-3c-0.4-0.5-0.5-0.6-0.5-0.7s0-0.2,0.5-0.7c0.7-0.8,1.7-2,3-2.9 L6.5,7.6c-1.3,1-2.4,2.2-3.1,3l-0.1,0.1C3,11.1,2.7,11.5,2.7,12s0.3,0.9,0.6,1.3l0.1,0.1c0.7,0.9,1.9,2.1,3.4,3.2 c1.5,1,3.3,1.9,5.2,1.9c1.5,0,2.9-0.5,4.2-1.2l-0.7-0.7C14.4,17.1,13.2,17.5,12,17.5z" />
<path d="M18.9,15.2c0.7-0.7,1.3-1.3,1.7-1.8l0.1-0.1c0.3-0.4,0.6-0.8,0.6-1.3s-0.3-0.9-0.6-1.3l-0.1-0.1c-0.7-0.9-1.9-2.1-3.4-3.2 c-1.5-1-3.3-1.9-5.2-1.9c-0.8,0-1.6,0.2-2.4,0.4L5.4,1.6L4.6,2.4l16,16l0.7-0.7L18.9,15.2z M12,6.5c1.7,0,3.3,0.7,4.6,1.7 c1.4,1,2.5,2.2,3.2,3c0.4,0.5,0.5,0.6,0.5,0.7s0,0.2-0.5,0.7c-0.4,0.5-1,1.1-1.6,1.7L16,12.3c0-0.1,0-0.2,0-0.3c0-2.2-1.8-4-4-4 c-0.1,0-0.2,0-0.3,0l-1.3-1.3C10.9,6.6,11.5,6.5,12,6.5z M14.9,11.2l-2-2C13.8,9.4,14.6,10.2,14.9,11.2z" />
</svg>

View File

@ -24,17 +24,19 @@ test.describe('recruit tests', () => {
// Fill [placeholder="Appleseed"] // Fill [placeholder="Appleseed"]
await page.fill('[placeholder="Appleseed"]', 'Dooliutl') await page.fill('[placeholder="Appleseed"]', 'Dooliutl')
// Click .ml-4 .tooltip-trigger .flex-center // Click .ml-4 .tooltip-trigger .flex-center
await page.click('#add-tag') await page.click('button:has-text("Skills")')
// Click text=Add/Create Skill Suggested Cancel >> button // Click text=Add/Create Skill Suggested Cancel >> button
await page.click('#new-tag') await page.click('.buttons-group button:nth-child(3)')
// Fill [placeholder="Please\ type\ Skill\ title"] // Fill [placeholder="Please\ type\ Skill\ title"]
await page.fill('[placeholder="Please\\ type\\ Skill\\ title"]', 's1') await page.fill('[placeholder="Please\\ type\\ \\ title"]', 's1')
// Click text=Create Skill s1 Please type description here Category Other Create Cancel >> button // Click text=Create Skill s1 Please type description here Category Other Create Cancel >> button
await page.click('text=Create more Create >> button') await page.click('text=Create more Create >> button')
await page.click('button:has-text("Other (1)")')
// Click text=s1 // Click text=s1
await page.click('text=s1') await page.click('text=s1')
// Click :nth-match(:text("Cancel"), 2) // Click :nth-match(:text("Cancel"), 2)
await page.click('button:has-text("Cancel")') // await page.click('button:has-text("Cancel")')
await page.keyboard.press('Escape')
// Click button:has-text("Create") // Click button:has-text("Create")
await page.click('button:has-text("Create")') await page.click('button:has-text("Create")')
}) })
@ -74,11 +76,13 @@ test.describe('recruit tests', () => {
// Click button:has-text("Candidate") // Click button:has-text("Candidate")
await page.click('button:has-text("Candidate")') await page.click('button:has-text("Candidate")')
// Click #add-tag div div // Click #add-tag div div
await page.click('#add-tag div div') await page.click('button:has-text("Skills")')
await page.click('button:has-text("Backend development (1)")')
// Click text=java // Click text=java
await page.click('text=java') await page.click('text=java')
// Click :nth-match(:text("Cancel"), 2) // Click :nth-match(:text("Cancel"), 2)
await page.click('button:has-text("Cancel")') // await page.click('button:has-text("Cancel")')
await page.keyboard.press('Escape')
// Click [placeholder="John"] // Click [placeholder="John"]
await page.click('[placeholder="John"]') await page.click('[placeholder="John"]')
// Fill [placeholder="John"] // Fill [placeholder="John"]