mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-10 11:47:42 +02:00
Merge remote-tracking branch 'origin/ano/remove-paddings-from-card-container' into ano/attachments-initial
# Conflicts: # plugins/board-resources/src/components/KanbanCard.svelte
This commit is contained in:
@@ -246,11 +246,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: .5rem 1rem;
|
||||
background-color: var(--board-card-bg-color);
|
||||
border: 1px solid var(--board-card-bg-color);
|
||||
border-radius: .25rem;
|
||||
user-select: none;
|
||||
|
||||
@@ -261,9 +257,7 @@
|
||||
cursor: grab;
|
||||
}
|
||||
&.dragged {
|
||||
padding: 1rem;
|
||||
background-color: var(--board-bg-color);
|
||||
border: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"Remove": "Remove",
|
||||
"Members": "Members",
|
||||
"Search": "Search...",
|
||||
"Unassigned": "Unassigned"
|
||||
"Unassigned": "Unassigned",
|
||||
"CreateMore": "Create more"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"Remove": "Удалить",
|
||||
"Members": "Участники",
|
||||
"Search": "Поиск...",
|
||||
"Unassigned": "Не назначен"
|
||||
"Unassigned": "Не назначен",
|
||||
"CreateMore": "Создать еще"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { Ref, Class, Space, DocumentQuery } from '@anticrm/core'
|
||||
|
||||
import { Button, Label } from '@anticrm/ui'
|
||||
import { Button, Label, IconClose, MiniToggle } from '@anticrm/ui'
|
||||
import SpaceSelect from './SpaceSelect.svelte'
|
||||
import presentation from '..'
|
||||
|
||||
@@ -34,31 +34,50 @@
|
||||
export let okAction: () => void
|
||||
export let canSave: boolean = false
|
||||
export let size: 'small'| 'medium' = 'small'
|
||||
|
||||
export let createMore: boolean | undefined = undefined
|
||||
export let okLabel: IntlString = presentation.string.Create
|
||||
export let cancelLabel: IntlString = presentation.string.Cancel
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<form class="antiCard" class:w-85={size === 'small'} class:w-165={size === 'medium'} on:submit|preventDefault={ () => {} }>
|
||||
<form class="antiCard dialog" on:submit|preventDefault={ () => {} }>
|
||||
<div class="antiCard-header">
|
||||
<div class="antiCard-header__title"><Label {label} params={labelProps ?? {}} /></div>
|
||||
{#if $$slots.error}
|
||||
<div class="antiCard-header__error">
|
||||
<slot name="error" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="antiCard-header__title-wrap">
|
||||
{#if spaceClass && spaceLabel && spacePlaceholder}
|
||||
{#if $$slots.space}
|
||||
<slot name="space" />
|
||||
{:else}
|
||||
<SpaceSelect _class={spaceClass} spaceQuery={spaceQuery} label={spaceLabel} placeholder={spacePlaceholder} bind:value={space} />
|
||||
{/if}
|
||||
<span class="antiCard-header__divider">›</span>
|
||||
{/if}
|
||||
<span class="antiCard-header__title"><Label {label} params={labelProps ?? {}} /></span>
|
||||
</div>
|
||||
<div class="buttons-group small-gap">
|
||||
<Button icon={IconClose} kind={'transparent'} on:click={() => { dispatch('close') }} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="antiCard-content"><slot /></div>
|
||||
{#if spaceClass && spaceLabel && spacePlaceholder}
|
||||
{#if (spaceClass && spaceLabel && spacePlaceholder) || $$slots.pool}
|
||||
<div class="antiCard-pool">
|
||||
<div class="antiCard-pool__separator" />
|
||||
<SpaceSelect _class={spaceClass} spaceQuery={spaceQuery} label={spaceLabel} placeholder={spacePlaceholder} bind:value={space} />
|
||||
<slot name="pool" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="antiCard-footer">
|
||||
<Button disabled={!canSave} label={okLabel} kind={'primary'} on:click={() => { okAction(); dispatch('close') }} />
|
||||
<Button label={cancelLabel} on:click={() => { dispatch('close') }} />
|
||||
<div class="antiCard-footer reverse">
|
||||
<div class="buttons-group text-sm flex-no-shrink">
|
||||
{#if createMore !== undefined}
|
||||
<MiniToggle label={presentation.string.CreateMore} bind:on={createMore} />
|
||||
{/if}
|
||||
<Button disabled={!canSave} label={okLabel} kind={'primary'} on:click={() => { okAction(); dispatch('close') }} />
|
||||
</div>
|
||||
<div class="buttons-group small-gap text-sm">
|
||||
<slot name="footer" />
|
||||
{#if $$slots.error}
|
||||
<div class="antiCard-footer__error">
|
||||
<slot name="error" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { getClient } from '../utils'
|
||||
|
||||
import { Label, showPopup, IconFolder } from '@anticrm/ui'
|
||||
import { Label, showPopup, IconFolder, Button } from '@anticrm/ui'
|
||||
import SpacesPopup from './SpacesPopup.svelte'
|
||||
|
||||
import type { Ref, Class, Space, DocumentQuery } from '@anticrm/core'
|
||||
@@ -27,10 +27,8 @@
|
||||
export let label: IntlString
|
||||
export let placeholder: IntlString
|
||||
export let value: Ref<Space> | undefined
|
||||
export let show: boolean = false
|
||||
|
||||
let selected: Space | undefined
|
||||
let btn: HTMLElement
|
||||
|
||||
const client = getClient()
|
||||
|
||||
@@ -39,43 +37,21 @@
|
||||
}
|
||||
|
||||
$: updateSelected(value)
|
||||
|
||||
onMount(() => {
|
||||
if (btn && show) {
|
||||
btn.click()
|
||||
show = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="flex-col cursor-pointer"
|
||||
bind:this={btn}
|
||||
on:click|preventDefault={() => {
|
||||
showPopup(SpacesPopup, { _class, spaceQuery }, btn, (result) => {
|
||||
<Button
|
||||
icon={IconFolder}
|
||||
size={'small'}
|
||||
kind={'no-border'}
|
||||
on:click={(ev) => {
|
||||
showPopup(SpacesPopup, { _class, spaceQuery }, ev.target, (result) => {
|
||||
if (result) {
|
||||
value = result._id
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div class="overflow-label label"><Label {label} /></div>
|
||||
<div class="flex-row-center space">
|
||||
<span class="mr-1"><IconFolder size={'small'} /></span>
|
||||
<span class="overflow-label" class:caption-color={selected} class:content-dark-color={!selected}>
|
||||
{#if selected}
|
||||
{selected.name}
|
||||
{:else}
|
||||
<Label label={placeholder} />
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.label {
|
||||
margin-bottom: .125rem;
|
||||
font-weight: 500;
|
||||
font-size: .75rem;
|
||||
color: var(--theme-content-accent-color);
|
||||
}
|
||||
</style>
|
||||
<span slot="content" class="text-sm">
|
||||
{#if selected}{selected.name}{:else}<Label {label} />{/if}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
@@ -32,6 +32,7 @@ export { default as SpaceSelect } from './components/SpaceSelect.svelte'
|
||||
export { default as UserBox } from './components/UserBox.svelte'
|
||||
export { default as UserBoxList } from './components/UserBoxList.svelte'
|
||||
export { default as UserInfo } from './components/UserInfo.svelte'
|
||||
export { default as UsersPopup } from './components/UsersPopup.svelte'
|
||||
export { connect, versionError } from './connect'
|
||||
export { default } from './plugin'
|
||||
export * from './types'
|
||||
|
||||
@@ -42,7 +42,8 @@ export default plugin(presentationId, {
|
||||
Remove: '' as IntlString,
|
||||
Members: '' as IntlString,
|
||||
Search: '' as IntlString,
|
||||
Unassigned: '' as IntlString
|
||||
Unassigned: '' as IntlString,
|
||||
CreateMore: '' as IntlString
|
||||
},
|
||||
metadata: {
|
||||
RequiredVersion: '' as Metadata<string>
|
||||
|
||||
@@ -244,8 +244,7 @@ p:last-child { margin-block-end: 0; }
|
||||
}
|
||||
|
||||
.gap-1, .gap-1-5, .gap-2 {
|
||||
& > * { margin-right: .25rem; }
|
||||
& > *:last-child { margin-right: 0; }
|
||||
& > *:not(:last-child) { margin-right: .25rem; }
|
||||
&.reverse {
|
||||
flex-direction: row-reverse;
|
||||
& > :last-child { margin-right: .25rem; }
|
||||
@@ -293,6 +292,8 @@ p:last-child { margin-block-end: 0; }
|
||||
.ml-6 { margin-left: 1.5rem; }
|
||||
.ml-8 { margin-left: 2rem; }
|
||||
.ml-10 { margin-left: 2.5rem; }
|
||||
.ml-12 { margin-left: 3rem; }
|
||||
.ml-22 { margin-left: 5.5rem; }
|
||||
.mr-1 { margin-right: .25rem; }
|
||||
.mr-2 { margin-right: .5rem; }
|
||||
.mr-3 { margin-right: .75rem; }
|
||||
@@ -300,6 +301,7 @@ p:last-child { margin-block-end: 0; }
|
||||
.mr-6 { margin-right: 1.5rem; }
|
||||
.mr-8 { margin-right: 2rem; }
|
||||
.mr-10 { margin-right: 2.5rem; }
|
||||
.mt-0-5 { margin-top: .125rem; }
|
||||
.mt-1 { margin-top: .25rem; }
|
||||
.mt-2 { margin-top: .5rem; }
|
||||
.mt-3 { margin-top: .75rem; }
|
||||
@@ -317,10 +319,18 @@ p:last-child { margin-block-end: 0; }
|
||||
.mx-1 { margin: 0 .25rem; }
|
||||
.mx-2 { margin: 0 .5rem; }
|
||||
.mx-3 { margin: 0 .75rem; }
|
||||
.my-4 { margin: 1rem 0; }
|
||||
|
||||
.pl-2 { padding-left: .5rem; }
|
||||
.pl-4 { padding-left: 1rem; }
|
||||
.pr-1 { padding-right: .25rem; }
|
||||
.pr-2 { padding-right: .5rem; }
|
||||
.pr-4 { padding-right: 1rem; }
|
||||
.pr-24 { padding-right: 6rem; }
|
||||
.pt-2 { padding-top: .5rem; }
|
||||
.pt-4 { padding-top: 1rem; }
|
||||
.pb-2 { padding-bottom: .5rem; }
|
||||
.pb-4 { padding-bottom: 1rem; }
|
||||
|
||||
.p-2 { padding: .5rem; }
|
||||
.p-3 { padding: .75rem; }
|
||||
@@ -377,6 +387,7 @@ p:last-child { margin-block-end: 0; }
|
||||
.min-w-4 { min-width: 1rem; }
|
||||
.min-w-9 { min-width: 2.25rem; }
|
||||
.min-h-0 { min-height: 0; }
|
||||
.min-w-min { min-width: min-content; }
|
||||
.clear-mins {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
@@ -540,13 +551,14 @@ a.no-line {
|
||||
.background-bg-accent { background-color: var(--theme-bg-accent-color); }
|
||||
.background-bg-accent-normal { background-color: var(--theme-bg-accent-normal); }
|
||||
|
||||
.content-color { color: var(--theme-content-color); }
|
||||
.content-color { color: var(--content-color); }
|
||||
.content-trans-color { color: var(--theme-content-trans-color); }
|
||||
.content-accent-color { color: var(--theme-content-accent-color); }
|
||||
.content-accent-color { color: var(--accent-color); }
|
||||
.content-dark-color { color: var(--theme-content-dark-color); }
|
||||
.caption-color { color: var(--theme-caption-color); }
|
||||
.caption-color { color: var(--caption-color); }
|
||||
|
||||
.red-color { color: var(--highlight-red); }
|
||||
.error-color { color: var(--error-color); }
|
||||
|
||||
.border-radius-4 { border-radius: 1rem; }
|
||||
.border-radius-3 { border-radius: 0.75rem; }
|
||||
|
||||
@@ -111,10 +111,14 @@
|
||||
}
|
||||
|
||||
.antiCard-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
margin: 0 1rem;
|
||||
height: fit-content;
|
||||
|
||||
& > *:not(:last-child) { margin-bottom: 1rem; }
|
||||
}
|
||||
|
||||
.antiCard-pool {
|
||||
@@ -157,20 +161,25 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
|
||||
& > *:not(:first-child) { margin-left: .375rem; }
|
||||
}
|
||||
|
||||
&__divider, &__title {
|
||||
margin-left: .375rem;
|
||||
font-weight: 400;
|
||||
font-size: .8125rem;
|
||||
}
|
||||
&__divider { color: var(--content-color); }
|
||||
&__title { color: var(--accent-color); }
|
||||
}
|
||||
.antiCard-content { margin: 0 1.125rem; }
|
||||
.antiCard-content { margin: .5rem 1.125rem 1rem; }
|
||||
.antiCard-pool {
|
||||
flex-direction: row;
|
||||
margin: .375rem .75rem .75rem;
|
||||
align-items: center;
|
||||
margin: 0 .75rem .75rem;
|
||||
font-size: .75rem;
|
||||
|
||||
& > *:not(:last-child) { margin-right: .375rem; }
|
||||
}
|
||||
.antiCard-footer {
|
||||
direction: ltr;
|
||||
@@ -182,6 +191,16 @@
|
||||
border-top: 1px solid var(--button-bg-color);
|
||||
|
||||
&.reverse { flex-direction: row-reverse; }
|
||||
&__error {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
margin-left: .375rem;
|
||||
min-width: 0;
|
||||
font-weight: 500;
|
||||
font-size: .75rem;
|
||||
color: var(--system-error-color);
|
||||
&:empty { visibility: hidden; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
scrollbar-color: var(--theme-menu-color) var(--theme-bg-color);
|
||||
scrollbar-width: thin;
|
||||
--font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
|
||||
--timing-shadow: cubic-bezier(0,.65,.35,1);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
||||
@@ -70,6 +70,13 @@
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
.color {
|
||||
margin-right: .75rem;
|
||||
width: .875rem;
|
||||
height: .875rem;
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
border-radius: .25rem;
|
||||
}
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -22,8 +22,9 @@
|
||||
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let labelParams: Record<string, any> = {}
|
||||
export let kind: 'primary' | 'secondary' | 'no-border' | 'transparent' | 'link' | 'dangerous' = 'secondary'
|
||||
export let kind: 'primary' | 'secondary' | 'no-border' | 'transparent' | 'link' | 'link-bordered' | 'dangerous' = 'secondary'
|
||||
export let size: 'small' | 'medium' | 'large' | 'x-large' = 'medium'
|
||||
export let shape: 'circle' | undefined = undefined
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let disabled: boolean = false
|
||||
@@ -31,7 +32,7 @@
|
||||
export let width: string | undefined = undefined
|
||||
export let resetIconSize: boolean = false
|
||||
export let focus: boolean = false
|
||||
export let isCircle: boolean = false
|
||||
export let title: string | undefined = undefined
|
||||
|
||||
export let input: HTMLButtonElement | undefined = undefined
|
||||
|
||||
@@ -49,14 +50,16 @@
|
||||
bind:this={input}
|
||||
class="button {kind} {size} jf-{justify}"
|
||||
class:only-icon={iconOnly}
|
||||
class:border-radius-1={!isCircle}
|
||||
class:border-radius-4={isCircle}
|
||||
class:border-radius-1={shape !== 'circle'}
|
||||
class:border-radius-4={shape === 'circle'}
|
||||
disabled={disabled || loading}
|
||||
style={width ? 'width: ' + width : ''}
|
||||
{title}
|
||||
type={kind === 'primary' ? 'submit' : 'button'}
|
||||
on:click
|
||||
>
|
||||
{#if icon && !loading}
|
||||
<div class="btn-icon"
|
||||
<div class="btn-icon pointer-events-none"
|
||||
class:mr-1={!iconOnly && kind === 'no-border'}
|
||||
class:mr-2={!iconOnly && kind !== 'no-border'}
|
||||
class:resetIconSize
|
||||
@@ -67,11 +70,13 @@
|
||||
{#if loading}
|
||||
<Spinner />
|
||||
{:else}
|
||||
{#if label}
|
||||
<Label {label} params={labelParams}/>
|
||||
{:else if $$slots.content}
|
||||
<slot name="content" />
|
||||
{/if}
|
||||
<span class="overflow-label pointer-events-none">
|
||||
{#if label}
|
||||
<Label {label} params={labelParams} />
|
||||
{:else if $$slots.content}
|
||||
<slot name="content" />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -174,6 +179,16 @@
|
||||
.btn-icon { color: var(--content-color); }
|
||||
}
|
||||
}
|
||||
&.link-bordered {
|
||||
padding: 0 .375rem;
|
||||
color: var(--acctent-color);
|
||||
border-color: var(--button-border-color);
|
||||
&:hover {
|
||||
color: var(--acctent-color);
|
||||
border-color: var(--button-border-hover);
|
||||
.btn-icon { color: var(--accent-color); }
|
||||
}
|
||||
}
|
||||
&.primary {
|
||||
padding: 0 1rem;
|
||||
color: var(--white-color);
|
||||
|
||||
@@ -13,14 +13,26 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let checked: boolean = false
|
||||
export let symbol: 'check' | 'minus' = 'check'
|
||||
export let circle: boolean = false
|
||||
export let primary: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const handleValueChanged = (event: Event) => {
|
||||
const eventTarget = event.target as HTMLInputElement
|
||||
const isChecked = eventTarget.checked
|
||||
|
||||
dispatch('value', isChecked)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<label class="checkbox" class:circle class:primary class:checked>
|
||||
<input class="chBox" type="checkbox" bind:checked on:change />
|
||||
<input class="chBox" type="checkbox" bind:checked on:change={handleValueChanged} />
|
||||
<svg class="checkSVG" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
{#if !circle}
|
||||
<path
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { IntlString } from '@anticrm/platform'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Icon, Label, getPlatformColor } from '..'
|
||||
|
||||
export let placeholder: IntlString | undefined = undefined
|
||||
export let placeholderParam: any | undefined = undefined
|
||||
export let searchable: boolean = false
|
||||
export let value: Array<{id: number | string, color: number, label: string}>
|
||||
|
||||
let search: string = ''
|
||||
|
||||
let phTraslate: string = ''
|
||||
$: if (placeholder) translate(placeholder, placeholderParam ?? {}).then(res => { phTraslate = res })
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="selectPopup">
|
||||
{#if searchable}
|
||||
<div class="header">
|
||||
<input type='text' bind:value={search} placeholder={phTraslate} on:input={(ev) => { }} on:change/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#each value.filter(el => el.label.toLowerCase().includes(search.toLowerCase())) as item}
|
||||
<button class="menu-item" on:click={() => { dispatch('close', item) }}>
|
||||
<div class="color" style="background-color: {getPlatformColor(item.color)}" />
|
||||
<span class="label">{item.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,9 +21,8 @@
|
||||
import Icon from './Icon.svelte'
|
||||
import { showPopup, Button, Tooltip, DropdownPopup } from '..'
|
||||
import type { AnySvelteComponent, ListItem, TooltipAligment } from '../types'
|
||||
import Add from './icons/Add.svelte'
|
||||
|
||||
export let icon: Asset | AnySvelteComponent = Add
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let label: IntlString
|
||||
export let placeholder: IntlString
|
||||
export let items: ListItem[] = []
|
||||
|
||||
@@ -14,24 +14,26 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
|
||||
import { IntlString, Asset } from '@anticrm/platform'
|
||||
import DropdownLabelsPopup from './DropdownLabelsPopup.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import IconUp from './icons/Up.svelte'
|
||||
import IconDown from './icons/Down.svelte'
|
||||
|
||||
import type { DropdownTextItem } from '../types'
|
||||
import { showPopup } from '..'
|
||||
import type { AnySvelteComponent, DropdownTextItem, TooltipAligment } from '../types'
|
||||
import { showPopup, Tooltip, Button, Label } from '..'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import ui from '../plugin'
|
||||
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let label: IntlString
|
||||
export let placeholder: IntlString | undefined = undefined
|
||||
export let placeholder: IntlString | undefined = ui.string.SearchDots
|
||||
export let items: DropdownTextItem[]
|
||||
export let selected: DropdownTextItem['id'] | undefined = undefined
|
||||
|
||||
let btn: HTMLElement
|
||||
export let kind: 'primary' | 'secondary' | 'no-border' | 'transparent' | 'link' | 'dangerous' = 'no-border'
|
||||
export let size: 'small' | 'medium' | 'large' | 'x-large' = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = undefined
|
||||
export let labelDirection: TooltipAligment | undefined = undefined
|
||||
|
||||
let container: HTMLElement
|
||||
let opened: boolean = false
|
||||
let isDisabled = false
|
||||
$: isDisabled = items.length === 0
|
||||
@@ -46,7 +48,33 @@
|
||||
const none = ui.string.None
|
||||
</script>
|
||||
|
||||
<div class="flex-col cursor-pointer"
|
||||
<div bind:this={container} class="min-w-0">
|
||||
<Tooltip label={label} fill={width === '100%'} direction={labelDirection}>
|
||||
<Button
|
||||
{icon}
|
||||
width={width ?? 'min-content'}
|
||||
{size} {kind} {justify}
|
||||
on:click={() => {
|
||||
if (!opened) {
|
||||
opened = true
|
||||
showPopup(DropdownLabelsPopup, { placeholder, items, selected }, container, (result) => {
|
||||
if (result) {
|
||||
selected = result
|
||||
dispatch('selected', result)
|
||||
}
|
||||
opened = false
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span slot="content" style="overflow: hidden">
|
||||
{#if selectedItem}{selectedItem.label}{:else}<Label label={label ?? ui.string.NotSelected} />{/if}
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<!-- <div class="flex-col cursor-pointer"
|
||||
bind:this={btn}
|
||||
on:click|preventDefault={() => {
|
||||
if (!opened) {
|
||||
@@ -87,4 +115,4 @@
|
||||
font-size: .75rem;
|
||||
color: var(--theme-content-accent-color);
|
||||
}
|
||||
</style>
|
||||
</style> -->
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
export let placeholder: IntlString = plugin.string.EditBoxPlaceholder
|
||||
export let placeholderParam: any | undefined = undefined
|
||||
export let format: 'text'| 'password' | 'number' = 'text'
|
||||
export let kind: 'editbox' | 'large-style' = 'editbox'
|
||||
export let kind: 'editbox' | 'large-style' | 'small-style' = 'editbox'
|
||||
export let focus: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -105,6 +105,10 @@
|
||||
font-weight: 500;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
.small-style {
|
||||
font-weight: 400;
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
|
||||
@@ -340,8 +340,8 @@
|
||||
height: 2.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
background-color: var(--theme-bg-color);
|
||||
color: var(--dark-color);
|
||||
background-color: var(--board-bg-color);
|
||||
box-shadow: inset 0 -1px 0 0 var(--theme-bg-focused-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -16,14 +16,12 @@
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Icon, Label } from '..'
|
||||
import { Icon, Label, getPlatformColor } from '..'
|
||||
|
||||
// export let _class: Ref<Class<Space>>
|
||||
// export let spaceQuery: DocumentQuery<Space> | undefined
|
||||
export let placeholder: IntlString | undefined = undefined
|
||||
export let placeholderParam: any | undefined = undefined
|
||||
export let searchable: boolean = false
|
||||
export let value: Array<{id: number | string, icon: Asset, label: IntlString}>
|
||||
export let value: Array<{id: number | string, color: number, label: IntlString}>
|
||||
|
||||
let search: string = ''
|
||||
|
||||
@@ -31,9 +29,6 @@
|
||||
$: if (placeholder) translate(placeholder, placeholderParam ?? {}).then(res => { phTraslate = res })
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
// const query = createQuery()
|
||||
// $: query.query(_class, { ...(spaceQuery ?? {}), name: { $like: '%' + search + '%' } }, result => { objects = result })
|
||||
// afterUpdate(() => { dispatch('update', Date.now()) })
|
||||
</script>
|
||||
|
||||
<div class="selectPopup">
|
||||
@@ -44,10 +39,10 @@
|
||||
{/if}
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#each value.filter(el => el.label.toLowerCase().includes(search.toLowerCase())) as space}
|
||||
<button class="menu-item" on:click={() => { dispatch('close', space.id) }}>
|
||||
<div class="icon"><Icon icon={space.icon} size={'small'} /></div>
|
||||
<span class="label"><Label label={space.label} /></span>
|
||||
{#each value.filter(el => el.label.toLowerCase().includes(search.toLowerCase())) as item}
|
||||
<button class="menu-item" on:click={() => { dispatch('close', item.id) }}>
|
||||
<div class="color" style="background-color: {getPlatformColor(item.color)}" />
|
||||
<span class="label"><Label label={item.label} /></span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -43,6 +43,7 @@ export { default as Tabs } from './components/Tabs.svelte'
|
||||
export { default as ScrollBox } from './components/ScrollBox.svelte'
|
||||
export { default as PopupMenu } from './components/PopupMenu.svelte'
|
||||
export { default as SelectPopup } from './components/SelectPopup.svelte'
|
||||
export { default as ColorPopup } from './components/ColorPopup.svelte'
|
||||
export { default as TextArea } from './components/TextArea.svelte'
|
||||
export { default as Section } from './components/Section.svelte'
|
||||
export { default as DatePickerPopup } from './components/calendar/DatePickerPopup.svelte'
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
{#each members as member}
|
||||
<MemberPresenter value={member} size="large" />
|
||||
{/each}
|
||||
<Button icon={IconAdd} isCircle={true} kind="no-border" size="large" on:click={membersHandler} />
|
||||
<Button icon={IconAdd} shape="circle" kind="no-border" size="large" on:click={membersHandler} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Employee } from '@anticrm/contact'
|
||||
import { Employee, formatName } from '@anticrm/contact'
|
||||
import { getFirstName, getLastName } from '@anticrm/contact'
|
||||
import { ContactPresenter } from '@anticrm/contact-resources'
|
||||
import { Button, showPopup } from '@anticrm/ui'
|
||||
@@ -9,13 +9,15 @@
|
||||
const firstName = getFirstName(value.name)
|
||||
const lastName = getLastName(value.name)
|
||||
const nameLabel = `${firstName?.[0] ?? ''}${lastName?.[0] ?? ''}`.toUpperCase()
|
||||
const formattedName = formatName(value.name)
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<Button
|
||||
{size}
|
||||
kind="no-border"
|
||||
isCircle={true}
|
||||
shape="circle"
|
||||
title={formattedName}
|
||||
on:click={() => {
|
||||
showPopup(ContactPresenter, { value }) // TODO: show proper popup
|
||||
}}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"New": "New",
|
||||
"MarkUnread": "Mark unread",
|
||||
"GetNewReplies": "Get notified about new replies",
|
||||
"TurnOffReplies": "Turn off notifications for replies"
|
||||
"TurnOffReplies": "Turn off notifications for replies",
|
||||
"DeleteMessage": "Delete message"
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
"New": "Новое",
|
||||
"MarkUnread": "Отметить как непрочитанное",
|
||||
"GetNewReplies": "Получать уведомления о новых ответах",
|
||||
"TurnOffReplies": "Выключить уведомления об ответах"
|
||||
"TurnOffReplies": "Выключить уведомления об ответах",
|
||||
"DeleteMessage": "Удалить сообщение"
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,11 @@
|
||||
import { AttachmentList } from '@anticrm/attachment-resources'
|
||||
import type { Message } from '@anticrm/chunter'
|
||||
import { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
|
||||
import { Ref, WithLookup } from '@anticrm/core'
|
||||
import { Ref, WithLookup, getCurrentAccount } from '@anticrm/core'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import { Avatar, getClient, MessageViewer } from '@anticrm/presentation'
|
||||
import { ActionIcon, IconMoreH, Menu, showPopup } from '@anticrm/ui'
|
||||
import { ActionIcon, IconMoreH, Menu, showPopup, getCurrentLocation, navigate } from '@anticrm/ui'
|
||||
import { Action } from '@anticrm/view'
|
||||
import { getActions } from '@anticrm/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@@ -59,6 +59,21 @@
|
||||
action: chunter.actionImpl.SubscribeMessage
|
||||
} as Action)
|
||||
|
||||
async function deleteMessage () {
|
||||
await client.remove(message)
|
||||
const loc = getCurrentLocation()
|
||||
|
||||
if (loc.path[3] === message._id) {
|
||||
loc.path.length = 3
|
||||
navigate(loc)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAction = {
|
||||
label: chunter.string.DeleteMessage,
|
||||
action: deleteMessage
|
||||
}
|
||||
|
||||
const showMenu = async (ev: Event): Promise<void> => {
|
||||
const actions = await getActions(client, message, chunter.class.Message)
|
||||
actions.push(subscribeAction)
|
||||
@@ -73,7 +88,8 @@
|
||||
const impl = await getResource(a.action)
|
||||
await impl(message)
|
||||
}
|
||||
}))
|
||||
})),
|
||||
...(getCurrentAccount()._id === message.createBy ? [deleteAction] : [])
|
||||
]
|
||||
},
|
||||
ev.target as HTMLElement
|
||||
@@ -108,7 +124,7 @@
|
||||
</div>
|
||||
{#if !thread}
|
||||
<div>
|
||||
{#if message.replies}<Replies
|
||||
{#if message.replies?.length}<Replies
|
||||
replies={message.replies}
|
||||
lastReply={message.lastReply}
|
||||
on:click={openThread}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { AttachmentList } from '@anticrm/attachment-resources'
|
||||
import type { ThreadMessage } from '@anticrm/chunter'
|
||||
import { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
|
||||
import { Ref, WithLookup } from '@anticrm/core'
|
||||
import { Ref, WithLookup, getCurrentAccount } from '@anticrm/core'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import { Avatar, getClient, MessageViewer } from '@anticrm/presentation'
|
||||
@@ -53,6 +53,11 @@
|
||||
action: chunter.actionImpl.SubscribeComment
|
||||
} as Action)
|
||||
|
||||
const deleteAction = {
|
||||
label: chunter.string.DeleteMessage,
|
||||
action: async () => await client.removeDoc(message._class, message.space, message._id)
|
||||
}
|
||||
|
||||
const showMenu = async (ev: Event): Promise<void> => {
|
||||
const actions = await getActions(client, message, chunter.class.ThreadMessage)
|
||||
actions.push(subscribeAction)
|
||||
@@ -67,7 +72,8 @@
|
||||
const impl = await getResource(a.action)
|
||||
await impl(message)
|
||||
}
|
||||
}))
|
||||
})),
|
||||
...(getCurrentAccount()._id === message.createBy ? [deleteAction] : [])
|
||||
]
|
||||
},
|
||||
ev.target as HTMLElement
|
||||
|
||||
@@ -50,6 +50,7 @@ export default mergeIds(chunterId, chunter, {
|
||||
LastReply: '' as IntlString,
|
||||
New: '' as IntlString,
|
||||
GetNewReplies: '' as IntlString,
|
||||
TurnOffReplies: '' as IntlString
|
||||
TurnOffReplies: '' as IntlString,
|
||||
DeleteMessage: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
@@ -51,4 +51,8 @@
|
||||
<path d="M12,0C5.4,0,0,5.4,0,12c0,6.6,5.4,12,12,12c6.6,0,12-5.4,12-12C24,5.4,18.6,0,12,0z M12,22.6 C6.2,22.6,1.4,17.8,1.4,12C1.4,6.2,6.2,1.4,12,1.4c5.8,0,10.6,4.7,10.6,10.6C22.6,17.8,17.8,22.6,12,22.6z"/>
|
||||
<path d="M18.6,16.2c-0.3-0.9-0.8-1.6-1.5-2.2c-0.9-0.7-2-1.1-3.1-1.1h-1.5h-0.9h-1.5c-1.1,0-2.2,0.4-3.1,1.1 c-0.7,0.6-1.2,1.3-1.5,2.2l-0.2,0.6c-0.1,0.4,0.1,0.8,0.5,0.9c0.1,0,0.1,0,0.2,0c0.3,0,0.6-0.2,0.7-0.5L7,16.7 c0.2-0.6,0.5-1.1,1-1.5c0.6-0.5,1.4-0.8,2.2-0.8h1.5h0.9h1.5c0.8,0,1.6,0.3,2.2,0.8c0.5,0.4,0.8,0.9,1,1.5l0.2,0.6 c0.1,0.3,0.4,0.5,0.7,0.5c0.1,0,0.1,0,0.2,0c0.4-0.1,0.6-0.5,0.5-0.9L18.6,16.2z"/>
|
||||
</symbol>
|
||||
<symbol id="social-edit" viewBox="0 0 24 24">
|
||||
<path d="M12.8,16.5c0-0.4-0.3-0.8-0.8-0.8c-1.2,0-2.3-0.1-3.5-0.2c-0.3-2.3-0.3-4.6,0-6.9c2.3-0.3,4.6-0.3,6.9,0 c0.1,1.1,0.2,2.3,0.2,3.5c0,0.4,0.3,0.8,0.8,0.8c0.4,0,0.8-0.3,0.8-0.8c0-1.1-0.1-2.2-0.2-3.2c1.3,0.2,2.5,0.5,3.8,0.9 c0.1,0,0.1,0,0.2,0c0.2,0.7,0.3,1.5,0.3,2.3c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8c0-5.9-4.8-10.8-10.8-10.8S1.2,6.1,1.2,12 S6.1,22.8,12,22.8c0.4,0,0.8-0.3,0.8-0.8s-0.3-0.8-0.8-0.8c-0.8,0-1.5-0.1-2.3-0.3c0-0.1,0-0.1,0-0.2C9.3,19.5,9,18.3,8.8,17 c1.1,0.1,2.1,0.2,3.2,0.2C12.4,17.2,12.8,16.9,12.8,16.5z M7,15.2c-1.3-0.2-2.5-0.5-3.8-0.9c-0.1,0-0.1,0-0.2,0 c-0.2-0.7-0.3-1.5-0.3-2.3c0-0.8,0.1-1.5,0.3-2.3c0.1,0,0.1,0,0.2,0C4.5,9.3,5.7,9,7,8.8C6.8,10.9,6.8,13.1,7,15.2z M20.3,8 c-1.2-0.3-2.3-0.6-3.5-0.8C16.6,6,16.3,4.8,16,3.7C17.9,4.6,19.4,6.1,20.3,8z M14.3,3c0,0.1,0,0.1,0,0.2C14.7,4.5,15,5.7,15.2,7 c-2.1-0.2-4.3-0.2-6.5,0C9,5.7,9.3,4.5,9.7,3.2c0-0.1,0-0.1,0-0.2c0.7-0.2,1.5-0.3,2.3-0.3C12.8,2.8,13.5,2.9,14.3,3z M7.9,3.7 C7.9,3.7,7.9,3.7,7.9,3.7C7.6,4.9,7.4,6,7.2,7.2C6,7.4,4.8,7.7,3.7,8C4.5,6.2,6,4.6,7.9,3.7z M8,20.2C7.9,20.3,7.9,20.3,8,20.2 c-1.9-0.9-3.4-2.3-4.3-4.1c0,0,0-0.1,0-0.1c1.1,0.3,2.3,0.6,3.5,0.8C7.4,18,7.6,19.1,8,20.2z" />
|
||||
<path d="M20.1,14.4c-0.5,0-1,0.3-1.5,0.8l-3.5,3.5c-0.2,0.2-0.5,0.7-0.5,1l-0.2,1.3c-0.1,0.5,0.1,0.9,0.4,1.2 c0.3,0.3,0.6,0.4,1,0.4c0.1,0,0.2,0,0.2,0l1.3-0.2c0.3,0,0.8-0.3,1-0.5l3.5-3.5c0.5-0.5,0.7-1,0.8-1.5c0.1-0.6-0.2-1.2-0.8-1.8 S20.8,14.4,20.1,14.4z M20.3,15.9C20.3,15.9,20.3,15.9,20.3,15.9c0.2,0,0.5,0.2,0.6,0.4c0.2,0.2,0.4,0.4,0.3,0.6 c0,0.1-0.1,0.3-0.3,0.5l-0.2,0.2c-0.5-0.2-0.9-0.7-1.1-1.2l0.2-0.2C19.9,16,20.1,15.9,20.3,15.9z M17.4,21c0,0-0.1,0.1-0.2,0.1 l-1.2,0.2l0.2-1.2c0,0,0-0.1,0.1-0.2l2.3-2.3c0.3,0.5,0.7,0.9,1.2,1.2L17.4,21z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 9.5 KiB |
@@ -32,6 +32,7 @@ loadMetadata(contact.icon, {
|
||||
GitHub: `${icons}#github`,
|
||||
Edit: `${icons}#edit`,
|
||||
Person: `${icons}#person`,
|
||||
Company: `${icons}#company`
|
||||
Company: `${icons}#company`,
|
||||
SocialEdit: `${icons}#social-edit`
|
||||
})
|
||||
addStringsLoader(contactId, async (lang: string) => await import(`../lang/${lang}.json`))
|
||||
|
||||
@@ -45,18 +45,20 @@
|
||||
<span class="ml-2"><Label label={presentation.string.AddSocialLinks} /></span>
|
||||
</div>
|
||||
{:else}
|
||||
<ChannelsView value={channels} size={'small'} {integrations} on:click />
|
||||
<div id="channels-edit" class="ml-1">
|
||||
<CircleButton
|
||||
icon={contact.icon.Edit}
|
||||
size={'small'}
|
||||
selected
|
||||
on:click={(ev) =>
|
||||
showPopup(contact.component.SocialEditor, { values: channels }, ev.target, (result) => {
|
||||
if (result !== undefined) {
|
||||
dispatch('change', result)
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<div class="flex-row-center min-w-min">
|
||||
<ChannelsView value={channels} size={'small'} {integrations} on:click />
|
||||
<div id="channels-edit" class="ml-1">
|
||||
<CircleButton
|
||||
icon={contact.icon.Edit}
|
||||
size={'small'}
|
||||
selected
|
||||
on:click={(ev) =>
|
||||
showPopup(contact.component.SocialEditor, { values: channels }, ev.target, (result) => {
|
||||
if (result !== undefined) {
|
||||
dispatch('change', result)
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -124,6 +124,8 @@
|
||||
<style lang="scss">
|
||||
.channels {
|
||||
display: grid;
|
||||
width: min-content;
|
||||
|
||||
&.one { display: block; }
|
||||
&.short {
|
||||
grid-template-columns: repeat(4, min-content);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
label: cl.label,
|
||||
action: async () => {
|
||||
closePopup()
|
||||
showPopup(f.component, {}, targetElement)
|
||||
showPopup(f.component, {}, 'top')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,13 +18,14 @@
|
||||
|
||||
import { getClient, Card } from '@anticrm/presentation'
|
||||
|
||||
import { EditBox } from '@anticrm/ui'
|
||||
import { EditBox, Button, showPopup, IconAdd } from '@anticrm/ui'
|
||||
|
||||
import { Channel, Organization } from '@anticrm/contact'
|
||||
import contact from '../plugin'
|
||||
import Company from './icons/Company.svelte'
|
||||
import { AttachedData, generateId } from '@anticrm/core'
|
||||
import Channels from './Channels.svelte'
|
||||
import ChannelsView from './ChannelsView.svelte'
|
||||
|
||||
export function canClose (): boolean {
|
||||
return object.name === ''
|
||||
@@ -63,31 +64,28 @@
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<div class="flex-row-center">
|
||||
<div class="mr-4 flex-center logo">
|
||||
<Company size={'large'} />
|
||||
</div>
|
||||
<div class="flex-col">
|
||||
<div class="fs-title">
|
||||
<EditBox placeholder={contact.string.OrganizationNamePlaceholder} maxWidth="11rem" bind:value={object.name} focus />
|
||||
</div>
|
||||
<div class="flex-row-center clear-mins">
|
||||
<div class="mr-3">
|
||||
<Button icon={Company} size={'medium'} kind={'link-bordered'} disabled />
|
||||
</div>
|
||||
<EditBox
|
||||
placeholder={contact.string.OrganizationNamePlaceholder}
|
||||
bind:value={object.name}
|
||||
maxWidth={'37.5rem'} kind={'large-style'} focus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-row-center channels">
|
||||
<Channels bind:channels={channels} on:change={(e) => { channels = e.detail }} />
|
||||
</div>
|
||||
{#if channels.length > 0}
|
||||
<ChannelsView value={channels} size={'small'} on:click />
|
||||
{/if}
|
||||
<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
|
||||
})
|
||||
}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.logo {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
color: var(--primary-button-color);
|
||||
background-color: var(--primary-button-enabled);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.channels {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
import { getClient, Card, EditableAvatar } from '@anticrm/presentation'
|
||||
|
||||
import attachment from '@anticrm/attachment'
|
||||
import { EditBox, IconInfo, Label } from '@anticrm/ui'
|
||||
import { EditBox, IconInfo, Label, Button, showPopup } from '@anticrm/ui'
|
||||
|
||||
import { Channel, combineName, findPerson, Person } from '@anticrm/contact'
|
||||
import contact from '../plugin'
|
||||
import Channels from './Channels.svelte'
|
||||
import ChannelsView from './ChannelsView.svelte'
|
||||
import PersonPresenter from './PersonPresenter.svelte'
|
||||
|
||||
let firstName = ''
|
||||
@@ -93,58 +93,41 @@
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
{#if matches.length > 0}
|
||||
<div class="flex-row update-container ERROR">
|
||||
<div class="flex mb-2">
|
||||
<svelte:fragment slot="error">
|
||||
{#if matches.length > 0}
|
||||
<div class="flex-row-center error-color">
|
||||
<IconInfo size={'small'} />
|
||||
<div class="text-sm ml-2 overflow-label">
|
||||
<span class="text-sm overflow-label ml-2">
|
||||
<Label label={contact.string.PersonAlreadyExists} />
|
||||
</div>
|
||||
</span>
|
||||
<div class="ml-4"><PersonPresenter value={matches[0]} /></div>
|
||||
</div>
|
||||
<PersonPresenter value={matches[0]} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<div class="flex-row-center">
|
||||
<div class="mr-4">
|
||||
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone} on:remove={removeAvatar} />
|
||||
</div>
|
||||
<div class="flex-col">
|
||||
<div class="fs-title">
|
||||
<EditBox placeholder={contact.string.PersonFirstNamePlaceholder} maxWidth="12rem" bind:value={firstName} focus />
|
||||
</div>
|
||||
<div class="fs-title mb-1">
|
||||
<EditBox placeholder={contact.string.PersonLastNamePlaceholder} maxWidth="12rem" bind:value={lastName} />
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<EditBox placeholder={contact.string.PersonLocationPlaceholder} maxWidth="12rem" bind:value={object.city} />
|
||||
<EditBox placeholder={contact.string.PersonFirstNamePlaceholder} bind:value={firstName} kind={'large-style'} maxWidth={'32rem'} focus />
|
||||
<EditBox placeholder={contact.string.PersonLastNamePlaceholder} bind:value={lastName} kind={'large-style'} maxWidth={'32rem'} />
|
||||
<div class="mt-1">
|
||||
<EditBox placeholder={contact.string.PersonLocationPlaceholder} bind:value={object.city} kind={'small-style'} maxWidth={'32rem'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-row-center mt-5">
|
||||
<Channels
|
||||
bind:channels
|
||||
on:change={(e) => {
|
||||
channels = e.detail
|
||||
}}
|
||||
{#if channels.length > 0}
|
||||
<div class="ml-22"><ChannelsView value={channels} size={'small'} on:click /></div>
|
||||
{/if}
|
||||
<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
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.update-container {
|
||||
margin-left: -1rem;
|
||||
margin-right: -1rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
user-select: none;
|
||||
font-size: 14px;
|
||||
|
||||
color: var(--theme-content-color);
|
||||
&.ERROR { color: var(--system-error-color); }
|
||||
|
||||
border: 1px dashed var(--theme-zone-border);
|
||||
border-radius: 0.5rem;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -170,7 +170,8 @@ const contactPlugin = plugin(contactId, {
|
||||
GitHub: '' as Asset,
|
||||
Edit: '' as Asset,
|
||||
Person: '' as Asset,
|
||||
Company: '' as Asset
|
||||
Company: '' as Asset,
|
||||
SocialEdit: '' as Asset
|
||||
},
|
||||
space: {
|
||||
Employee: '' as Ref<Space>,
|
||||
|
||||
@@ -28,43 +28,49 @@
|
||||
export let object: WithLookup<Lead>
|
||||
export let dragged: boolean
|
||||
|
||||
function showMenu(ev?: Event): void {
|
||||
function showMenu (ev?: Event): void {
|
||||
showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement)
|
||||
}
|
||||
|
||||
function showLead() {
|
||||
function showLead () {
|
||||
showPanel(view.component.EditDoc, object._id, object._class, 'full')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-between mb-4">
|
||||
<div class="flex-col">
|
||||
<div class="fs-title cursor-pointer" on:click={showLead}>{object.title}</div>
|
||||
</div>
|
||||
<div class="flex-row-center">
|
||||
<div class="mr-2">
|
||||
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
|
||||
<div class="flex-col pt-2 pb-2 pr-4 pl-4">
|
||||
<div class="flex-between mb-4">
|
||||
<div class="flex-col">
|
||||
<div class="fs-title cursor-pointer" on:click={showLead}>{object.title}</div>
|
||||
</div>
|
||||
<div class="flex-row-center">
|
||||
<div class="mr-2">
|
||||
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
|
||||
</div>
|
||||
<ActionIcon
|
||||
label={lead.string.More}
|
||||
action={(evt) => {
|
||||
showMenu(evt)
|
||||
}}
|
||||
icon={IconMoreH}
|
||||
size={'small'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-between">
|
||||
{#if object.$lookup?.attachedTo}
|
||||
<ContactPresenter value={object.$lookup.attachedTo} />
|
||||
{/if}
|
||||
<div class="flex-row-center">
|
||||
{#if (object.attachments ?? 0) > 0}
|
||||
<div class="step-lr75">
|
||||
<AttachmentsPresenter value={object} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if (object.comments ?? 0) > 0}
|
||||
<div class="step-lr75">
|
||||
<CommentsPresenter value={object} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<ActionIcon
|
||||
label={lead.string.More}
|
||||
action={(evt) => {
|
||||
showMenu(evt)
|
||||
}}
|
||||
icon={IconMoreH}
|
||||
size={'small'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-between">
|
||||
{#if object.$lookup?.attachedTo}
|
||||
<ContactPresenter value={object.$lookup.attachedTo} />
|
||||
{/if}
|
||||
<div class="flex-row-center">
|
||||
{#if (object.attachments ?? 0) > 0}
|
||||
<div class="step-lr75"><AttachmentsPresenter value={object} /></div>
|
||||
{/if}
|
||||
{#if (object.comments ?? 0) > 0}
|
||||
<div class="step-lr75"><CommentsPresenter value={object} /></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"OpinionShortLabel": "OPE",
|
||||
"ReviewShortLabel": "RVE",
|
||||
"StartDate": "Дата начала",
|
||||
"DueDate": "Дата конца",
|
||||
"DueDate": "Дата окончания",
|
||||
"ReviewCategoryTitle":"Категория",
|
||||
"Verdict": "Вердикт",
|
||||
"OpinionSave": "Сохранить",
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
import FileDuo from './icons/FileDuo.svelte'
|
||||
import chunter from '@anticrm/chunter'
|
||||
import attachment from '@anticrm/attachment'
|
||||
import { Applicant } from '@anticrm/recruit'
|
||||
import { BuildModelKey } from '@anticrm/view'
|
||||
import { Applicant } from '@anticrm/recruit'
|
||||
import { BuildModelKey } from '@anticrm/view'
|
||||
|
||||
export let objectId: Ref<Doc>
|
||||
// export let space: Ref<Space>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
}
|
||||
|
||||
function showCreateDialog (ev: Event) {
|
||||
showPopup(CreateApplication, { }, ev.target as HTMLElement)
|
||||
showPopup(CreateApplication, { }, 'top')
|
||||
}
|
||||
|
||||
function updateResultQuery (search: string): void {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import recruit from '../plugin'
|
||||
|
||||
export let candidate: Candidate
|
||||
export let disabled: boolean = false
|
||||
|
||||
let channels: Channel[] = []
|
||||
const channelsQuery = createQuery()
|
||||
@@ -43,8 +44,8 @@
|
||||
<div class="label uppercase"><Label label={recruit.string.Candidate} /></div>
|
||||
<Avatar avatar={candidate.avatar} size={'large'} />
|
||||
{#if candidate}
|
||||
<div class="name lines-limit-2 over-underline" on:click={() => {
|
||||
showPanel(view.component.EditDoc, candidate._id, candidate._class, 'full')
|
||||
<div class="name lines-limit-2" class:over-underline={!disabled} on:click={() => {
|
||||
if (!disabled) showPanel(view.component.EditDoc, candidate._id, candidate._class, 'full')
|
||||
}}>{formatName(candidate.name)}</div>
|
||||
<div class="description lines-limit-2">{candidate.title ?? ''}</div>
|
||||
<div class="description overflow-label">{candidate.city ?? ''}</div>
|
||||
@@ -64,15 +65,16 @@
|
||||
.card-container {
|
||||
padding: 1rem 1.5rem 1.25rem;
|
||||
background-color: var(--board-card-bg-color);
|
||||
border: 1px solid var(--board-card-bg-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: .5rem;
|
||||
transition-property: box-shadow, background-color;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-property: box-shadow, background-color, border-color;
|
||||
transition-timing-function: var(--timing-shadow);
|
||||
transition-duration: .15s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--board-card-bg-hover);
|
||||
box-shadow: var(--popup-shadow);
|
||||
border-color: var(--button-border-color);
|
||||
box-shadow: rgb(0 0 0 / 15%) 0px 4px 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
const tableDescriptor = client.findOne<Viewlet>(view.class.Viewlet, { attachTo: recruit.mixin.Candidate, descriptor: view.viewlet.Table })
|
||||
|
||||
function showCreateDialog (ev: Event) {
|
||||
showPopup(CreateCandidate, { space: recruit.space.CandidatesPublic }, ev.target as HTMLElement)
|
||||
showPopup(CreateCandidate, { space: recruit.space.CandidatesPublic }, 'top')
|
||||
}
|
||||
|
||||
let category: Ref<TagCategory> | undefined = undefined
|
||||
|
||||
@@ -17,13 +17,16 @@
|
||||
import contact from '@anticrm/contact'
|
||||
import { Account, Class, Client, Doc, generateId, Ref, SortingOrder } from '@anticrm/core'
|
||||
import { getResource, OK, Resource, Severity, Status } from '@anticrm/platform'
|
||||
import { Card, getClient, UserBox } from '@anticrm/presentation'
|
||||
import type { Applicant, Candidate } from '@anticrm/recruit'
|
||||
import { Card, getClient, UserBox, createQuery, AttributeEditor } from '@anticrm/presentation'
|
||||
import type { Applicant, Candidate, Vacancy } from '@anticrm/recruit'
|
||||
import task, { calcRank, SpaceWithStates, State } from '@anticrm/task'
|
||||
import { Grid, Status as StatusControl } from '@anticrm/ui'
|
||||
import ui, { Status as StatusControl, Label, Button, ColorPopup, showPopup, getPlatformColor } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../plugin'
|
||||
import CandidateCard from './CandidateCard.svelte'
|
||||
import VacancyCard from './VacancyCard.svelte'
|
||||
import ExpandRightDouble from './icons/ExpandRightDouble.svelte'
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let candidate: Ref<Candidate>
|
||||
@@ -58,7 +61,7 @@
|
||||
}
|
||||
|
||||
async function createApplication () {
|
||||
const state = await client.findOne(task.class.State, { space: doc.space })
|
||||
const state = await client.findOne(task.class.State, { space: doc.space, _id: selectedState._id })
|
||||
if (state === undefined) {
|
||||
throw new Error(`create application: state not found space:${doc.space}`)
|
||||
}
|
||||
@@ -115,6 +118,35 @@
|
||||
}
|
||||
|
||||
$: validate(doc, doc._class)
|
||||
|
||||
let selectedVacancy: Vacancy
|
||||
let selectedCandidate: Person
|
||||
const vacancyQuery = createQuery()
|
||||
$: if (doc.space !== undefined) {
|
||||
vacancyQuery.query(recruit.class.Vacancy, { _id: doc.space }, (result) => {
|
||||
selectedVacancy = result[0]
|
||||
})
|
||||
}
|
||||
const candidateQuery = createQuery()
|
||||
$: if (doc.attachedTo !== undefined) {
|
||||
candidateQuery.query(contact.class.Person, { _id: doc.attachedTo as Ref<Person> }, (result) => {
|
||||
selectedCandidate = result[0]
|
||||
})
|
||||
}
|
||||
let states: Array<{id: number | string, color: number, label: string}> = []
|
||||
let selectedState: State
|
||||
const statesQuery = createQuery()
|
||||
$: if (doc.space !== undefined) {
|
||||
statesQuery.query(
|
||||
task.class.State,
|
||||
{ space: doc.space },
|
||||
(res) => {
|
||||
states = res.map(s => { return { id: s._id, label: s.title, color: s.color} })
|
||||
selectedState = res.filter(s => s._id === doc.state)[0] ?? res[0]
|
||||
},
|
||||
{ sort: { rank: SortingOrder.Ascending } }
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
@@ -125,20 +157,38 @@
|
||||
spaceQuery={{ archived: false }}
|
||||
spaceLabel={recruit.string.Vacancy}
|
||||
spacePlaceholder={recruit.string.SelectVacancy}
|
||||
createMore={false}
|
||||
bind:space={doc.space}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<StatusControl slot="error" {status} />
|
||||
<Grid column={1} rowGap={1}>
|
||||
<!-- <div class="flex-between mt-2 mb-2">
|
||||
<div class="card" class:empty={!selectedCandidate}>
|
||||
{#if selectedCandidate}
|
||||
<CandidateCard candidate={selectedCandidate} disabled />
|
||||
{:else}
|
||||
<Label label={recruit.status.CandidateRequired} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="arrows"><ExpandRightDouble /></div>
|
||||
<div class="card" class:empty={!selectedVacancy}>
|
||||
{#if selectedVacancy}
|
||||
<VacancyCard vacancy={selectedVacancy} disabled />
|
||||
{:else}
|
||||
<Label label={recruit.status.VacancyRequired} />
|
||||
{/if}
|
||||
</div>
|
||||
</div> -->
|
||||
<svelte:fragment slot="pool">
|
||||
{#if !preserveCandidate}
|
||||
<UserBox
|
||||
_class={contact.class.Person}
|
||||
label={recruit.string.Candidate}
|
||||
placeholder={recruit.string.Candidates}
|
||||
bind:value={doc.attachedTo}
|
||||
kind={'link'} size={'x-large'} justify={'left'} width={'100%'} labelDirection={'left'}
|
||||
kind={'no-border'} size={'small'}
|
||||
/>
|
||||
{/if}
|
||||
<UserBox
|
||||
@@ -148,7 +198,67 @@
|
||||
bind:value={doc.assignee}
|
||||
allowDeselect
|
||||
titleDeselect={recruit.string.UnAssignRecruiter}
|
||||
kind={'link'} size={'x-large'} justify={'left'} width={'100%'} labelDirection={'left'}
|
||||
kind={'no-border'} size={'small'}
|
||||
/>
|
||||
</Grid>
|
||||
{#if states && doc.space}
|
||||
<Button
|
||||
width="min-content"
|
||||
size="small"
|
||||
kind="no-border"
|
||||
on:click={(ev) => {
|
||||
showPopup(
|
||||
ColorPopup,
|
||||
{ value: states, searchable: true, placeholder: ui.string.SearchDots },
|
||||
ev.currentTarget,
|
||||
(result) => {
|
||||
if (result && result.id !== doc.state) {
|
||||
doc.state = result.id
|
||||
selectedState = result
|
||||
}
|
||||
}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div slot="content" class="flex-row-center">
|
||||
{#if selectedState}
|
||||
<div class="color" style="background-color: {getPlatformColor(selectedState.color)}" />
|
||||
<span class="label overflow-label">{selectedState.title}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.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; }
|
||||
.color {
|
||||
margin-right: .375rem;
|
||||
width: .875rem;
|
||||
height: .875rem;
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
border-radius: .25rem;
|
||||
}
|
||||
.label {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import attachment from '@anticrm/attachment'
|
||||
import contact, { Channel, ChannelProvider, combineName, findPerson, Person } from '@anticrm/contact'
|
||||
import { Channels } from '@anticrm/contact-resources'
|
||||
import { ChannelsView } from '@anticrm/contact-resources'
|
||||
import PersonPresenter from '@anticrm/contact-resources/src/components/PersonPresenter.svelte'
|
||||
import {
|
||||
Account,
|
||||
@@ -50,7 +50,9 @@
|
||||
Label,
|
||||
Link,
|
||||
showPopup,
|
||||
Spinner
|
||||
Spinner,
|
||||
Button,
|
||||
IconAttachment
|
||||
} from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../plugin'
|
||||
@@ -397,91 +399,24 @@
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
{#if matches.length > 0}
|
||||
<div class="flex-row update-container ERROR">
|
||||
<div class="flex mb-2">
|
||||
<IconInfo size={'small'} />
|
||||
<div class="text-sm ml-2 overflow-label">
|
||||
<Label label={contact.string.PersonAlreadyExists} />
|
||||
</div>
|
||||
</div>
|
||||
<PersonPresenter value={matches[0]} />
|
||||
</div>
|
||||
{/if}
|
||||
<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-col">
|
||||
<div class="fs-title">
|
||||
<EditBox placeholder={recruit.string.PersonFirstNamePlaceholder} maxWidth="10rem" bind:value={firstName} focus />
|
||||
</div>
|
||||
<div class="fs-title mb-1">
|
||||
<EditBox placeholder={recruit.string.PersonLastNamePlaceholder} maxWidth="10rem" bind:value={lastName} />
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<EditBox placeholder={recruit.string.Title} maxWidth="10rem" bind:value={object.title} />
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<EditBox placeholder={recruit.string.Location} maxWidth="10rem" bind:value={object.city} />
|
||||
<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'} />
|
||||
<div class="mt-1">
|
||||
<EditBox placeholder={recruit.string.Title} bind:value={object.title} kind={'small-style'} maxWidth={'32rem'} />
|
||||
</div>
|
||||
<EditBox placeholder={recruit.string.Location} bind:value={object.city} kind={'small-style'} maxWidth={'32rem'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-row-center channels">
|
||||
<Channels
|
||||
bind:channels
|
||||
on:change={(e) => {
|
||||
channels = e.detail
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="separator" />
|
||||
<div class="flex-col locations">
|
||||
<span><Label label={recruit.string.WorkLocationPreferences} /></span>
|
||||
<div class="row"><Label label={recruit.string.Onsite} /><YesNo bind:value={object.onsite} /></div>
|
||||
<div class="row"><Label label={recruit.string.Remote} /><YesNo bind:value={object.remote} /></div>
|
||||
</div>
|
||||
<div class="separator" />
|
||||
<div class="flex-col locations">
|
||||
<span><Label label={recruit.string.SkillsLabel} /></span>
|
||||
{#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}
|
||||
@@ -495,60 +430,48 @@
|
||||
/>
|
||||
</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 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}
|
||||
{#if matches.length > 0}
|
||||
<div class="flex-row-center error-color">
|
||||
<IconInfo size={'small'} />
|
||||
<span class="text-sm overflow-label ml-2">
|
||||
<Label label={contact.string.PersonAlreadyExists} />
|
||||
</span>
|
||||
<div class="ml-4"><PersonPresenter value={matches[0]} /></div>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.channels {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.locations {
|
||||
span {
|
||||
margin-bottom: 0.125rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-content-accent-color);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 0.75rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 1rem 0;
|
||||
height: 1px;
|
||||
background-color: var(--theme-card-divider);
|
||||
}
|
||||
|
||||
.resume {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--theme-zone-bg);
|
||||
border: 1px dashed var(--theme-zone-border);
|
||||
border-radius: 0.5rem;
|
||||
backdrop-filter: blur(10px);
|
||||
&.solid {
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
.update-container {
|
||||
margin-left: -1rem;
|
||||
margin-right: -1rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
user-select: none;
|
||||
font-size: 14px;
|
||||
|
||||
color: var(--theme-content-color);
|
||||
&.ERROR { color: var(--system-error-color); }
|
||||
|
||||
border: 1px dashed var(--theme-zone-border);
|
||||
border-radius: 0.5rem;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<script lang="ts">
|
||||
import { Organization } from '@anticrm/contact'
|
||||
import core, { Ref } from '@anticrm/core'
|
||||
import { getClient,SpaceCreateCard } from '@anticrm/presentation'
|
||||
import task, { createKanban,KanbanTemplate } from '@anticrm/task'
|
||||
import { Component,EditBox,Grid } from '@anticrm/ui'
|
||||
import { getClient, Card } from '@anticrm/presentation'
|
||||
import task, { createKanban, KanbanTemplate } from '@anticrm/task'
|
||||
import { Component, EditBox, Button } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../plugin'
|
||||
import { OrganizationSelector } from '@anticrm/contact-resources'
|
||||
@@ -55,24 +55,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<SpaceCreateCard
|
||||
<Card
|
||||
label={recruit.string.CreateVacancy}
|
||||
okAction={createVacancy}
|
||||
canSave={!!name}
|
||||
on:close={() => { dispatch('close') }}
|
||||
>
|
||||
<Grid column={1} rowGap={1.5}>
|
||||
<EditBox label={recruit.string.VacancyName} bind:value={name} icon={Vacancy} placeholder={recruit.string.VacancyPlaceholder} maxWidth={'16rem'} focus/>
|
||||
<div class="flex-row-center clear-mins">
|
||||
<div class="mr-3">
|
||||
<Button icon={Vacancy} size={'medium'} kind={'link-bordered'} disabled />
|
||||
</div>
|
||||
<EditBox
|
||||
bind:value={name}
|
||||
placeholder={recruit.string.VacancyPlaceholder}
|
||||
maxWidth={'37.5rem'} kind={'large-style'} focus
|
||||
/>
|
||||
</div>
|
||||
<svelte:fragment slot="pool">
|
||||
<OrganizationSelector
|
||||
bind:value={company} label={recruit.string.Company}
|
||||
kind={'link'} size={'x-large'} justify={'left'} width={'100%'} labelDirection={'left'}
|
||||
kind={'no-border'} size={'small'}
|
||||
/>
|
||||
|
||||
<Component is={task.component.KanbanTemplateSelector} props={{
|
||||
folders: [recruit.space.VacancyTemplates],
|
||||
template: templateId
|
||||
}} on:change={(evt) => {
|
||||
templateId = evt.detail
|
||||
}}/>
|
||||
</Grid>
|
||||
</SpaceCreateCard>
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
@@ -64,7 +64,8 @@
|
||||
<style lang="scss">
|
||||
.card {
|
||||
align-self: stretch;
|
||||
width: calc(50% - 3.5rem);
|
||||
width: calc(50% - 3rem);
|
||||
min-height: 16rem;
|
||||
}
|
||||
.arrows { width: 4rem; }
|
||||
</style>
|
||||
|
||||
@@ -36,43 +36,47 @@
|
||||
$: doneTasks = todoItems.filter((it) => it.done)
|
||||
</script>
|
||||
|
||||
<div class="flex-between mb-3">
|
||||
<div class="flex-row-center">
|
||||
<Avatar avatar={object.$lookup?.attachedTo?.avatar} size={'medium'} />
|
||||
<div class="flex-grow flex-col min-w-0 ml-2">
|
||||
<div class="fs-title over-underline lines-limit-2" on:click={showCandidate}>
|
||||
{formatName(object.$lookup?.attachedTo?.name ?? '')}
|
||||
<div class="flex-col pt-2 pb-2 pr-4 pl-4">
|
||||
<div class="flex-between mb-3">
|
||||
<div class="flex-row-center">
|
||||
<Avatar avatar={object.$lookup?.attachedTo?.avatar} size={'medium'} />
|
||||
<div class="flex-grow flex-col min-w-0 ml-2">
|
||||
<div class="fs-title over-underline lines-limit-2" on:click={showCandidate}>
|
||||
{formatName(object.$lookup?.attachedTo?.name ?? '')}
|
||||
</div>
|
||||
<div class="text-sm lines-limit-2">{object.$lookup?.attachedTo?.title ?? ''}</div>
|
||||
</div>
|
||||
<div class="text-sm lines-limit-2">{object.$lookup?.attachedTo?.title ?? ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool mr-1 flex-row-center">
|
||||
{#if !dragged}
|
||||
<div class="mr-2">
|
||||
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
|
||||
</div>
|
||||
<ActionIcon label={undefined} icon={IconMoreH} size={'small'} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-between">
|
||||
<div class="flex-row-center">
|
||||
<div class="sm-tool-icon step-lr75">
|
||||
<ApplicationPresenter value={object} />
|
||||
{#if todoItems.length > 0}
|
||||
<Tooltip label={task.string.TodoItems} component={task.component.TodoItemsPopup} props={{ value: object }}>
|
||||
<div class="ml-2">
|
||||
( {doneTasks?.length}/ {todoItems.length} )
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="tool mr-1 flex-row-center">
|
||||
{#if !dragged}
|
||||
<div class="mr-2">
|
||||
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
|
||||
</div>
|
||||
<ActionIcon label={undefined} icon={IconMoreH} size={'small'} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if (object.attachments ?? 0) > 0}
|
||||
<div class="step-lr75"><AttachmentsPresenter value={object} /></div>
|
||||
{/if}
|
||||
{#if (object.comments ?? 0) > 0}
|
||||
<div class="step-lr75"><CommentsPresenter value={object} /></div>
|
||||
{/if}
|
||||
</div>
|
||||
<Avatar avatar={object.$lookup?.assignee?.avatar} size={'x-small'} />
|
||||
<div class="flex-between">
|
||||
<div class="flex-row-center">
|
||||
<div class="sm-tool-icon step-lr75">
|
||||
<ApplicationPresenter value={object} />
|
||||
{#if todoItems.length > 0}
|
||||
<Tooltip label={task.string.TodoItems} component={task.component.TodoItemsPopup} props={{ value: object }}>
|
||||
<div class="ml-2">( {doneTasks?.length}/ {todoItems.length} )</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{#if (object.attachments ?? 0) > 0}
|
||||
<div class="step-lr75">
|
||||
<AttachmentsPresenter value={object} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if (object.comments ?? 0) > 0}
|
||||
<div class="step-lr75">
|
||||
<CommentsPresenter value={object} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Avatar avatar={object.$lookup?.assignee?.avatar} size={'x-small'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
}
|
||||
|
||||
function showCreateDialog (ev: Event) {
|
||||
showPopup(CreateVacancy, { space: recruit.space.CandidatesPublic }, ev.target as HTMLElement)
|
||||
showPopup(CreateVacancy, { space: recruit.space.CandidatesPublic }, 'top')
|
||||
}
|
||||
const applicationSorting = (a:Doc, b:Doc) => ((applications?.get(b._id as Ref<Vacancy>)?.count ?? 0) - (applications?.get(a._id as Ref<Vacancy>)?.count ?? 0)) ?? 0
|
||||
const modifiedSorting = (a:Doc, b:Doc) => ((applications?.get(b._id as Ref<Vacancy>)?.modifiedOn ?? 0) - (applications?.get(a._id as Ref<Vacancy>)?.modifiedOn ?? 0)) ?? 0
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import { Ref } from '@anticrm/core'
|
||||
|
||||
export let vacancy: Vacancy
|
||||
export let disabled: boolean = false
|
||||
let company: Organization | undefined
|
||||
|
||||
$: getOrganization(vacancy?.company)
|
||||
@@ -43,14 +44,16 @@
|
||||
<VacancyIcon size={'large'} />
|
||||
</div>
|
||||
{#if vacancy}
|
||||
<div class="name lines-limit-2 over-underline" on:click={() => {
|
||||
closeTooltip()
|
||||
closePopup()
|
||||
closePanel()
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[2] = vacancy._id
|
||||
loc.path.length = 3
|
||||
navigate(loc)
|
||||
<div class="name lines-limit-2" class:over-underline={!disabled} on:click={() => {
|
||||
if (!disabled) {
|
||||
closeTooltip()
|
||||
closePopup()
|
||||
closePanel()
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[2] = vacancy._id
|
||||
loc.path.length = 3
|
||||
navigate(loc)
|
||||
}
|
||||
}}>{vacancy.name}</div>
|
||||
{#if company}
|
||||
<span class="label">{company.name}</span>
|
||||
@@ -63,15 +66,16 @@
|
||||
.card-container {
|
||||
padding: 1rem 1.5rem 1.25rem;
|
||||
background-color: var(--board-card-bg-color);
|
||||
border: 1px solid var(--board-card-bg-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: .5rem;
|
||||
transition-property: box-shadow, background-color;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-property: box-shadow, background-color, border-color;
|
||||
transition-timing-function: var(--timing-shadow);
|
||||
transition-duration: .15s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--board-card-bg-hover);
|
||||
box-shadow: var(--popup-shadow);
|
||||
border-color: var(--button-border-color);
|
||||
box-shadow: rgb(0 0 0 / 15%) 0px 4px 8px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
|
||||
@@ -18,56 +18,82 @@
|
||||
import { Label } from '@anticrm/ui'
|
||||
import recruit from '../plugin'
|
||||
|
||||
export let label: IntlString
|
||||
export let value: boolean | undefined
|
||||
|
||||
function getLabel(value: boolean | undefined): IntlString {
|
||||
if (value === true) return recruit.string.Yes
|
||||
if (value === false) return recruit.string.No
|
||||
return recruit.string.NA
|
||||
}
|
||||
export let disabled: boolean = false
|
||||
</script>
|
||||
|
||||
<div class="flex-row-center yesno-container" class:yes={value === true} class:no={value === false} on:click={() => {
|
||||
<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
|
||||
}}>
|
||||
<svg class="yesno-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<circle fill="#fff" cx="8" cy="8" r="6"/>
|
||||
{#if value === true}
|
||||
<polygon fill="#60B96E" 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="#F06C63" 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="#77818E" 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 class="label"><Label label={getLabel(value)} /></div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<style lang="scss">
|
||||
.yesno-container {
|
||||
padding: .25rem .5rem .25rem .4rem;
|
||||
max-width: fit-content;
|
||||
border-radius: 1.25rem;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding: 0 .25rem 0 .5rem;
|
||||
min-width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5rem;
|
||||
white-space: nowrap;
|
||||
color: var(--accent-color);
|
||||
background-color: var(--button-bg-color);
|
||||
border: 1px solid transparent;
|
||||
border-radius: .25rem;
|
||||
box-shadow: var(--button-shadow);
|
||||
transition-property: border, background-color, color, box-shadow;
|
||||
transition-duration: .15s;
|
||||
|
||||
background-color: var(--grayscale-grey-03);
|
||||
&.yes { background-color: #60B96E; }
|
||||
&.no { background-color: #F06C63; }
|
||||
.btn-icon {
|
||||
color: var(--content-color);
|
||||
transition: color .15s;
|
||||
pointer-events: none;
|
||||
}
|
||||
&:hover {
|
||||
color: var(--caption-color);
|
||||
background-color: var(--button-bg-hover);
|
||||
transition-duration: 0;
|
||||
|
||||
.btn-icon { color: var(--caption-color); }
|
||||
}
|
||||
&:disabled {
|
||||
color: var(--content-color);
|
||||
background-color: #30323655;
|
||||
|
||||
.label {
|
||||
width: 1.4rem;
|
||||
margin-left: .25rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
font-size: .625rem;
|
||||
color: #FFF;
|
||||
&:hover {
|
||||
color: var(--content-color);
|
||||
.btn-icon { color: var(--content-color); }
|
||||
}
|
||||
}
|
||||
}
|
||||
.yesno-svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
|
||||
.circle {
|
||||
fill: var(--grayscale-grey-03);
|
||||
&.yes { fill: #60B96E; }
|
||||
&.no { fill: #F06C63; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import type { Candidate, Review } from '@anticrm/recruit'
|
||||
import task, { SpaceWithStates } from '@anticrm/task'
|
||||
import { StyledTextBox } from '@anticrm/text-editor'
|
||||
import { DateRangePicker, Grid, Status as StatusControl, EditBox, Row } from '@anticrm/ui'
|
||||
import { DateRangePicker, Grid, Status as StatusControl, EditBox, Row, DateRangePresenter } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../../plugin'
|
||||
@@ -39,8 +39,8 @@
|
||||
|
||||
let title: string = ''
|
||||
let description: string = ''
|
||||
let startDate: number = Date.now()
|
||||
let dueDate: number = Date.now()
|
||||
let startDate: number | null = null
|
||||
let dueDate: number | null = null
|
||||
let location: string = ''
|
||||
let company: Ref<Organization> | undefined = undefined
|
||||
|
||||
@@ -103,8 +103,8 @@
|
||||
|
||||
await client.addCollection(recruit.class.Review, doc.space, doc.attachedTo, doc.attachedToClass, 'reviews', {
|
||||
number: (incResult as any).object.sequence,
|
||||
date: startDate ?? null,
|
||||
dueDate: dueDate ?? null,
|
||||
date: startDate ?? 0,
|
||||
dueDate: dueDate ?? 0,
|
||||
description,
|
||||
verdict: '',
|
||||
title,
|
||||
@@ -143,7 +143,6 @@
|
||||
</script>
|
||||
|
||||
<Card
|
||||
size={'medium'}
|
||||
label={recruit.string.CreateReviewParams}
|
||||
labelProps={{ label: spaceLabel }}
|
||||
okAction={createReview}
|
||||
@@ -158,61 +157,47 @@
|
||||
}}
|
||||
>
|
||||
<StatusControl slot="error" {status} />
|
||||
|
||||
<Grid column={2} rowGap={1.75}>
|
||||
<EditBox label={recruit.string.Title} icon={recruit.icon.Review} bind:value={title} maxWidth={'13rem'} />
|
||||
<EditBox
|
||||
placeholder={recruit.string.Title} bind:value={title}
|
||||
maxWidth={'37.5rem'} kind={'large-style'} focus
|
||||
/>
|
||||
<EditBox
|
||||
placeholder={recruit.string.Location} bind:value={location}
|
||||
maxWidth={'37.5rem'} kind={'small-style'}
|
||||
/>
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={doc.participants}
|
||||
title={calendar.string.Participants}
|
||||
on:open={(evt) => {
|
||||
doc.participants = [...(doc.participants ?? []), evt.detail._id]
|
||||
}}
|
||||
on:delete={(evt) => {
|
||||
doc.participants = doc.participants?.filter((it) => it !== evt.detail._id) ?? [currentUser.employee]
|
||||
}}
|
||||
noItems={calendar.string.NoParticipants}
|
||||
/>
|
||||
<StyledTextBox
|
||||
emphasized
|
||||
showButtons={false}
|
||||
bind:content={description}
|
||||
label={recruit.string.Description}
|
||||
alwaysEdit
|
||||
placeholder={recruit.string.AddDescription}
|
||||
/>
|
||||
<svelte:fragment slot="pool">
|
||||
{#if !preserveCandidate}
|
||||
<UserBox
|
||||
_class={contact.class.Person}
|
||||
label={recruit.string.Candidate}
|
||||
placeholder={recruit.string.Candidates}
|
||||
bind:value={doc.attachedTo}
|
||||
kind={'link'}
|
||||
size={'x-large'}
|
||||
justify={'left'}
|
||||
width={'100%'}
|
||||
labelDirection={'left'}
|
||||
_class={contact.class.Person} bind:value={doc.attachedTo}
|
||||
label={recruit.string.Candidate} placeholder={recruit.string.Candidates}
|
||||
kind={'no-border'} size={'small'}
|
||||
/>
|
||||
{:else}
|
||||
<div />
|
||||
{/if}
|
||||
<EditBox label={recruit.string.Location} icon={recruit.icon.Location} bind:value={location} maxWidth={'13rem'} />
|
||||
<OrganizationSelector
|
||||
bind:value={company}
|
||||
label={recruit.string.Company}
|
||||
kind={'link'}
|
||||
size={'x-large'}
|
||||
justify={'left'}
|
||||
width={'100%'}
|
||||
labelDirection={'left'}
|
||||
bind:value={company} label={recruit.string.Company}
|
||||
kind={'no-border'} size={'small'}
|
||||
/>
|
||||
<DateRangePicker title={recruit.string.StartDate} bind:value={startDate} withTime on:change={updateStart} />
|
||||
<DateRangePicker title={recruit.string.DueDate} bind:value={dueDate} withTime />
|
||||
|
||||
<Row>
|
||||
<UserBoxList
|
||||
_class={contact.class.Employee}
|
||||
items={doc.participants}
|
||||
title={calendar.string.Participants}
|
||||
on:open={(evt) => {
|
||||
doc.participants = [...(doc.participants ?? []), evt.detail._id]
|
||||
}}
|
||||
on:delete={(evt) => {
|
||||
doc.participants = doc.participants?.filter((it) => it !== evt.detail._id) ?? [currentUser.employee]
|
||||
}}
|
||||
noItems={calendar.string.NoParticipants}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<StyledTextBox
|
||||
emphasized
|
||||
showButtons={false}
|
||||
bind:content={description}
|
||||
label={recruit.string.Description}
|
||||
alwaysEdit
|
||||
placeholder={recruit.string.AddDescription}
|
||||
/>
|
||||
</Row>
|
||||
</Grid>
|
||||
<DateRangePresenter bind:value={startDate} labelNull={recruit.string.StartDate} withTime editable on:change={updateStart} />
|
||||
<DateRangePresenter bind:value={dueDate} labelNull={recruit.string.DueDate} withTime editable />
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="tags" viewBox="0 0 16 16">
|
||||
<path d="M8,8.5c1.7,0,3.2-1.4,3.2-3.2S9.7,2.2,8,2.2c-1.7,0-3.2,1.4-3.2,3.2S6.3,8.5,8,8.5z M8,3.2c1.2,0,2.2,1,2.2,2.2 S9.2,7.5,8,7.5s-2.2-1-2.2-2.2S6.8,3.2,8,3.2z"/>
|
||||
<path d="M4.2,11.2c-0.9,0.6-1.5,1.3-1.8,2.2c-0.1,0.3,0,0.5,0.3,0.6c0.3,0.1,0.5,0,0.6-0.3c0.2-0.6,0.7-1.3,1.4-1.7 c0.7-0.5,1.6-0.8,2.6-0.9c0.1,0,0.1,0,0.2,0c0.2-0.4,0.6-0.8,1-1c-0.4,0-0.8,0-1.2,0C6.1,10.3,5.1,10.7,4.2,11.2z"/>
|
||||
<path d="M14.7,11.5h-2.2V9.3c0-0.3-0.2-0.5-0.5-0.5s-0.5,0.2-0.5,0.5v2.2H9.3c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h2.2v2.2 c0,0.3,0.2,0.5,0.5,0.5s0.5-0.2,0.5-0.5v-2.2h2.2c0.3,0,0.5-0.2,0.5-0.5S14.9,11.5,14.7,11.5z"/>
|
||||
<symbol id="tags" viewBox="0 0 24 24">
|
||||
<path d="M22.8,8.7c0-0.3-0.2-0.6-0.5-0.7L11.8,3.8c-0.2-0.1-0.4-0.1-0.6,0L0.7,8C0.4,8.1,0.2,8.4,0.2,8.7 s0.2,0.6,0.5,0.7L5,11.1v6c0,0.2,0.1,0.4,0.2,0.5l0.5-0.5c-0.5,0.5-0.5,0.5-0.5,0.5l0,0l0,0l0,0l0,0c0,0,0.1,0.1,0.1,0.1 c0.1,0.1,0.2,0.2,0.3,0.3C6,18.3,6.5,18.6,7,19c1.1,0.6,2.7,1.3,4.5,1.3c1.8,0,3.4-0.7,4.5-1.3c0.5-0.3,1-0.6,1.3-0.9 c0.2-0.1,0.3-0.2,0.3-0.3c0,0,0.1-0.1,0.1-0.1l0,0l0,0l0,0l0,0l-0.5-0.5c0.5,0.5,0.5,0.5,0.5,0.5c0.1-0.1,0.2-0.3,0.2-0.5v-6 l3.3-1.3v3.6c0,0.4,0.3,0.8,0.8,0.8c0.4,0,0.8-0.3,0.8-0.8L22.8,8.7C22.8,8.7,22.8,8.7,22.8,8.7C22.8,8.7,22.8,8.7,22.8,8.7z M16.5,16.8c0,0-0.1,0.1-0.1,0.1c-0.3,0.2-0.6,0.5-1.1,0.8c-0.9,0.6-2.2,1.1-3.7,1.1c-1.5,0-2.8-0.5-3.7-1.1 c-0.5-0.3-0.8-0.5-1.1-0.8c-0.1,0-0.1-0.1-0.1-0.1v-5.1l4.7,1.9c0.2,0.1,0.4,0.1,0.6,0l4.7-1.9V16.8z M11.5,12.1L3,8.7l8.5-3.4 L20,8.7L11.5,12.1z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 746 B After Width: | Height: | Size: 956 B |
@@ -16,7 +16,7 @@
|
||||
import { Class, Data, Doc, generateId, Ref } from '@anticrm/core'
|
||||
import { Card, createQuery, getClient } from '@anticrm/presentation'
|
||||
import { findTagCategory, TagCategory, TagElement } from '@anticrm/tags'
|
||||
import { DropdownLabels, EditBox, getColorNumberByText, getPlatformColor, showPopup } from '@anticrm/ui'
|
||||
import { DropdownLabels, EditBox, getColorNumberByText, getPlatformColor, showPopup, Button, IconFolder } from '@anticrm/ui'
|
||||
import { DropdownTextItem } from '@anticrm/ui/src/types'
|
||||
import { ColorsPopup } from '@anticrm/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@@ -88,58 +88,65 @@
|
||||
okAction={createTagElenent}
|
||||
canSave={title.length > 0}
|
||||
space={tags.space.Tags}
|
||||
createMore={false}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<div class="flex-row-center">
|
||||
<div class="flex-col">
|
||||
<div class="fs-title flex-row-center">
|
||||
<div
|
||||
class="color"
|
||||
style={getTagStyle(getPlatformColor(color))}
|
||||
on:click={(evt) => {
|
||||
showPopup(ColorsPopup, {}, evt.target, (col) => {
|
||||
if (col != null) {
|
||||
color = col
|
||||
colorSet = true
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<div class="flex-row-top clear-mins">
|
||||
<div class="mr-3">
|
||||
<Button
|
||||
size={'medium'}
|
||||
kind={'link-bordered'}
|
||||
on:click={(evt) => {
|
||||
showPopup(ColorsPopup, {}, evt.target, (col) => {
|
||||
if (col != null) {
|
||||
color = col
|
||||
colorSet = true
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="color" style={getTagStyle(getPlatformColor(color))} />
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex-col mt-0-5">
|
||||
<EditBox
|
||||
bind:value={title}
|
||||
placeholder={tags.string.TagName}
|
||||
placeholderParam={{ word: keyTitle }}
|
||||
maxWidth={'35rem'} kind={'large-style'} focus
|
||||
/>
|
||||
<div class="mt-2">
|
||||
<EditBox
|
||||
focus
|
||||
placeholder={tags.string.TagName}
|
||||
placeholderParam={{ word: keyTitle }}
|
||||
maxWidth={'16rem'}
|
||||
bind:value={title}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-sm mt-4">
|
||||
<EditBox placeholder={tags.string.TagDescriptionPlaceholder} maxWidth={'17.75rem'} bind:value={description} />
|
||||
</div>
|
||||
|
||||
<div class="text-sm mt-4">
|
||||
<DropdownLabels
|
||||
label={tags.string.CategoryLabel}
|
||||
bind:selected={category}
|
||||
items={categoryItems}
|
||||
on:selected={() => {
|
||||
categoryWasSet = true
|
||||
}}
|
||||
bind:value={description}
|
||||
placeholder={tags.string.TagDescriptionPlaceholder}
|
||||
maxWidth={'35rem'} kind={'small-style'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="pool">
|
||||
<div class="ml-12">
|
||||
<DropdownLabels
|
||||
icon={IconFolder}
|
||||
label={tags.string.CategoryLabel}
|
||||
bind:selected={category}
|
||||
items={categoryItems}
|
||||
on:selected={() => {
|
||||
categoryWasSet = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.color {
|
||||
margin-right: 0.75rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
}
|
||||
|
||||
function showCreateDialog (ev: Event) {
|
||||
showPopup(CreateTagElement, { targetClass, keyTitle }, ev.target as HTMLElement)
|
||||
showPopup(CreateTagElement, { targetClass, keyTitle }, 'top')
|
||||
}
|
||||
const opt: FindOptions<TagElement> = {
|
||||
lookup: {
|
||||
|
||||
@@ -34,41 +34,45 @@
|
||||
$: doneTasks = todoItems.filter((it) => it.done)
|
||||
</script>
|
||||
|
||||
<div class="flex-between mb-2">
|
||||
<div class="flex">
|
||||
<TaskPresenter value={object} />
|
||||
{#if todoItems.length > 0}
|
||||
<Tooltip label={task.string.TodoItems} component={task.component.TodoItemsPopup} props={{ value: object }}>
|
||||
<div class="ml-2">
|
||||
( {doneTasks?.length}/ {todoItems.length} )
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-row-center">
|
||||
<div class="mr-2">
|
||||
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
|
||||
<div class="flex-col pt-2 pb-2 pr-4 pl-4">
|
||||
<div class="flex-between mb-2">
|
||||
<div class="flex">
|
||||
<TaskPresenter value={object} />
|
||||
{#if todoItems.length > 0}
|
||||
<Tooltip label={task.string.TodoItems} component={task.component.TodoItemsPopup} props={{ value: object }}>
|
||||
<div class="ml-2">( {doneTasks?.length}/ {todoItems.length} )</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
<ActionIcon
|
||||
label={task.string.More}
|
||||
action={(evt) => {
|
||||
showMenu(evt)
|
||||
}}
|
||||
icon={IconMoreH}
|
||||
size={'small'}
|
||||
/>
|
||||
<div class="flex-row-center">
|
||||
<div class="mr-2">
|
||||
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
|
||||
</div>
|
||||
<ActionIcon
|
||||
label={task.string.More}
|
||||
action={(evt) => {
|
||||
showMenu(evt)
|
||||
}}
|
||||
icon={IconMoreH}
|
||||
size={'small'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption-color mb-3 lines-limit-4">{object.name}</div>
|
||||
<!-- <div class="text-sm lines-limit-2">{object.description}</div> -->
|
||||
<div class="flex-between">
|
||||
<div class="flex-row-center">
|
||||
{#if (object.attachments ?? 0) > 0}
|
||||
<div class="step-lr75">
|
||||
<AttachmentsPresenter value={object} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if (object.comments ?? 0) > 0}
|
||||
<div class="step-lr75">
|
||||
<CommentsPresenter value={object} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Avatar avatar={object.$lookup?.assignee?.avatar} size={'x-small'} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="caption-color mb-3 lines-limit-4">{object.name}</div>
|
||||
<!-- <div class="text-sm lines-limit-2">{object.description}</div> -->
|
||||
<div class="flex-between">
|
||||
<div class="flex-row-center">
|
||||
{#if (object.attachments ?? 0) > 0}
|
||||
<div class="step-lr75"><AttachmentsPresenter value={object} /></div>
|
||||
{/if}
|
||||
{#if (object.comments ?? 0) > 0}
|
||||
<div class="step-lr75"><CommentsPresenter value={object} /></div>
|
||||
{/if}
|
||||
</div>
|
||||
<Avatar avatar={object.$lookup?.assignee?.avatar} size={'x-small'} />
|
||||
</div>
|
||||
|
||||
@@ -41,4 +41,4 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownLabels {items} bind:selected={selectedItem} label={plugin.string.States} />
|
||||
<DropdownLabels {items} icon={task.icon.ManageStatuses} bind:selected={selectedItem} label={plugin.string.States} />
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import { Ref } from '@anticrm/core'
|
||||
import task, { State } from '@anticrm/task'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { showPopup } from '@anticrm/ui'
|
||||
import { showPopup, Button, SelectPopup } from '@anticrm/ui'
|
||||
import StatePresenter from './StatePresenter.svelte'
|
||||
import StatesPopup from './StatesPopup.svelte'
|
||||
|
||||
@@ -35,19 +35,30 @@
|
||||
</script>
|
||||
|
||||
{#if state}
|
||||
<div class="flex-row-center cursor-pointer" bind:this={container}
|
||||
on:click|preventDefault={() => {
|
||||
<Button
|
||||
width="min-content"
|
||||
size="small"
|
||||
kind="no-border"
|
||||
on:click={(ev) => {
|
||||
if (!opened) {
|
||||
opened = true
|
||||
showPopup(StatesPopup, { space: state.space }, container, (result) => {
|
||||
if (result && result._id !== value) {
|
||||
value = result._id
|
||||
onChange(value)
|
||||
}
|
||||
opened = false
|
||||
})
|
||||
showPopup(
|
||||
StatesPopup,
|
||||
{ space: state.space },
|
||||
ev.currentTarget,
|
||||
(result) => {
|
||||
if (result && result._id !== value) {
|
||||
value = result._id
|
||||
onChange(value)
|
||||
}
|
||||
opened = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}} >
|
||||
<StatePresenter value={state} />
|
||||
</div>
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
<StatePresenter value={state} />
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -19,22 +19,19 @@
|
||||
|
||||
export let value: State
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="overflow-label state-container" style="background-color: {getPlatformColor(value.color)}">
|
||||
{value.title}
|
||||
<div class="flex-row-center">
|
||||
<div class="state-container" style="background-color: {getPlatformColor(value.color)}" />
|
||||
<span class="overflow-label">{value.title ?? ''}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.state-container {
|
||||
padding: 0.25rem 0.5rem;
|
||||
width: 6.25rem;
|
||||
max-width: 6.25rem;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 0.625rem;
|
||||
color: #fff;
|
||||
margin-right: .5rem;
|
||||
width: .875rem;
|
||||
height: .875rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
"AddIssue": "Add Issue",
|
||||
"NewIssue": "New issue",
|
||||
"SaveIssue": "Save issue",
|
||||
"CreateMore": "Create more",
|
||||
"Todo": "Todo",
|
||||
"InProgress": "In Progress",
|
||||
"Done": "Done",
|
||||
"Canceled": "Canceled",
|
||||
"SetPriority": "Set priority\u2026",
|
||||
"SetStatus": "Set status\u2026",
|
||||
"Priority": "Priority",
|
||||
"NoPriority": "No priority",
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
"CreateTeam": "Create team",
|
||||
"NewIssue": "Новая задача",
|
||||
"SaveIssue": "Сохранить задачу",
|
||||
"CreateMore": "Создать еще",
|
||||
"Todo": "Todo",
|
||||
"InProgress": "В работе",
|
||||
"Done": "Выполнено",
|
||||
"Canceled": "Отменено",
|
||||
"SetPriority": "Установить приоритет\u2026",
|
||||
"SetStatus": "Установить статус\u2026",
|
||||
"Priority": "Установить приоритет",
|
||||
"NoPriority": "Нет приоритета",
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<!--
|
||||
// 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">
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { Ref, Class, Space, DocumentQuery } from '@anticrm/core'
|
||||
|
||||
import { Button, Label, IconAttachment, IconExpand, IconClose, MiniToggle } from '@anticrm/ui'
|
||||
import SpaceSelect from './SpaceSelect.svelte'
|
||||
import presentation from '@anticrm/presentation'
|
||||
import tracker from '../plugin'
|
||||
|
||||
export let spaceClass: Ref<Class<Space>> | undefined = undefined
|
||||
export let space: Ref<Space> | undefined = undefined
|
||||
export let spaceQuery: DocumentQuery<Space> | undefined = { archived: false }
|
||||
export let spaceLabel: IntlString | undefined = undefined
|
||||
export let spacePlaceholder: IntlString | undefined = undefined
|
||||
export let label: IntlString
|
||||
export let labelProps: any | undefined = undefined
|
||||
export let icon: Asset | undefined = undefined
|
||||
export let okAction: () => void
|
||||
export let canSave: boolean = false
|
||||
export let createMore: boolean | undefined = undefined
|
||||
|
||||
export let okLabel: IntlString = presentation.string.Create
|
||||
export let cancelLabel: IntlString = presentation.string.Cancel
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<form class="antiCard dialog" on:submit|preventDefault={ () => {} }>
|
||||
<div class="antiCard-header">
|
||||
<div class="antiCard-header__title-wrap">
|
||||
<Button icon={icon} label={presentation.string.Save} size={'small'} kind={'no-border'} disabled on:click={() => { }} />
|
||||
<span class="antiCard-header__divider">›</span>
|
||||
<span class="antiCard-header__title"><Label {label} params={labelProps ?? {}} /></span>
|
||||
</div>
|
||||
<div class="buttons-group small-gap">
|
||||
<Button icon={IconExpand} kind={'transparent'} on:click={() => { }} />
|
||||
<Button icon={IconClose} kind={'transparent'} on:click={() => { dispatch('close') }} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="antiCard-content"><slot /></div>
|
||||
{#if spaceClass && spaceLabel && spacePlaceholder}
|
||||
<div class="antiCard-pool">
|
||||
<slot name="pool" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="antiCard-footer reverse">
|
||||
<div class="buttons-group text-sm">
|
||||
{#if createMore !== undefined}
|
||||
<MiniToggle label={tracker.string.CreateMore} bind:on={createMore} />
|
||||
{/if}
|
||||
<Button disabled={!canSave} label={okLabel} kind={'primary'} on:click={() => { okAction(); dispatch('close') }} />
|
||||
</div>
|
||||
<Button icon={IconAttachment} kind={'transparent'} on:click={() => { }} />
|
||||
</div>
|
||||
</form>
|
||||
@@ -16,14 +16,13 @@
|
||||
import contact, { Employee } from '@anticrm/contact'
|
||||
import core, { Data, generateId, Ref, SortingOrder } from '@anticrm/core'
|
||||
import { Asset, IntlString } from '@anticrm/platform'
|
||||
import { getClient, UserBox } from '@anticrm/presentation'
|
||||
import presentation, { getClient, UserBox, Card } from '@anticrm/presentation'
|
||||
import { Issue, IssuePriority, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { StyledTextBox } from '@anticrm/text-editor'
|
||||
import { EditBox, Button, showPopup, DatePresenter, SelectPopup } from '@anticrm/ui'
|
||||
import { EditBox, Button, showPopup, DatePresenter, SelectPopup, IconAttachment } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../plugin'
|
||||
import { calcRank } from '../utils'
|
||||
import Card from './Card.svelte'
|
||||
import StatusSelector from './StatusSelector.svelte'
|
||||
import PrioritySelector from './PrioritySelector.svelte'
|
||||
|
||||
@@ -91,14 +90,30 @@
|
||||
{ icon: tracker.icon.DueDate, label: tracker.string.DueDate },
|
||||
{ icon: tracker.icon.Parent, label: tracker.string.Parent }
|
||||
]
|
||||
|
||||
let issueDate: number | null = null
|
||||
|
||||
const handlePriorityChanged = (newPriority: IssuePriority | undefined) => {
|
||||
if (newPriority === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
object.priority = newPriority
|
||||
}
|
||||
|
||||
const handleStatusChanged = (newStatus: IssueStatus | undefined) => {
|
||||
if (newStatus === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
object.status = newStatus
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- canSave: object.title.length > 0 && _space != null -->
|
||||
<Card
|
||||
label={tracker.string.NewIssue}
|
||||
okAction={createIssue}
|
||||
icon={tracker.icon.Home}
|
||||
canSave={true}
|
||||
okLabel={tracker.string.SaveIssue}
|
||||
spaceClass={tracker.class.Team}
|
||||
@@ -110,6 +125,9 @@
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="space">
|
||||
<Button icon={tracker.icon.Home} label={presentation.string.Save} size={'small'} kind={'no-border'} disabled on:click={() => { }} />
|
||||
</svelte:fragment>
|
||||
<EditBox
|
||||
bind:value={object.title}
|
||||
placeholder={tracker.string.IssueTitlePlaceholder}
|
||||
@@ -117,17 +135,15 @@
|
||||
kind={'large-style'}
|
||||
focus
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<StyledTextBox
|
||||
alwaysEdit
|
||||
showButtons={false}
|
||||
bind:content={object.description}
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
<div slot="pool" class="flex-row-center text-sm gap-1-5">
|
||||
<StatusSelector bind:status={object.status} />
|
||||
<PrioritySelector bind:priority={object.priority} />
|
||||
<StyledTextBox
|
||||
alwaysEdit
|
||||
showButtons={false}
|
||||
bind:content={object.description}
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
/>
|
||||
<svelte:fragment slot="pool">
|
||||
<StatusSelector bind:status={object.status} onStatusChange={handleStatusChanged} />
|
||||
<PrioritySelector bind:priority={object.priority} onPriorityChange={handlePriorityChanged} />
|
||||
<UserBox
|
||||
_class={contact.class.Employee}
|
||||
label={tracker.string.Assignee}
|
||||
@@ -160,5 +176,8 @@
|
||||
showPopup(SelectPopup, { value: moreActions }, ev.currentTarget)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button icon={IconAttachment} kind={'transparent'} on:click={() => { }} />
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
@@ -14,11 +14,14 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IssuePriority } from '@anticrm/tracker'
|
||||
import { Button, showPopup, SelectPopup } from '@anticrm/ui'
|
||||
import { Button, showPopup, SelectPopup, Icon, Label } from '@anticrm/ui'
|
||||
import { issuePriorities } from '../utils'
|
||||
import tracker from '../plugin'
|
||||
|
||||
export let priority: IssuePriority
|
||||
export let kind: 'button' | 'icon' = 'button'
|
||||
export let shouldShowLabel: boolean = true
|
||||
export let onPriorityChange: ((newPriority: IssuePriority | undefined) => void) | undefined = undefined
|
||||
|
||||
const prioritiesInfo = [
|
||||
IssuePriority.NoPriority,
|
||||
@@ -26,27 +29,36 @@
|
||||
IssuePriority.High,
|
||||
IssuePriority.Medium,
|
||||
IssuePriority.Low
|
||||
].map((s) => ({ id: s, ...issuePriorities[s] }))
|
||||
].map((p) => ({ id: p, ...issuePriorities[p] }))
|
||||
|
||||
function handlePriorityChange (id: any) {
|
||||
if (id !== undefined) {
|
||||
priority = id
|
||||
}
|
||||
const handlePriorityEditorOpened = (event: Event) => {
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{ value: prioritiesInfo, placeholder: tracker.string.SetPriority, searchable: true },
|
||||
event.currentTarget,
|
||||
onPriorityChange
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button
|
||||
label={issuePriorities[priority].label}
|
||||
icon={issuePriorities[priority].icon}
|
||||
width="min-content"
|
||||
size="small"
|
||||
kind="no-border"
|
||||
on:click={(ev) => {
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{ value: prioritiesInfo, placeholder: tracker.string.SetStatus },
|
||||
ev.currentTarget,
|
||||
handlePriorityChange
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{#if kind === 'button'}
|
||||
<Button
|
||||
label={shouldShowLabel ? issuePriorities[priority].label : undefined}
|
||||
icon={issuePriorities[priority].icon}
|
||||
width="min-content"
|
||||
size="small"
|
||||
kind="no-border"
|
||||
on:click={handlePriorityEditorOpened}
|
||||
/>
|
||||
{:else if kind === 'icon'}
|
||||
<div class="flex-presenter" on:click={handlePriorityEditorOpened}>
|
||||
<div class="icon">
|
||||
<Icon icon={issuePriorities[priority].icon} size={'small'} />
|
||||
</div>
|
||||
{#if shouldShowLabel}
|
||||
<div class="label nowrap">
|
||||
<Label label={issuePriorities[priority].label} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -14,11 +14,14 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IssueStatus } from '@anticrm/tracker'
|
||||
import { Button, showPopup, SelectPopup } from '@anticrm/ui'
|
||||
import { Button, Icon, Label, showPopup, SelectPopup } from '@anticrm/ui'
|
||||
import { issueStatuses } from '../utils'
|
||||
import tracker from '../plugin'
|
||||
|
||||
export let status: IssueStatus
|
||||
export let kind: 'button' | 'icon' = 'button'
|
||||
export let shouldShowLabel: boolean = true
|
||||
export let onStatusChange: ((newStatus: IssueStatus | undefined) => void) | undefined = undefined
|
||||
|
||||
const statusesInfo = [
|
||||
IssueStatus.Backlog,
|
||||
@@ -28,25 +31,34 @@
|
||||
IssueStatus.Canceled
|
||||
].map((s) => ({ id: s, ...issueStatuses[s] }))
|
||||
|
||||
function handleStatusChange (id: any) {
|
||||
if (id !== undefined) {
|
||||
status = id
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button
|
||||
label={issueStatuses[status].label}
|
||||
icon={issueStatuses[status].icon}
|
||||
width="min-content"
|
||||
size="small"
|
||||
kind="no-border"
|
||||
on:click={(ev) => {
|
||||
const handleStatusEditorOpened = (event: Event) => {
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{ value: statusesInfo, placeholder: tracker.string.SetStatus, searchable: true },
|
||||
ev.currentTarget,
|
||||
handleStatusChange
|
||||
event.currentTarget,
|
||||
onStatusChange
|
||||
)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if kind === 'button'}
|
||||
<Button
|
||||
label={shouldShowLabel ? issueStatuses[status].label : undefined}
|
||||
icon={issueStatuses[status].icon}
|
||||
width="min-content"
|
||||
size="small"
|
||||
kind="no-border"
|
||||
on:click={handleStatusEditorOpened}
|
||||
/>
|
||||
{:else if kind === 'icon'}
|
||||
<div class="flex-presenter" on:click={handleStatusEditorOpened}>
|
||||
<div class="icon">
|
||||
<Icon icon={issueStatuses[status].icon} size={'small'} />
|
||||
</div>
|
||||
{#if shouldShowLabel}
|
||||
<div class="label nowrap">
|
||||
<Label label={issueStatuses[status].label} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<!--
|
||||
// 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 contact, { Employee, formatName } from '@anticrm/contact'
|
||||
import { Ref, WithLookup } from '@anticrm/core'
|
||||
import { Issue, Team } from '@anticrm/tracker'
|
||||
import { Avatar, UsersPopup, getClient } from '@anticrm/presentation'
|
||||
import { showPopup, Tooltip } from '@anticrm/ui'
|
||||
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let value: WithLookup<Issue>
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
|
||||
$: employee = value?.$lookup?.assignee as Employee | undefined
|
||||
$: avatar = employee?.avatar
|
||||
$: formattedName = employee?.name ? formatName(employee.name) : ''
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const handleAssigneeChanged = async (result: Employee | null | undefined) => {
|
||||
if (result === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIssue = await client.findOne(tracker.class.Issue, { space: currentSpace, _id: value._id })
|
||||
|
||||
if (currentIssue === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const newAssignee = result === null ? null : result._id
|
||||
|
||||
await client.update(currentIssue, { assignee: newAssignee })
|
||||
}
|
||||
|
||||
const handleAssigneeEditorOpened = async (event: Event) => {
|
||||
showPopup(
|
||||
UsersPopup,
|
||||
{
|
||||
_class: contact.class.Employee,
|
||||
selected: employee?._id,
|
||||
allowDeselect: true,
|
||||
placeholder: tracker.string.AssignTo
|
||||
},
|
||||
event.target,
|
||||
handleAssigneeChanged
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tooltip label={employee ? tracker.string.AssignedTo : tracker.string.AssignTo} props={{ value: formattedName }}>
|
||||
<div class="flex-presenter" on:click={handleAssigneeEditorOpened}>
|
||||
<div class="icon">
|
||||
<Avatar size={'x-small'} {avatar} />
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -107,7 +107,7 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="card" let:object>
|
||||
{@const issue = toIssue(object)}
|
||||
<div class="flex-row h-18">
|
||||
<div class="flex-row h-18 pt-2 pb-2 pr-4 pl-4">
|
||||
<div class="flex-between mb-2">
|
||||
<IssuePresenter value={object} {currentTeam} />
|
||||
{#if issue.$lookup?.assignee}
|
||||
|
||||
@@ -50,17 +50,12 @@
|
||||
<IssuesList
|
||||
_class={tracker.class.Issue}
|
||||
config={[
|
||||
{ key: '', presenter: tracker.component.PriorityPresenter, props: { currentSpace } },
|
||||
{ key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam } },
|
||||
{ key: '', presenter: tracker.component.StatusPresenter, props: { currentSpace } },
|
||||
{ key: '', presenter: tracker.component.TitlePresenter },
|
||||
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
|
||||
{
|
||||
key: '$lookup.assignee',
|
||||
props: {
|
||||
shouldShowName: false,
|
||||
shouldShowPlaceholder: true,
|
||||
tooltipLabels: { personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }
|
||||
}
|
||||
}
|
||||
{ key: '', presenter: tracker.component.AssigneePresenter, props: { currentSpace } }
|
||||
]}
|
||||
{options}
|
||||
query={{ ...query, status: category }}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
// 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
|
||||
@@ -18,7 +17,6 @@
|
||||
import type { Issue, Team } from '@anticrm/tracker'
|
||||
import { Icon, showPanel } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
import { issuePriorities } from '../../utils'
|
||||
|
||||
export let value: Issue
|
||||
export let currentTeam: Team
|
||||
@@ -27,20 +25,13 @@
|
||||
const client = getClient()
|
||||
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel
|
||||
|
||||
function show () {
|
||||
const handleIssueEditorOpened = () => {
|
||||
showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value && shortLabel}
|
||||
<div
|
||||
class="flex-presenter"
|
||||
class:inline-presenter={inline}
|
||||
on:click={show}
|
||||
>
|
||||
<div class="icon">
|
||||
<Icon icon={issuePriorities[value.priority].icon} size={'small'} />
|
||||
</div>
|
||||
<div class="flex-presenter" class:inline-presenter={inline} on:click={handleIssueEditorOpened}>
|
||||
<div class="icon">
|
||||
<Icon icon={tracker.icon.Issue} size={'small'} />
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<Label label={title} params={{ value: getTotalIssues() }} />
|
||||
</div>
|
||||
|
||||
<div class="ml-4 mt-4">
|
||||
<div class="mt-4">
|
||||
{#each categories as category}
|
||||
<CategoryPresenter
|
||||
{category}
|
||||
|
||||
@@ -75,11 +75,8 @@
|
||||
})
|
||||
}
|
||||
|
||||
const handleIssueSelected = (id: Ref<Doc>, event: Event) => {
|
||||
const eventTarget = event.target as HTMLInputElement
|
||||
const isChecked = eventTarget.checked
|
||||
|
||||
if (isChecked) {
|
||||
const handleIssueSelected = (id: Ref<Doc>, event: CustomEvent<boolean>) => {
|
||||
if (event.detail) {
|
||||
selectedIssueIds.add(id)
|
||||
} else {
|
||||
selectedIssueIds.delete(id)
|
||||
@@ -113,27 +110,35 @@
|
||||
{#each attributeModels as attributeModel, attributeModelIndex}
|
||||
{#if attributeModelIndex === 0}
|
||||
<div class="gridElement">
|
||||
<div class="antiTable-cells__checkCell">
|
||||
<div class="eListGridCheckBox ml-2">
|
||||
<CheckBox
|
||||
checked={selectedIssueIds.has(docObject._id)}
|
||||
on:change={(event) => {
|
||||
on:value={(event) => {
|
||||
handleIssueSelected(docObject._id, event)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="issuePresenter">
|
||||
<div class="priorityPresenter">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
<div
|
||||
id="context-menu"
|
||||
class="eIssuePresenterContextMenu"
|
||||
on:click={(event) => showMenu(event, docObject, rowIndex)}
|
||||
>
|
||||
<IconMoreV size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if attributeModelIndex === 1}
|
||||
<div class="issuePresenter">
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
<div
|
||||
id="context-menu"
|
||||
class="eIssuePresenterContextMenu"
|
||||
on:click={(event) => showMenu(event, docObject, rowIndex)}
|
||||
>
|
||||
<IconMoreV size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -178,13 +183,17 @@
|
||||
|
||||
.listGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 9rem auto 4rem 2rem;
|
||||
grid-template-columns: 4rem 5rem 2rem auto 4rem 2rem;
|
||||
height: 3.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
border-bottom: 1px solid var(--theme-button-border-hovered);
|
||||
|
||||
&.mListGridChecked {
|
||||
background-color: var(--theme-table-bg-hover);
|
||||
|
||||
.eListGridCheckBox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.mListGridFixed {
|
||||
@@ -200,6 +209,34 @@
|
||||
&:hover {
|
||||
background-color: var(--theme-table-bg-hover);
|
||||
}
|
||||
|
||||
.eListGridCheckBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.03rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgba(247, 248, 248, 0.5);
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.03rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: rgba(247, 248, 248, 0.5);
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.gridElement {
|
||||
@@ -208,10 +245,14 @@
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.priorityPresenter {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.issuePresenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
padding-right: 1rem;
|
||||
|
||||
.eIssuePresenterContextMenu {
|
||||
visibility: hidden;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<!--
|
||||
// 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 { Ref } from '@anticrm/core'
|
||||
import { Issue, IssuePriority, Team } from '@anticrm/tracker'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import tracker from '../../plugin'
|
||||
import PrioritySelector from '../PrioritySelector.svelte'
|
||||
|
||||
export let value: Issue
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const handlePriorityChanged = async (newPriority: IssuePriority | undefined) => {
|
||||
if (newPriority === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIssue = await client.findOne(tracker.class.Issue, { space: currentSpace, _id: value._id })
|
||||
|
||||
if (currentIssue === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.update(currentIssue, { priority: newPriority })
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<PrioritySelector
|
||||
kind={'icon'}
|
||||
shouldShowLabel={false}
|
||||
priority={value.priority}
|
||||
onPriorityChange={handlePriorityChanged}
|
||||
/>
|
||||
{/if}
|
||||
@@ -0,0 +1,44 @@
|
||||
<!--
|
||||
// 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 { Ref } from '@anticrm/core'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import tracker from '../../plugin'
|
||||
import StatusSelector from '../StatusSelector.svelte'
|
||||
|
||||
export let value: Issue
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const handleStatusChanged = async (newStatus: IssueStatus | undefined) => {
|
||||
if (newStatus === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIssue = await client.findOne(tracker.class.Issue, { space: currentSpace, _id: value._id })
|
||||
|
||||
if (currentIssue === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.update(currentIssue, { status: newStatus })
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<StatusSelector kind={'icon'} shouldShowLabel={false} status={value.status} onStatusChange={handleStatusChanged} />
|
||||
{/if}
|
||||
@@ -14,28 +14,10 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Issue } from '@anticrm/tracker'
|
||||
import { Icon } from '@anticrm/ui'
|
||||
import { issueStatuses } from '../../utils'
|
||||
|
||||
export let value: Issue
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="titlePresenter">
|
||||
<div class="icon">
|
||||
<Icon icon={issueStatuses[value.status].icon} size={'small'} />
|
||||
</div>
|
||||
<span class="label nowrap" title={value.title}>{value.title}</span>
|
||||
</div>
|
||||
<span class="label nowrap" title={value.title}>{value.title}</span>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.titlePresenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,6 +27,10 @@ import Projects from './components/projects/Projects.svelte'
|
||||
import Views from './components/views/Views.svelte'
|
||||
import IssuePresenter from './components/issues/IssuePresenter.svelte'
|
||||
import TitlePresenter from './components/issues/TitlePresenter.svelte'
|
||||
import PriorityPresenter from './components/issues/PriorityPresenter.svelte'
|
||||
import StatusPresenter from './components/issues/StatusPresenter.svelte'
|
||||
import AssigneePresenter from './components/issues/AssigneePresenter.svelte'
|
||||
|
||||
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
|
||||
import EditIssue from './components/issues/EditIssue.svelte'
|
||||
import NewIssueHeader from './components/NewIssueHeader.svelte'
|
||||
@@ -45,6 +49,9 @@ export default async (): Promise<Resources> => ({
|
||||
IssuePresenter,
|
||||
TitlePresenter,
|
||||
ModificationDatePresenter,
|
||||
PriorityPresenter,
|
||||
StatusPresenter,
|
||||
AssigneePresenter,
|
||||
EditIssue,
|
||||
NewIssueHeader
|
||||
}
|
||||
|
||||
@@ -42,11 +42,11 @@ export default mergeIds(trackerId, tracker, {
|
||||
Team: '' as IntlString,
|
||||
SelectTeam: '' as IntlString,
|
||||
SaveIssue: '' as IntlString,
|
||||
CreateMore: '' as IntlString,
|
||||
Todo: '' as IntlString,
|
||||
InProgress: '' as IntlString,
|
||||
Done: '' as IntlString,
|
||||
Canceled: '' as IntlString,
|
||||
SetPriority: '' as IntlString,
|
||||
SetStatus: '' as IntlString,
|
||||
Priority: '' as IntlString,
|
||||
NoPriority: '' as IntlString,
|
||||
@@ -95,6 +95,9 @@ export default mergeIds(trackerId, tracker, {
|
||||
IssuePresenter: '' as AnyComponent,
|
||||
TitlePresenter: '' as AnyComponent,
|
||||
ModificationDatePresenter: '' as AnyComponent,
|
||||
PriorityPresenter: '' as AnyComponent,
|
||||
StatusPresenter: '' as AnyComponent,
|
||||
AssigneePresenter: '' as AnyComponent,
|
||||
EditIssue: '' as AnyComponent,
|
||||
CreateTeam: '' as AnyComponent,
|
||||
NewIssueHeader: '' as AnyComponent
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: sortingFunction = (config.find(it => (typeof it !== 'string') && it.sortingKey === sortKey) as BuildModelKey)?.sortingFunction
|
||||
$: sortingFunction = (config.find((it) => typeof it !== 'string' && it.sortingKey === sortKey) as BuildModelKey)
|
||||
?.sortingFunction
|
||||
|
||||
let qindex = 0
|
||||
async function update (
|
||||
@@ -103,11 +104,9 @@
|
||||
|
||||
let checked: Set<Ref<Doc>> = new Set<Ref<Doc>>()
|
||||
|
||||
function check (id: Ref<Doc>, e: Event) {
|
||||
function check (id: Ref<Doc>, event: CustomEvent<boolean>) {
|
||||
if (!enableChecking) return
|
||||
const target = e.target as HTMLInputElement
|
||||
const value = target.checked
|
||||
if (value) {
|
||||
if (event.detail) {
|
||||
checked.add(id)
|
||||
} else {
|
||||
checked.delete(id)
|
||||
@@ -138,8 +137,8 @@
|
||||
<CheckBox
|
||||
symbol={'minus'}
|
||||
checked={objects?.length === checked.size && objects?.length > 0}
|
||||
on:change={(e) => {
|
||||
objects.map((o) => check(o._id, e))
|
||||
on:value={(event) => {
|
||||
objects.map((object) => check(object._id, event))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -182,20 +181,23 @@
|
||||
<div class="antiTable-cells__checkCell">
|
||||
<CheckBox
|
||||
checked={checked.has(object._id)}
|
||||
on:change={(e) => {
|
||||
check(object._id, e)
|
||||
on:value={(event) => {
|
||||
check(object._id, event)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Component is={notification.component.NotificationPresenter} props={{ value: object, kind: enableChecking ? 'table' : 'block' }} />
|
||||
<Component
|
||||
is={notification.component.NotificationPresenter}
|
||||
props={{ value: object, kind: enableChecking ? 'table' : 'block' }}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="antiTable-cells__checkCell">
|
||||
<CheckBox
|
||||
checked={checked.has(object._id)}
|
||||
on:change={(e) => {
|
||||
check(object._id, e)
|
||||
on:value={(event) => {
|
||||
check(object._id, event)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -209,7 +211,11 @@
|
||||
value={getObjectValue(attribute.key, object) ?? ''}
|
||||
{...attribute.props}
|
||||
/>
|
||||
<div id='context-menu' class="antiTable-cells__firstCell-menuRow" on:click={(ev) => showMenu(ev, object, row)}>
|
||||
<div
|
||||
id="context-menu"
|
||||
class="antiTable-cells__firstCell-menuRow"
|
||||
on:click={(ev) => showMenu(ev, object, row)}
|
||||
>
|
||||
<MoreV size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,9 +242,7 @@
|
||||
{#if enableChecking}
|
||||
<td>
|
||||
<div class="antiTable-cells__checkCell">
|
||||
<CheckBox
|
||||
checked={false}
|
||||
/>
|
||||
<CheckBox checked={false} />
|
||||
</div>
|
||||
</td>
|
||||
{/if}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
$: query.query(core.class.Space, { _id: spaceId }, result => { space = result[0] })
|
||||
|
||||
function showCreateDialog (ev: Event) {
|
||||
showPopup(createItemDialog as AnyComponent, { space: spaceId }, ev.target as HTMLElement)
|
||||
showPopup(createItemDialog as AnyComponent, { space: spaceId }, 'top')
|
||||
}
|
||||
|
||||
$: updateViewlets(viewlets)
|
||||
|
||||
@@ -16,11 +16,10 @@
|
||||
<script lang="ts">
|
||||
import core, { Class, Doc, Ref, Space, WithLookup } from '@anticrm/core'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { AnyComponent, Component } from '@anticrm/ui'
|
||||
import view, { Viewlet } from '@anticrm/view'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { AnyComponent,Component } from '@anticrm/ui'
|
||||
import view,{ Viewlet } from '@anticrm/view'
|
||||
import type { ViewConfiguration } from '@anticrm/workbench'
|
||||
|
||||
import SpaceContent from './SpaceContent.svelte'
|
||||
import SpaceHeader from './SpaceHeader.svelte'
|
||||
|
||||
@@ -33,34 +32,38 @@
|
||||
let viewlet: WithLookup<Viewlet> | undefined = undefined
|
||||
let space: Space | undefined
|
||||
let _class: Ref<Class<Doc>> | undefined = undefined
|
||||
let header: AnyComponent | undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let viewlets: WithLookup<Viewlet>[] = []
|
||||
|
||||
async function update (attachTo?: Ref<Class<Doc>>): Promise<void> {
|
||||
$: update(currentSpace, currentView?.class)
|
||||
|
||||
async function update (currentSpace?: Ref<Space>, attachTo?: Ref<Class<Doc>>): Promise<void> {
|
||||
if (currentSpace === undefined) {
|
||||
space = undefined
|
||||
return
|
||||
}
|
||||
space = await client.findOne(core.class.Space, { _id: currentSpace })
|
||||
if (space === undefined) {
|
||||
header = undefined
|
||||
} else {
|
||||
header = await getHeader(space._class)
|
||||
}
|
||||
if (attachTo) {
|
||||
viewlets = await client.findAll(view.class.Viewlet, { attachTo }, {
|
||||
lookup: {
|
||||
descriptor: core.class.Class
|
||||
}
|
||||
})
|
||||
if (header !== undefined) {
|
||||
viewlet = viewlets[0]
|
||||
}
|
||||
_class = attachTo
|
||||
}
|
||||
}
|
||||
|
||||
$: update(currentView?.class)
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
$: currentSpace && query.query(core.class.Space, {
|
||||
_id: currentSpace
|
||||
}, (res) => {
|
||||
space = res[0]
|
||||
}, {
|
||||
limit: 1
|
||||
})
|
||||
|
||||
const hierarchy = client.getHierarchy()
|
||||
async function getHeader (_class: Ref<Class<Space>>): Promise<AnyComponent | undefined> {
|
||||
const clazz = hierarchy.getClass(_class)
|
||||
@@ -71,12 +74,10 @@
|
||||
|
||||
</script>
|
||||
{#if _class && space}
|
||||
{#await getHeader(space._class) then header}
|
||||
{#if header}
|
||||
<Component is={header} props={{ spaceId: space._id, viewlets, createItemDialog, createItemLabel }} />
|
||||
{:else}
|
||||
<SpaceHeader spaceId={space._id} {viewlets} {createItemDialog} {createItemLabel} bind:search={search} bind:viewlet={viewlet} />
|
||||
{/if}
|
||||
{/await}
|
||||
{#if header}
|
||||
<Component is={header} props={{ spaceId: space._id, viewlets, createItemDialog, createItemLabel }} />
|
||||
{:else}
|
||||
<SpaceHeader spaceId={space._id} {viewlets} {createItemDialog} {createItemLabel} bind:search={search} bind:viewlet={viewlet} />
|
||||
{/if}
|
||||
<SpaceContent space={space._id} {_class} {search} {viewlet} />
|
||||
{/if}
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import chunter, { Channel, Comment, Message } from '@anticrm/chunter'
|
||||
import chunter, { Channel, Comment, Message, ThreadMessage } from '@anticrm/chunter'
|
||||
import { EmployeeAccount } from '@anticrm/contact'
|
||||
import core, { Class, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, Ref, Tx, TxCreateDoc, TxProcessor, TxUpdateDoc } from '@anticrm/core'
|
||||
import core, { Class, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, Ref, Tx, TxCreateDoc, TxProcessor, TxUpdateDoc, TxRemoveDoc } from '@anticrm/core'
|
||||
import login from '@anticrm/login'
|
||||
import { getMetadata } from '@anticrm/platform'
|
||||
import { TriggerControl } from '@anticrm/server-core'
|
||||
@@ -58,7 +58,7 @@ export async function CommentCreate (tx: Tx, control: TriggerControl): Promise<T
|
||||
const hierarchy = control.hierarchy
|
||||
if (tx._class !== core.class.TxCreateDoc) return []
|
||||
const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc<Doc>)
|
||||
if (!hierarchy.isDerived(doc._class, chunter.class.Comment)) {
|
||||
if (!hierarchy.isDerived(doc._class, chunter.class.ThreadMessage)) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -80,6 +80,44 @@ export async function CommentCreate (tx: Tx, control: TriggerControl): Promise<T
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function CommentDelete (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const hierarchy = control.hierarchy
|
||||
if (tx._class !== core.class.TxRemoveDoc) return []
|
||||
|
||||
const rmTx = tx as TxRemoveDoc<ThreadMessage>
|
||||
if (!hierarchy.isDerived(rmTx.objectClass, chunter.class.ThreadMessage)) {
|
||||
return []
|
||||
}
|
||||
const createTx = (await control.findAll(core.class.TxCreateDoc, {
|
||||
objectId: rmTx.objectId
|
||||
}, { limit: 1 }))[0]
|
||||
|
||||
const comment = TxProcessor.createDoc2Doc(createTx as TxCreateDoc<ThreadMessage>)
|
||||
|
||||
const comments = await control.findAll(chunter.class.ThreadMessage, {
|
||||
attachedTo: comment.attachedTo
|
||||
})
|
||||
const updateTx = control.txFactory.createTxUpdateDoc<Message>(
|
||||
chunter.class.Message,
|
||||
comment.space,
|
||||
comment.attachedTo,
|
||||
{
|
||||
replies:
|
||||
comments
|
||||
.map(comm => (control.modelDb.getObject(comm.createBy) as EmployeeAccount).employee),
|
||||
lastReply:
|
||||
comments.length > 0
|
||||
? Math.max(...comments.map(comm => comm.createOn))
|
||||
: undefined
|
||||
}
|
||||
)
|
||||
|
||||
return [updateTx]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -112,7 +150,8 @@ export async function MessageCreate (tx: Tx, control: TriggerControl): Promise<T
|
||||
export async function ChunterTrigger (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const promises = [
|
||||
MessageCreate(tx, control),
|
||||
CommentCreate(tx, control)
|
||||
CommentCreate(tx, control),
|
||||
CommentDelete(tx, control)
|
||||
]
|
||||
const res = await Promise.all(promises)
|
||||
return res.flat()
|
||||
|
||||
@@ -58,7 +58,7 @@ test.describe('recruit tests', () => {
|
||||
// Click on Add button
|
||||
await page.click('.applications-container .flex-row-center .flex-center')
|
||||
|
||||
await page.click('span:has-text("Select vacancy")')
|
||||
await page.click('button:has-text("Vacancy")')
|
||||
|
||||
await page.click(`button:has-text("${vacancyId}")`)
|
||||
|
||||
@@ -142,13 +142,13 @@ test.describe('recruit tests', () => {
|
||||
// Click button:has-text("Review")
|
||||
await page.click('button:has-text("Review")')
|
||||
// Click [placeholder="\ "]
|
||||
await page.click('[placeholder="placeholder"]')
|
||||
await page.click('[placeholder="Title"]')
|
||||
// Fill [placeholder="\ "]
|
||||
await page.fill('[placeholder="placeholder"]', 'Meet PEterson')
|
||||
await page.fill('[placeholder="Title"]', 'Meet PEterson')
|
||||
// Click text=Location Company Company >> [placeholder="\ "]
|
||||
await page.click('text=placeholder Location >> [placeholder="placeholder"]')
|
||||
await page.click('[placeholder="Location"]')
|
||||
// Fill text=Location Company Company >> [placeholder="\ "]
|
||||
await page.fill('text=placeholder Location >> [placeholder="placeholder"]', 'NSK')
|
||||
await page.fill('[placeholder="Location"]', 'NSK')
|
||||
// Click text=Company Company >> div
|
||||
// await page.click('text=Company Company >> div')
|
||||
// Click button:has-text("Apple")
|
||||
|
||||
@@ -30,11 +30,11 @@ test.describe('recruit tests', () => {
|
||||
// Fill [placeholder="Please\ type\ Skill\ title"]
|
||||
await page.fill('[placeholder="Please\\ type\\ Skill\\ title"]', 's1')
|
||||
// Click text=Create Skill s1 Please type description here Category Other Create Cancel >> button
|
||||
await page.click('text=Create Skill s1 Please type description here Category Other Create Cancel >> button')
|
||||
await page.click('text=Create more Create >> button')
|
||||
// Click text=s1
|
||||
await page.click('text=s1')
|
||||
// Click :nth-match(:text("Cancel"), 2)
|
||||
await page.click(':nth-match(:text("Cancel"), 2)')
|
||||
await page.click('button:has-text("Cancel")')
|
||||
// Click button:has-text("Create")
|
||||
await page.click('button:has-text("Create")')
|
||||
})
|
||||
@@ -78,7 +78,7 @@ test.describe('recruit tests', () => {
|
||||
// Click text=java
|
||||
await page.click('text=java')
|
||||
// Click :nth-match(:text("Cancel"), 2)
|
||||
await page.click(':nth-match(:text("Cancel"), 2)')
|
||||
await page.click('button:has-text("Cancel")')
|
||||
// Click [placeholder="John"]
|
||||
await page.click('[placeholder="John"]')
|
||||
// Fill [placeholder="John"]
|
||||
|
||||
@@ -14,14 +14,14 @@ test.describe('workbench tests', () => {
|
||||
await page.click('text=Applications')
|
||||
await expect(page).toHaveURL('http://localhost:8083/workbench%3Acomponent%3AWorkbenchApp/recruit%3Aapp%3ARecruit/applicants')
|
||||
// Click text=Applications Application >> span
|
||||
await expect(page.locator('text=Applications Application >> span')).toBeVisible()
|
||||
await expect(page.locator('text=Applications Application')).toBeVisible()
|
||||
await expect(page.locator('text=APP-1')).toBeVisible()
|
||||
|
||||
// Click text=Candidates
|
||||
await page.click('text=Candidates')
|
||||
await expect(page).toHaveURL('http://localhost:8083/workbench%3Acomponent%3AWorkbenchApp/recruit%3Aapp%3ARecruit/candidates')
|
||||
|
||||
await expect(page.locator('text=Candidates Candidate >> span')).toBeVisible()
|
||||
await expect(page.locator('text=Candidates Candidate')).toBeVisible()
|
||||
await expect(page.locator('text=Andrey P.')).toBeVisible()
|
||||
|
||||
// Click text=Vacancies
|
||||
|
||||
Reference in New Issue
Block a user