diff --git a/packages/theme/styles/_layouts.scss b/packages/theme/styles/_layouts.scss index 1482b17517..dcee918a73 100644 --- a/packages/theme/styles/_layouts.scss +++ b/packages/theme/styles/_layouts.scss @@ -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 */ diff --git a/packages/theme/styles/dialogs.scss b/packages/theme/styles/dialogs.scss index 9d2bd1e61b..ff7d4264d8 100644 --- a/packages/theme/styles/dialogs.scss +++ b/packages/theme/styles/dialogs.scss @@ -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 { diff --git a/packages/ui/src/components/CheckBox.svelte b/packages/ui/src/components/CheckBox.svelte index 2cf2878991..74ea87828e 100644 --- a/packages/ui/src/components/CheckBox.svelte +++ b/packages/ui/src/components/CheckBox.svelte @@ -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); diff --git a/packages/ui/src/components/Link.svelte b/packages/ui/src/components/Link.svelte index 3a82da9169..84675854fa 100644 --- a/packages/ui/src/components/Link.svelte +++ b/packages/ui/src/components/Link.svelte @@ -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> diff --git a/plugins/activity-resources/src/components/Activity.svelte b/plugins/activity-resources/src/components/Activity.svelte index 7f4a49c4ec..d22d9ef409 100644 --- a/plugins/activity-resources/src/components/Activity.svelte +++ b/plugins/activity-resources/src/components/Activity.svelte @@ -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)} diff --git a/plugins/activity-resources/src/components/TxViewTx.svelte b/plugins/activity-resources/src/components/TxViewTx.svelte index 377b87fdd9..0041dbc514 100644 --- a/plugins/activity-resources/src/components/TxViewTx.svelte +++ b/plugins/activity-resources/src/components/TxViewTx.svelte @@ -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; diff --git a/plugins/recruit-resources/src/components/CreateCandidate.svelte b/plugins/recruit-resources/src/components/CreateCandidate.svelte index 01c1c34f7f..ee1acefbff 100644 --- a/plugins/recruit-resources/src/components/CreateCandidate.svelte +++ b/plugins/recruit-resources/src/components/CreateCandidate.svelte @@ -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> diff --git a/plugins/recruit-resources/src/components/icons/FileUpload.svelte b/plugins/recruit-resources/src/components/icons/FileUpload.svelte index a8fa6c4b1b..a78abb021c 100644 --- a/plugins/recruit-resources/src/components/icons/FileUpload.svelte +++ b/plugins/recruit-resources/src/components/icons/FileUpload.svelte @@ -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"> diff --git a/plugins/tags-resources/src/components/TagItem.svelte b/plugins/tags-resources/src/components/TagItem.svelte index 838701d918..5350e6eebb 100644 --- a/plugins/tags-resources/src/components/TagItem.svelte +++ b/plugins/tags-resources/src/components/TagItem.svelte @@ -41,7 +41,7 @@ > {name} {#if action} - <div class="ml-2"> + <div class="ml-1"> <ActionIcon icon={action} size={'small'} diff --git a/plugins/tags-resources/src/components/Tags.svelte b/plugins/tags-resources/src/components/Tags.svelte index 68f7b088fd..33d4a05e18 100644 --- a/plugins/tags-resources/src/components/Tags.svelte +++ b/plugins/tags-resources/src/components/Tags.svelte @@ -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} /> diff --git a/plugins/tags-resources/src/components/TagsDropdownEditor.svelte b/plugins/tags-resources/src/components/TagsDropdownEditor.svelte new file mode 100644 index 0000000000..4af77055b4 --- /dev/null +++ b/plugins/tags-resources/src/components/TagsDropdownEditor.svelte @@ -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> diff --git a/plugins/tags-resources/src/components/TagsEditor.svelte b/plugins/tags-resources/src/components/TagsEditor.svelte index f0e3a4d852..e9ea0d069f 100644 --- a/plugins/tags-resources/src/components/TagsEditor.svelte +++ b/plugins/tags-resources/src/components/TagsEditor.svelte @@ -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> diff --git a/plugins/tags-resources/src/components/TagsPopup.svelte b/plugins/tags-resources/src/components/TagsPopup.svelte index c429ecbc63..48b5a1a3af 100644 --- a/plugins/tags-resources/src/components/TagsPopup.svelte +++ b/plugins/tags-resources/src/components/TagsPopup.svelte @@ -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> diff --git a/plugins/tags-resources/src/index.ts b/plugins/tags-resources/src/index.ts index 8209deb729..e160b4e291 100644 --- a/plugins/tags-resources/src/index.ts +++ b/plugins/tags-resources/src/index.ts @@ -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 diff --git a/plugins/tags/src/index.ts b/plugins/tags/src/index.ts index b7d2688bd2..89bf390238 100644 --- a/plugins/tags/src/index.ts +++ b/plugins/tags/src/index.ts @@ -79,6 +79,7 @@ const tagsPlugin = plugin(tagsId, { component: { TagsView: '' as AnyComponent, TagsEditor: '' as AnyComponent, + TagsDropdownEditor: '' as AnyComponent, TagsCategoryBar: '' as AnyComponent }, category: {