mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-26 13:47:26 +03:00
Update TagEditor layout (#1420)
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
parent
5dee8d9072
commit
f0cca26e9b
@ -15,6 +15,6 @@
|
||||
"Search": "Search...",
|
||||
"Unassigned": "Unassigned",
|
||||
"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}}"
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,6 @@
|
||||
"Search": "Поиск...",
|
||||
"Unassigned": "Не назначен",
|
||||
"CreateMore": "Создать еще",
|
||||
"NumberMembers": "{lenght, plural, =0 {нет участников} =1 {1 участик} other {# участника}}"
|
||||
"NumberMembers": "{count, plural, =0 {нет участников} =1 {1 участик} other {# участника}}"
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@
|
||||
{#if persons.length > 0}
|
||||
<div class="flex-row-center flex-nowrap">
|
||||
<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>
|
||||
{/await}
|
||||
</div>
|
||||
|
@ -296,6 +296,7 @@ p:last-child { margin-block-end: 0; }
|
||||
.ml-12 { margin-left: 3rem; }
|
||||
.ml-22 { margin-left: 5.5rem; }
|
||||
.mr-1 { margin-right: .25rem; }
|
||||
.mr-1-5 { margin-right: .375rem; }
|
||||
.mr-2 { margin-right: .5rem; }
|
||||
.mr-3 { margin-right: .75rem; }
|
||||
.mr-4 { margin-right: 1rem; }
|
||||
@ -461,6 +462,7 @@ a.no-line {
|
||||
color: var(--theme-content-trans-color);
|
||||
user-select: none;
|
||||
}
|
||||
.text-xs { font-size: .625rem; }
|
||||
.text-sm { font-size: .75rem; }
|
||||
.text-md { font-size: .8125rem; }
|
||||
.text-lg { font-size: 1.125rem; }
|
||||
|
@ -18,18 +18,20 @@
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min-content;
|
||||
width: fit-content;
|
||||
min-width: 10rem;
|
||||
max-width: 45rem;
|
||||
max-height: 20rem;
|
||||
max-width: 15rem;
|
||||
max-height: 22rem;
|
||||
background: var(--popup-bg-color);
|
||||
border-radius: .5rem;
|
||||
box-shadow: var(--popup-shadow);
|
||||
will-change: transform;
|
||||
|
||||
&.maxHeight { height: 22rem; }
|
||||
.header {
|
||||
border-bottom: 1px solid var(--popup-divider);
|
||||
|
||||
&.no-border { border-bottom-color: transparent; }
|
||||
input {
|
||||
margin: 0;
|
||||
padding: .625rem .75rem;
|
||||
@ -59,25 +61,33 @@
|
||||
.menu-item {
|
||||
flex-shrink: 0;
|
||||
justify-content: start;
|
||||
padding: 0 .75rem;
|
||||
height: 2rem;
|
||||
padding: .25rem .75rem;
|
||||
min-height: 2rem;
|
||||
text-align: left;
|
||||
color: var(--caption-color);
|
||||
cursor: pointer;
|
||||
|
||||
&.high { height: 3rem; }
|
||||
.icon {
|
||||
|
||||
.icon, .color, .tag {
|
||||
flex-shrink: 0;
|
||||
margin-right: .75rem;
|
||||
}
|
||||
.icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
.color {
|
||||
margin-right: .75rem;
|
||||
width: .875rem;
|
||||
height: .875rem;
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
border-radius: .25rem;
|
||||
}
|
||||
.tag {
|
||||
width: .5rem;
|
||||
height: .5rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
@ -89,11 +99,50 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: .75rem;
|
||||
height: 2rem;
|
||||
}
|
||||
.check-right { margin: 0 0 0 2rem; }
|
||||
&: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 {
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { AnySvelteComponent } from '../types'
|
||||
|
||||
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 filled: boolean = false
|
||||
|
||||
|
@ -31,6 +31,7 @@
|
||||
let modalHTML: HTMLElement
|
||||
let componentInstance: any
|
||||
let show: boolean = false
|
||||
let height: number
|
||||
|
||||
function _update (result: any): void {
|
||||
if (onUpdate !== undefined) onUpdate(result)
|
||||
@ -61,11 +62,12 @@
|
||||
}
|
||||
}
|
||||
afterUpdate(() => fitPopup())
|
||||
$: if (height) fitPopup()
|
||||
</script>
|
||||
|
||||
<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)} />
|
||||
</div>
|
||||
<div class="modal-overlay" class:antiOverlay={show} style={`z-index: ${zIndex};`} on:click={() => escapeClose()} />
|
||||
|
@ -86,7 +86,8 @@
|
||||
"Participants": "Participants",
|
||||
"NoParticipants": "No participants added",
|
||||
"PersonsLabel": "{name}",
|
||||
"AddDescription": "Add description"
|
||||
"AddDescription": "Add description",
|
||||
"NumberSkills": "{count, plural, =0 {no skills} =1 {1 skill} other {# skills}}"
|
||||
},
|
||||
"status": {
|
||||
"CandidateRequired": "Please select candidate",
|
||||
|
@ -87,7 +87,8 @@
|
||||
"Participants": "Участники",
|
||||
"NoParticipants": "Участники не добавлены",
|
||||
"PersonsLabel": "{name}",
|
||||
"AddDescription": "Add description"
|
||||
"AddDescription": "Add description",
|
||||
"NumberSkills": "{count, plural, =0 {нет навыков} =1 {1 навык} =2 {2 навыка} other {# навыков}}"
|
||||
},
|
||||
"status": {
|
||||
"CandidateRequired": "Пожалуйста выберите кандидата",
|
||||
|
@ -231,22 +231,22 @@
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.card {
|
||||
align-self: stretch;
|
||||
width: calc(50% - 3rem);
|
||||
min-height: 16rem;
|
||||
// .card {
|
||||
// align-self: stretch;
|
||||
// width: calc(50% - 3rem);
|
||||
// min-height: 16rem;
|
||||
|
||||
&.empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: .75rem;
|
||||
color: var(--dark-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: .25rem;
|
||||
}
|
||||
}
|
||||
.arrows { width: 4rem; }
|
||||
// &.empty {
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// font-size: .75rem;
|
||||
// color: var(--dark-color);
|
||||
// border: 1px solid var(--divider-color);
|
||||
// border-radius: .25rem;
|
||||
// }
|
||||
// }
|
||||
// .arrows { width: 4rem; }
|
||||
.color {
|
||||
margin-right: .375rem;
|
||||
width: .875rem;
|
||||
|
@ -415,29 +415,26 @@
|
||||
{#if channels.length > 0}
|
||||
<div class="ml-22"><ChannelsView value={channels} size={'small'} on:click /></div>
|
||||
{/if}
|
||||
<div class="flex-col">
|
||||
<span class="text-sm fs-bold content-accent-color"><Label label={recruit.string.SkillsLabel} /></span>
|
||||
<div class="flex-grow">
|
||||
<Component
|
||||
is={tags.component.TagsEditor}
|
||||
props={{ items: skills, key, targetClass: recruit.mixin.Candidate, showTitle: false, elements }}
|
||||
on:open={(evt) => {
|
||||
addTagRef(evt.detail)
|
||||
}}
|
||||
on:delete={(evt) => {
|
||||
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>
|
||||
<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}
|
||||
props={{
|
||||
items: skills,
|
||||
key,
|
||||
targetClass: recruit.mixin.Candidate,
|
||||
showTitle: false,
|
||||
elements,
|
||||
countLabel: recruit.string.NumberSkills
|
||||
}}
|
||||
on:open={(evt) => {
|
||||
addTagRef(evt.detail)
|
||||
}}
|
||||
on:delete={(evt) => {
|
||||
skills = skills.filter((it) => it._id !== evt.detail)
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button
|
||||
|
@ -15,35 +15,40 @@
|
||||
|
||||
<script lang="ts">
|
||||
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'
|
||||
|
||||
export let label: IntlString
|
||||
export let tooltip: IntlString
|
||||
export let value: boolean | undefined
|
||||
export let disabled: boolean = false
|
||||
export let labelDirection: TooltipAlignment | undefined = undefined
|
||||
</script>
|
||||
|
||||
<button class="yesno-container" {disabled} class:yes={value === true} class:no={value === false} on:click={() => {
|
||||
if (value === true) value = false
|
||||
else if (value === false) value = undefined
|
||||
else value = true
|
||||
}}>
|
||||
<span class="overflow-label">
|
||||
<Label {label} />
|
||||
</span>
|
||||
<div class="btn-icon ml-1">
|
||||
<svg class="yesno-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<circle class="circle" class:yes={value === true} class:no={value === false} cx="8" cy="8" r="6"/>
|
||||
{#if value === true}
|
||||
<polygon fill="#fff" points="7.4,10.9 4.9,8.4 5.7,7.6 7.3,9.1 10.2,5.6 11.1,6.4 "/>
|
||||
{:else if value === false}
|
||||
<polygon fill="#fff" points="10.7,6 10,5.3 8,7.3 6,5.3 5.3,6 7.3,8 5.3,10 6,10.7 8,8.7 10,10.7 10.7,10 8.7,8 "/>
|
||||
{:else}
|
||||
<path fill="#fff" d="M7.3,9.3h1.3V9c0.1-0.5,0.6-0.9,1.1-1.4c0.4-0.4,0.8-0.9,0.8-1.6c0-1.1-0.8-1.8-2.2-1.8c-1.4,0-2.4,0.8-2.5,2.2 h1.4c0.1-0.6,0.4-1,1-1C8.8,5.4,9,5.7,9,6.2c0,0.4-0.3,0.7-0.7,1.1c-0.5,0.5-1,0.9-1,1.7V9.3z M8,11.6c0.5,0,0.9-0.4,0.9-0.9 c0-0.5-0.4-0.9-0.9-0.9c-0.5,0-0.9,0.4-0.9,0.9C7.1,11.2,7.5,11.6,8,11.6z"/>
|
||||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<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
|
||||
else if (value === false) value = undefined
|
||||
else value = true
|
||||
}}>
|
||||
<span class="overflow-label">
|
||||
<Label {label} />
|
||||
</span>
|
||||
<div class="btn-icon ml-1">
|
||||
<svg class="yesno-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<circle class="circle" class:yes={value === true} class:no={value === false} cx="8" cy="8" r="6"/>
|
||||
{#if value === true}
|
||||
<polygon fill="#fff" points="7.4,10.9 4.9,8.4 5.7,7.6 7.3,9.1 10.2,5.6 11.1,6.4 "/>
|
||||
{:else if value === false}
|
||||
<polygon fill="#fff" points="10.7,6 10,5.3 8,7.3 6,5.3 5.3,6 7.3,8 5.3,10 6,10.7 8,8.7 10,10.7 10.7,10 8.7,8 "/>
|
||||
{:else}
|
||||
<path fill="#fff" d="M7.3,9.3h1.3V9c0.1-0.5,0.6-0.9,1.1-1.4c0.4-0.4,0.8-0.9,0.8-1.6c0-1.1-0.8-1.8-2.2-1.8c-1.4,0-2.4,0.8-2.5,2.2 h1.4c0.1-0.6,0.4-1,1-1C8.8,5.4,9,5.7,9,6.2c0,0.4-0.3,0.7-0.7,1.1c-0.5,0.5-1,0.9-1,1.7V9.3z M8,11.6c0.5,0,0.9-0.4,0.9-0.9 c0-0.5-0.4-0.9-0.9-0.9c-0.5,0-0.9,0.4-0.9,0.9C7.1,11.2,7.5,11.6,8,11.6z"/>
|
||||
{/if}
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<style lang="scss">
|
||||
.yesno-container {
|
||||
|
@ -102,7 +102,8 @@ export default mergeIds(recruitId, recruit, {
|
||||
StartDate: '' as IntlString,
|
||||
DueDate: '' as IntlString,
|
||||
CandidateReviews: '' as IntlString,
|
||||
AddDescription: '' as IntlString
|
||||
AddDescription: '' as IntlString,
|
||||
NumberSkills: '' as IntlString
|
||||
},
|
||||
space: {
|
||||
CandidatesPublic: '' as Ref<Space>
|
||||
|
@ -16,7 +16,7 @@
|
||||
import { Class, Doc, Ref, SortingOrder } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { TagCategory, TagElement } from '@anticrm/tags'
|
||||
import { getPlatformColorForText, Label } from '@anticrm/ui'
|
||||
import { getPlatformColorForText, Button } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tags from '../plugin'
|
||||
import { getTagStyle } from '../utils'
|
||||
@ -76,7 +76,7 @@
|
||||
|
||||
$: visibleCategories = categories.filter((it) => categoryKeys.includes(it._id))
|
||||
|
||||
const selectItem = (ev: Event, item: TagCategory): void => {
|
||||
const selectItem = (item: TagCategory): void => {
|
||||
if (category === item._id) {
|
||||
category = undefined
|
||||
} else {
|
||||
@ -87,28 +87,27 @@
|
||||
</script>
|
||||
|
||||
{#if visibleCategories.length > 0}
|
||||
<div class="flex-between mb-4 header">
|
||||
<div class="flex-between header">
|
||||
<div class="flex-row-center buttons">
|
||||
<div
|
||||
class="button flex-center"
|
||||
class:active={category === undefined}
|
||||
<Button
|
||||
label={tags.string.AllCategories}
|
||||
kind={'transparent'}
|
||||
size={'large'}
|
||||
on:click={() => {
|
||||
category = undefined
|
||||
dispatch('change', { category, elements: [] })
|
||||
}}
|
||||
>
|
||||
<Label label={tags.string.AllCategories} />
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-row-center caption-color states">
|
||||
<div class="antiStatesBar mask-none {stepStyle}">
|
||||
{#each visibleCategories as item, i (item._id)}
|
||||
{#each visibleCategories as item (item._id)}
|
||||
<div
|
||||
class="categoryElement flex-center"
|
||||
label={item.label}
|
||||
style={getTagStyle(getPlatformColorForText(item.label), item._id === category)}
|
||||
on:click={(ev) => {
|
||||
if (item._id !== category) selectItem(ev, item)
|
||||
on:click={() => {
|
||||
if (item._id !== category) selectItem(item)
|
||||
}}
|
||||
>
|
||||
{item.label} ({categoryCounts.get(item._id)?.length ?? ''})
|
||||
@ -121,20 +120,20 @@
|
||||
|
||||
<style lang="scss">
|
||||
.categoryElement {
|
||||
padding: 0.5rem 0.75rem;
|
||||
height: 2.5rem;
|
||||
padding: .375rem .75rem;
|
||||
// height: 2.5rem;
|
||||
white-space: nowrap;
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
border-radius: 0.5rem;
|
||||
border-radius: .25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.categoryElement + .categoryElement {
|
||||
margin-left: 0.75rem;
|
||||
margin-left: .125rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-left: 2.5rem;
|
||||
margin-right: 1.75rem;
|
||||
padding: 0 1.75rem 1rem 2.5rem;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
.buttons {
|
||||
padding: 0.125rem 0;
|
||||
}
|
||||
|
@ -56,16 +56,17 @@
|
||||
|
||||
<style lang="scss">
|
||||
.tag-item {
|
||||
margin: .25rem;
|
||||
padding: .5rem;
|
||||
margin: .125rem;
|
||||
padding: .125rem .25rem;
|
||||
|
||||
border-radius: .5rem;
|
||||
border-radius: .25rem;
|
||||
|
||||
font-weight: 500;
|
||||
font-size: .625rem;
|
||||
|
||||
text-transform: uppercase;
|
||||
color: var(--theme-caption-color);
|
||||
color: var(--accent-color);
|
||||
&:hover { color: var(--caption-color); }
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -63,4 +63,5 @@
|
||||
targetClass={_class}
|
||||
on:open={(evt) => addRef(evt.detail)}
|
||||
on:delete={(evt) => removeTag(evt.detail)}
|
||||
countLabel={key.attr.label}
|
||||
/>
|
||||
|
@ -14,13 +14,13 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 { 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 tags from '../plugin'
|
||||
import TagItem from './TagItem.svelte'
|
||||
import TagsPopup from './TagsPopup.svelte'
|
||||
|
||||
export let items: TagReference[] = []
|
||||
@ -28,6 +28,13 @@
|
||||
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()
|
||||
|
||||
@ -47,17 +54,12 @@
|
||||
TagsPopup,
|
||||
{
|
||||
targetClass,
|
||||
title: keyLabel,
|
||||
addRef,
|
||||
selected: items.map((it) => it.tag)
|
||||
removeTag,
|
||||
selected: items.map((it) => it.tag),
|
||||
keyLabel
|
||||
},
|
||||
evt.target as HTMLElement,
|
||||
async (result?: TagElement) => {
|
||||
if (result === null || result === undefined) {
|
||||
return
|
||||
}
|
||||
addRef(result)
|
||||
}
|
||||
evt.target as HTMLElement
|
||||
)
|
||||
}
|
||||
|
||||
@ -66,70 +68,22 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-row">
|
||||
{#if showTitle}
|
||||
<div class="flex-row-center">
|
||||
<div class="title">
|
||||
<Label label={key.attr.label} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<ShowMore ignore={!showTitle}>
|
||||
<div class:tags-container={showTitle} class:mt-4={showTitle}>
|
||||
<div class="flex flex-reverse">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
@ -15,27 +15,38 @@
|
||||
<script lang="ts">
|
||||
import { Class, Doc, Ref } from '@anticrm/core'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { TagElement } from '@anticrm/tags'
|
||||
import ui, { ActionIcon, Button, EditWithIcon, IconAdd, IconSearch, Label, showPopup } from '@anticrm/ui'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import presentation, { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { TagCategory, TagElement } from '@anticrm/tags'
|
||||
import { CheckBox, Button, Icon, IconAdd, IconClose, Label, showPopup, getPlatformColor } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tags from '../plugin'
|
||||
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 title: string
|
||||
export let caption: IntlString = ui.string.Suggested
|
||||
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 = ''
|
||||
|
||||
let search: string = ''
|
||||
let searchElement: HTMLInputElement
|
||||
let show: boolean = false
|
||||
let objects: TagElement[] = []
|
||||
let available: TagElement[] = []
|
||||
let categories: TagCategory[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
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
|
||||
$: query.query(
|
||||
tags.class.TagElement,
|
||||
@ -46,70 +57,141 @@
|
||||
{ limit: 200 }
|
||||
)
|
||||
|
||||
$: available = objects.filter((it) => !selected.includes(it._id))
|
||||
|
||||
let anchor: HTMLElement
|
||||
async function createTagElement (): Promise<void> {
|
||||
showPopup(CreateTagElement, { targetClass, keyTitle: title }, anchor)
|
||||
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
|
||||
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>
|
||||
|
||||
<div class="antiPopup antiPopup-withHeader antiPopup-withTitle">
|
||||
<div class="ap-title">
|
||||
<Label label={tags.string.AddTagTooltip} params={{ word: title }} />
|
||||
</div>
|
||||
<div class="ap-header">
|
||||
<EditWithIcon icon={IconSearch} bind:value={search} placeholder={tags.string.SearchCreate} focus>
|
||||
<svelte:fragment slot="extra">
|
||||
<div id='new-tag' class="ml-27" bind:this={anchor}>
|
||||
<ActionIcon
|
||||
label={tags.string.AddNowTooltip}
|
||||
labelProps={{ word: title }}
|
||||
icon={IconAdd}
|
||||
action={createTagElement}
|
||||
size={'small'}
|
||||
/>
|
||||
<div class="selectPopup maxHeight">
|
||||
<div class="header no-border">
|
||||
<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 class="buttons-group small-gap">
|
||||
<div class="clear-btn" class:show={search !== ''} on:click={() => {
|
||||
search = ''
|
||||
searchElement.focus()
|
||||
}}>
|
||||
{#if search !== ''}<div class="icon"><Icon icon={IconClose} size={'inline'} /></div>{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</EditWithIcon>
|
||||
<div class="ap-caption">
|
||||
<Label label={caption} />
|
||||
<Button kind={'transparent'} size={'small'} icon={show ? IconView : IconViewHide} on:click={() => show = !show} />
|
||||
<Button kind={'transparent'} size={'small'} icon={IconAdd} on:click={createTagElement} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ap-space" />
|
||||
<div class="ap-scroll">
|
||||
<div class="flex flex-wrap" style={'max-height: 15rem;'}>
|
||||
{#each available as element}
|
||||
<div
|
||||
class="hover-trans"
|
||||
on:click={() => {
|
||||
addTag(element)
|
||||
}}
|
||||
>
|
||||
<div class="flex-between">
|
||||
<TagItem
|
||||
{element}
|
||||
action={IconAdd}
|
||||
on:action={() => {
|
||||
addTag(element)
|
||||
}}
|
||||
/>
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#each categories as cat}
|
||||
{#if objects.filter(el => el.category === cat._id).length > 0}
|
||||
<div class="sticky-wrapper">
|
||||
<button class="menu-group__header" class:show={search !== '' || show} on:click={toggleGroup}>
|
||||
<div class="flex-row-center">
|
||||
<span class="mr-1-5">{cat.label}</span>
|
||||
<div class="icon">
|
||||
<svg fill="var(--content-color)" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0,0L6,3L0,6Z" />
|
||||
</svg>
|
||||
</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}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if objects.length === 0}
|
||||
<div class="empty">
|
||||
<Label label={tags.string.NoItems} params={{ word: keyLabel }} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ap-footer">
|
||||
<Button
|
||||
label={tags.string.CancelLabel}
|
||||
size={'small'}
|
||||
on:click={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
|
@ -42,7 +42,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateDialog (ev: Event) {
|
||||
function showCreateDialog () {
|
||||
showPopup(CreateTagElement, { targetClass, keyTitle }, 'top')
|
||||
}
|
||||
const opt: FindOptions<TagElement> = {
|
||||
@ -65,7 +65,7 @@
|
||||
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>
|
||||
|
||||
<CategoryBar
|
||||
|
25
plugins/tags-resources/src/components/icons/View.svelte
Normal file
25
plugins/tags-resources/src/components/icons/View.svelte
Normal 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>
|
26
plugins/tags-resources/src/components/icons/ViewHide.svelte
Normal file
26
plugins/tags-resources/src/components/icons/ViewHide.svelte
Normal 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>
|
@ -24,17 +24,19 @@ test.describe('recruit tests', () => {
|
||||
// Fill [placeholder="Appleseed"]
|
||||
await page.fill('[placeholder="Appleseed"]', 'Dooliutl')
|
||||
// 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
|
||||
await page.click('#new-tag')
|
||||
await page.click('.buttons-group button:nth-child(3)')
|
||||
// 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
|
||||
await page.click('text=Create more Create >> button')
|
||||
await page.click('button:has-text("Other (1)")')
|
||||
// Click text=s1
|
||||
await page.click('text=s1')
|
||||
// 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")
|
||||
await page.click('button:has-text("Create")')
|
||||
})
|
||||
@ -74,11 +76,13 @@ test.describe('recruit tests', () => {
|
||||
// Click button:has-text("Candidate")
|
||||
await page.click('button:has-text("Candidate")')
|
||||
// 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
|
||||
await page.click('text=java')
|
||||
// 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"]
|
||||
await page.click('[placeholder="John"]')
|
||||
// Fill [placeholder="John"]
|
||||
|
Loading…
Reference in New Issue
Block a user