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:
Anna No
2022-04-13 12:34:02 +07:00
81 changed files with 1321 additions and 885 deletions
@@ -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>
+2 -1
View File
@@ -13,6 +13,7 @@
"Remove": "Remove",
"Members": "Members",
"Search": "Search...",
"Unassigned": "Unassigned"
"Unassigned": "Unassigned",
"CreateMore": "Create more"
}
}
+2 -1
View File
@@ -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>
+1
View File
@@ -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'
+2 -1
View File
@@ -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>
+17 -5
View File
@@ -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; }
+22 -3
View File
@@ -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; }
}
}
}
}
+1
View File
@@ -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 {
+7
View File
@@ -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;
+25 -10
View File
@@ -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 -1
View File
@@ -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>
+1 -2
View File
@@ -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> -->
+5 -1
View File
@@ -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;
+2 -2
View File
@@ -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;
}
+6 -11
View File
@@ -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>
+1
View File
@@ -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
}}
+2 -1
View File
@@ -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"
}
}
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
}
})
+4
View File
@@ -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

+2 -1
View File
@@ -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>
+2 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+2 -4
View File
@@ -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;
}
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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>
+7
View File
@@ -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
}
+4 -1
View File
@@ -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}
+43 -4
View File
@@ -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()
+5 -5
View File
@@ -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")
+3 -3
View File
@@ -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"]
+2 -2
View File
@@ -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