mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-20 15:20:18 +00:00
*Candidate Flow & Notifications (#2272)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
096a14578b
commit
1adb05e4ae
@ -15,7 +15,7 @@
|
||||
|
||||
// To help typescript locate view plugin properly
|
||||
import automation, { AutomationSupport } from '@anticrm/automation'
|
||||
import { Board, Card, MenuPage, CommonBoardPreference, CardCover, boardId } from '@anticrm/board'
|
||||
import { Board, boardId, Card, CardCover, CommonBoardPreference, MenuPage } from '@anticrm/board'
|
||||
import type { Employee } from '@anticrm/contact'
|
||||
import { Class, DOMAIN_MODEL, IndexKind, Markup, Ref, Type } from '@anticrm/core'
|
||||
import {
|
||||
@ -35,13 +35,13 @@ import attachment from '@anticrm/model-attachment'
|
||||
import chunter from '@anticrm/model-chunter'
|
||||
import contact from '@anticrm/model-contact'
|
||||
import core, { TDoc, TType } from '@anticrm/model-core'
|
||||
import preference, { TPreference } from '@anticrm/model-preference'
|
||||
import tags from '@anticrm/model-tags'
|
||||
import task, { TSpaceWithStates, TTask } from '@anticrm/model-task'
|
||||
import view, { actionTemplates, createAction } from '@anticrm/model-view'
|
||||
import view, { actionTemplates, createAction, actionTemplates as viewTemplates } from '@anticrm/model-view'
|
||||
import workbench, { Application } from '@anticrm/model-workbench'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
import preference, { TPreference } from '@anticrm/model-preference'
|
||||
import tags from '@anticrm/model-tags'
|
||||
import board from './plugin'
|
||||
|
||||
@Model(board.class.Board, task.class.SpaceWithStates)
|
||||
@ -526,6 +526,19 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
board.action.ConvertToCard
|
||||
)
|
||||
|
||||
createAction(builder, {
|
||||
...viewTemplates.open,
|
||||
target: board.class.Board,
|
||||
context: {
|
||||
mode: ['browser', 'context'],
|
||||
group: 'create'
|
||||
},
|
||||
action: workbench.actionImpl.Navigate,
|
||||
actionProps: {
|
||||
mode: 'space'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export { boardOperation } from './migration'
|
||||
|
@ -47,7 +47,7 @@ import attachment from '@anticrm/model-attachment'
|
||||
import core, { TAttachedDoc, TSpace } from '@anticrm/model-core'
|
||||
import notification from '@anticrm/model-notification'
|
||||
import preference, { TPreference } from '@anticrm/model-preference'
|
||||
import view, { createAction } from '@anticrm/model-view'
|
||||
import view, { actionTemplates as viewTemplates, createAction } from '@anticrm/model-view'
|
||||
import workbench from '@anticrm/model-workbench'
|
||||
import chunter from './plugin'
|
||||
|
||||
@ -478,6 +478,19 @@ export function createModel (builder: Builder): void {
|
||||
builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ClassFilters, {
|
||||
filters: ['private', 'archived']
|
||||
})
|
||||
|
||||
createAction(builder, {
|
||||
...viewTemplates.open,
|
||||
target: chunter.class.Channel,
|
||||
context: {
|
||||
mode: ['browser', 'context'],
|
||||
group: 'create'
|
||||
},
|
||||
action: workbench.actionImpl.Navigate,
|
||||
actionProps: {
|
||||
mode: 'space'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export { chunterOperation } from './migration'
|
||||
|
@ -87,7 +87,9 @@
|
||||
if (r instanceof Promise) {
|
||||
r.then(() => {
|
||||
okProcessing = false
|
||||
dispatch('close')
|
||||
if (!createMore) {
|
||||
dispatch('close')
|
||||
}
|
||||
})
|
||||
} else if (!createMore) {
|
||||
okProcessing = false
|
||||
|
@ -28,10 +28,10 @@
|
||||
showPopup,
|
||||
tooltip
|
||||
} from '@anticrm/ui'
|
||||
import { createEventDispatcher, afterUpdate } from 'svelte'
|
||||
import { afterUpdate, createEventDispatcher } from 'svelte'
|
||||
import presentation from '..'
|
||||
import { createQuery, getClient } from '../utils'
|
||||
import { ObjectCreate } from '../types'
|
||||
import { createQuery, getClient } from '../utils'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let options: FindOptions<Doc> | undefined = undefined
|
||||
@ -155,6 +155,35 @@
|
||||
}
|
||||
|
||||
afterUpdate(() => dispatch('changeContent'))
|
||||
|
||||
let selectedDiv: HTMLElement | undefined
|
||||
let scrollDiv: HTMLElement | undefined
|
||||
let cHeight = 0
|
||||
|
||||
const updateLocation = (scrollDiv?: HTMLElement, selectedDiv?: HTMLElement, objects?: Doc[], selected?: Ref<Doc>) => {
|
||||
const objIt = objects?.find((it) => it._id === selected)
|
||||
if (objIt === undefined) {
|
||||
cHeight = 0
|
||||
return
|
||||
}
|
||||
if (scrollDiv && selectedDiv) {
|
||||
const r = selectedDiv.getBoundingClientRect()
|
||||
const r2 = scrollDiv.getBoundingClientRect()
|
||||
if (r && r2) {
|
||||
if (r.top > r2.top && r.bottom < r2.bottom) {
|
||||
cHeight = 0
|
||||
} else {
|
||||
if (r.bottom < r2.bottom) {
|
||||
cHeight = 1
|
||||
} else {
|
||||
cHeight = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: updateLocation(scrollDiv, selectedDiv, objects, selected)
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
@ -181,7 +210,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="scroll">
|
||||
{#if cHeight === 1}
|
||||
<div class="background-theme-content-accent" style:height={'2px'} />
|
||||
{/if}
|
||||
<div class="scroll" on:scroll={() => updateLocation(scrollDiv, selectedDiv)} bind:this={scrollDiv}>
|
||||
<div class="box">
|
||||
<ListView bind:this={list} count={objects.length} bind:selection>
|
||||
<svelte:fragment slot="category" let:item>
|
||||
@ -199,6 +231,8 @@
|
||||
{@const obj = objects[item]}
|
||||
<button
|
||||
class="menu-item w-full"
|
||||
class:background-bg-focused={!allowDeselect && obj._id === selected}
|
||||
class:border-radius-1={!allowDeselect && obj._id === selected}
|
||||
on:click={() => {
|
||||
handleSelection(undefined, objects, item)
|
||||
}}
|
||||
@ -206,19 +240,27 @@
|
||||
{#if allowDeselect && selected}
|
||||
<div class="icon">
|
||||
{#if obj._id === selected}
|
||||
{#if titleDeselect}
|
||||
<div class="clear-mins" use:tooltip={{ label: titleDeselect ?? presentation.string.Deselect }}>
|
||||
<div bind:this={selectedDiv}>
|
||||
{#if titleDeselect}
|
||||
<div class="clear-mins" use:tooltip={{ label: titleDeselect ?? presentation.string.Deselect }}>
|
||||
<Icon icon={IconCheck} {size} />
|
||||
</div>
|
||||
{:else}
|
||||
<Icon icon={IconCheck} {size} />
|
||||
</div>
|
||||
{:else}
|
||||
<Icon icon={IconCheck} {size} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span class="label">
|
||||
<slot name="item" item={obj} />
|
||||
{#if obj._id === selected}
|
||||
<div bind:this={selectedDiv}>
|
||||
<slot name="item" item={obj} />
|
||||
</div>
|
||||
{:else}
|
||||
<slot name="item" item={obj} />
|
||||
{/if}
|
||||
</span>
|
||||
{#if multiSelect}
|
||||
<div class="check-right pointer-events-none">
|
||||
@ -230,6 +272,9 @@
|
||||
</ListView>
|
||||
</div>
|
||||
</div>
|
||||
{#if cHeight === -1}
|
||||
<div class="background-theme-content-accent" style:height={'2px'} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -17,24 +17,27 @@
|
||||
import { getClient } from '../utils'
|
||||
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
Button,
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
eventToHTMLElement,
|
||||
getEventPositionElement,
|
||||
getFocusManager,
|
||||
IconFolder,
|
||||
Label,
|
||||
showPopup,
|
||||
IconFolder,
|
||||
Button,
|
||||
eventToHTMLElement,
|
||||
getFocusManager,
|
||||
TooltipAlignment,
|
||||
ButtonKind,
|
||||
ButtonSize
|
||||
TooltipAlignment
|
||||
} from '@anticrm/ui'
|
||||
import SpacesPopup from './SpacesPopup.svelte'
|
||||
|
||||
import type { Ref, Class, Space, DocumentQuery } from '@anticrm/core'
|
||||
import type { Class, DocumentQuery, FindOptions, Ref, Space } from '@anticrm/core'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { ObjectCreate } from '../types'
|
||||
|
||||
export let _class: Ref<Class<Space>>
|
||||
export let spaceQuery: DocumentQuery<Space> | undefined = { archived: false }
|
||||
export let spaceOptions: FindOptions<Space> | undefined = {}
|
||||
export let label: IntlString
|
||||
export let value: Ref<Space> | undefined
|
||||
export let focusIndex = -1
|
||||
@ -46,6 +49,8 @@
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = undefined
|
||||
export let allowDeselect = false
|
||||
export let component: AnySvelteComponent | undefined = undefined
|
||||
export let componentProps: any | undefined = undefined
|
||||
|
||||
let selected: Space | undefined
|
||||
|
||||
@ -74,12 +79,14 @@
|
||||
_class,
|
||||
label,
|
||||
allowDeselect,
|
||||
options: { sort: { modifiedOn: -1 } },
|
||||
selected,
|
||||
spaceOptions: { ...(spaceOptions ?? {}), sort: { ...(spaceOptions?.sort ?? {}), modifiedOn: -1 } },
|
||||
selected: selected?._id,
|
||||
spaceQuery,
|
||||
create
|
||||
create,
|
||||
component,
|
||||
componentProps
|
||||
},
|
||||
eventToHTMLElement(ev),
|
||||
!$$slots.content ? eventToHTMLElement(ev) : getEventPositionElement(ev),
|
||||
(result) => {
|
||||
if (result) {
|
||||
value = result._id
|
||||
@ -91,19 +98,25 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button
|
||||
id="space.selector"
|
||||
{focus}
|
||||
{focusIndex}
|
||||
icon={IconFolder}
|
||||
{size}
|
||||
{kind}
|
||||
{justify}
|
||||
{width}
|
||||
showTooltip={{ label, direction: labelDirection }}
|
||||
on:click={showSpacesPopup}
|
||||
>
|
||||
<span slot="content" class="overflow-label disabled text-sm">
|
||||
{#if selected}{selected.name}{:else}<Label {label} />{/if}
|
||||
</span>
|
||||
</Button>
|
||||
{#if $$slots.content}
|
||||
<div id="space.selector" class="w-full h-full flex-streatch" on:click={showSpacesPopup}>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
{:else}
|
||||
<Button
|
||||
id="space.selector"
|
||||
{focus}
|
||||
{focusIndex}
|
||||
icon={IconFolder}
|
||||
{size}
|
||||
{kind}
|
||||
{justify}
|
||||
{width}
|
||||
showTooltip={{ label, direction: labelDirection }}
|
||||
on:click={showSpacesPopup}
|
||||
>
|
||||
<span slot="content" class="overflow-label disabled text-sm">
|
||||
{#if selected}{selected.name}{:else}<Label {label} />{/if}
|
||||
</span>
|
||||
</Button>
|
||||
{/if}
|
||||
|
@ -13,15 +13,16 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Class, DocumentQuery, Ref, Space } from '@anticrm/core'
|
||||
import type { Class, DocumentQuery, FindOptions, Ref, Space } from '@anticrm/core'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import { AnySvelteComponent, ButtonKind, ButtonSize } from '@anticrm/ui'
|
||||
import { ObjectCreate } from '../types'
|
||||
import SpaceSelect from './SpaceSelect.svelte'
|
||||
|
||||
export let space: Ref<Space> | undefined = undefined
|
||||
export let _class: Ref<Class<Space>>
|
||||
export let query: DocumentQuery<Space> = { archived: false }
|
||||
export let queryOptions: FindOptions<Space> | undefined = {}
|
||||
export let label: IntlString
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
@ -29,6 +30,8 @@
|
||||
export let width: string | undefined = undefined
|
||||
export let allowDeselect = false
|
||||
export let focus = true
|
||||
export let component: AnySvelteComponent | undefined = undefined
|
||||
export let componentProps: any | undefined = undefined
|
||||
|
||||
export let create: ObjectCreate | undefined = undefined
|
||||
</script>
|
||||
@ -39,12 +42,15 @@
|
||||
{focus}
|
||||
{_class}
|
||||
spaceQuery={query}
|
||||
spaceOptions={queryOptions}
|
||||
{allowDeselect}
|
||||
{label}
|
||||
{size}
|
||||
{kind}
|
||||
{justify}
|
||||
{width}
|
||||
{component}
|
||||
{componentProps}
|
||||
bind:value={space}
|
||||
on:change={(evt) => {
|
||||
space = evt.detail
|
||||
|
@ -13,7 +13,8 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Class, Doc, DocumentQuery, Ref, Space } from '@anticrm/core'
|
||||
import type { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@anticrm/core'
|
||||
import { AnySvelteComponent } from '@anticrm/ui'
|
||||
import { ObjectCreate } from '../types'
|
||||
import ObjectPopup from './ObjectPopup.svelte'
|
||||
import SpaceInfo from './SpaceInfo.svelte'
|
||||
@ -21,8 +22,11 @@
|
||||
export let _class: Ref<Class<Space>>
|
||||
export let selected: Ref<Space> | undefined
|
||||
export let spaceQuery: DocumentQuery<Space> | undefined
|
||||
export let spaceOptions: FindOptions<Space> | undefined = {}
|
||||
export let create: ObjectCreate | undefined = undefined
|
||||
export let allowDeselect = false
|
||||
export let component: AnySvelteComponent | undefined = undefined
|
||||
export let componentProps: any | undefined = undefined
|
||||
|
||||
$: _create =
|
||||
create !== undefined
|
||||
@ -35,6 +39,7 @@
|
||||
|
||||
<ObjectPopup
|
||||
{_class}
|
||||
options={spaceOptions}
|
||||
{selected}
|
||||
bind:docQuery={spaceQuery}
|
||||
multiSelect={false}
|
||||
@ -45,6 +50,10 @@
|
||||
on:close
|
||||
>
|
||||
<svelte:fragment slot="item" let:item={space}>
|
||||
<SpaceInfo size={'large'} value={space} />
|
||||
{#if component}
|
||||
<svelte:component this={component} {...componentProps} size={'large'} value={space} />
|
||||
{:else}
|
||||
<SpaceInfo size={'large'} value={space} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ObjectPopup>
|
||||
|
@ -23,6 +23,7 @@
|
||||
Button,
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
getEventPositionElement,
|
||||
getFocusManager,
|
||||
Icon,
|
||||
IconOpen,
|
||||
@ -59,6 +60,7 @@
|
||||
export let focusIndex = -1
|
||||
export let showTooltip: LabelAndProps | undefined = undefined
|
||||
export let showNavigate = true
|
||||
export let id: string | undefined = undefined
|
||||
|
||||
export let create: ObjectCreate | undefined = undefined
|
||||
|
||||
@ -81,7 +83,7 @@
|
||||
}
|
||||
const mgr = getFocusManager()
|
||||
|
||||
const _click = (): void => {
|
||||
const _click = (ev: MouseEvent): void => {
|
||||
if (!readonly) {
|
||||
showPopup(
|
||||
UsersPopup,
|
||||
@ -97,7 +99,7 @@
|
||||
placeholder,
|
||||
create
|
||||
},
|
||||
container,
|
||||
!$$slots.content ? container : getEventPositionElement(ev),
|
||||
(result) => {
|
||||
if (result === null) {
|
||||
value = null
|
||||
@ -116,44 +118,54 @@
|
||||
$: hideIcon = size === 'x-large' || (size === 'large' && kind !== 'link')
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class="min-w-0" class:w-full={width === '100%'}>
|
||||
<Button {focusIndex} width={width ?? 'min-content'} {size} {kind} {justify} {showTooltip} on:click={_click}>
|
||||
<span slot="content" class="overflow-label flex-grow" class:flex-between={showNavigate && selected}>
|
||||
<div
|
||||
class="disabled"
|
||||
style:width={showNavigate && selected
|
||||
? `calc(${width ?? 'min-content'} - 1.5rem)`
|
||||
: `${width ?? 'min-content'}`}
|
||||
use:tooltip={selected !== undefined ? { label: getEmbeddedLabel(getName(selected)) } : undefined}
|
||||
>
|
||||
{#if selected}
|
||||
{#if hideIcon || selected}
|
||||
<UserInfo value={selected} size={kind === 'link' ? 'x-small' : 'tiny'} {icon} />
|
||||
{:else}
|
||||
{getName(selected)}
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex-row-center">
|
||||
{#if icon}
|
||||
<Icon {icon} size={kind === 'link' ? 'small' : size} />
|
||||
<div {id} bind:this={container} class="min-w-0" class:w-full={width === '100%'} class:h-full={$$slots.content}>
|
||||
{#if $$slots.content}
|
||||
<div
|
||||
class="w-full h-full flex-streatch"
|
||||
on:click={_click}
|
||||
use:tooltip={selected !== undefined ? { label: getEmbeddedLabel(getName(selected)) } : undefined}
|
||||
>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
{:else}
|
||||
<Button {focusIndex} width={width ?? 'min-content'} {size} {kind} {justify} {showTooltip} on:click={_click}>
|
||||
<span slot="content" class="overflow-label flex-grow" class:flex-between={showNavigate && selected}>
|
||||
<div
|
||||
class="disabled"
|
||||
style:width={showNavigate && selected
|
||||
? `calc(${width ?? 'min-content'} - 1.5rem)`
|
||||
: `${width ?? 'min-content'}`}
|
||||
use:tooltip={selected !== undefined ? { label: getEmbeddedLabel(getName(selected)) } : undefined}
|
||||
>
|
||||
{#if selected}
|
||||
{#if hideIcon || selected}
|
||||
<UserInfo value={selected} size={kind === 'link' ? 'x-small' : 'tiny'} {icon} />
|
||||
{:else}
|
||||
{getName(selected)}
|
||||
{/if}
|
||||
<div class="ml-2">
|
||||
<Label {label} />
|
||||
{:else}
|
||||
<div class="flex-row-center">
|
||||
{#if icon}
|
||||
<Icon {icon} size={kind === 'link' ? 'small' : size} />
|
||||
{/if}
|
||||
<div class="ml-2">
|
||||
<Label {label} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if selected && showNavigate}
|
||||
<ActionIcon
|
||||
icon={IconOpen}
|
||||
size={'small'}
|
||||
action={() => {
|
||||
if (selected) {
|
||||
showPanel(view.component.EditDoc, selected._id, selected._class, 'content')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if selected && showNavigate}
|
||||
<ActionIcon
|
||||
icon={IconOpen}
|
||||
size={'small'}
|
||||
action={() => {
|
||||
if (selected) {
|
||||
showPanel(view.component.EditDoc, selected._id, selected._class, 'content')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
</Button>
|
||||
</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -700,6 +700,7 @@ a.no-line {
|
||||
.background-card-divider { background-color: var(--theme-card-divider); }
|
||||
.background-primary-color { background-color: var(--primary-button-enabled); }
|
||||
.background-bg-accent { background-color: var(--theme-bg-accent-color); }
|
||||
.background-bg-focused {background-color: var(--theme-bg-focused-color); }
|
||||
.background-bg-accent-normal { background-color: var(--theme-bg-accent-normal); }
|
||||
|
||||
.dark-color { color: var(--dark-color); }
|
||||
|
@ -22,8 +22,9 @@
|
||||
NotificationStatus,
|
||||
NotificationType
|
||||
} from '@anticrm/notification'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { getCurrentLocation } from '@anticrm/ui'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { getCurrentLocation, showPanel } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import notification from '../plugin'
|
||||
import { NotificationClientImpl } from '../utils'
|
||||
|
||||
@ -76,7 +77,7 @@
|
||||
notification.class.Notification,
|
||||
{
|
||||
attachedTo: (getCurrentAccount() as EmployeeAccount).employee,
|
||||
status: NotificationStatus.New
|
||||
status: { $nin: [NotificationStatus.Read] }
|
||||
},
|
||||
(res) => {
|
||||
process(res.reverse())
|
||||
@ -110,15 +111,23 @@
|
||||
await notify(text, notification)
|
||||
}
|
||||
}
|
||||
const client = getClient()
|
||||
|
||||
let clearTimer: number | undefined
|
||||
|
||||
async function notify (text: string, notifyInstance: PlatformNotification): Promise<void> {
|
||||
if (notifyInstance.status !== NotificationStatus.New) {
|
||||
return
|
||||
}
|
||||
if (alreadyShown.has(notifyInstance._id)) {
|
||||
return
|
||||
}
|
||||
alreadyShown.add(notifyInstance._id)
|
||||
|
||||
client.updateDoc(notifyInstance._class, notifyInstance.space, notifyInstance._id, {
|
||||
status: NotificationStatus.Notified
|
||||
})
|
||||
|
||||
if (clearTimer) {
|
||||
clearTimeout(clearTimer)
|
||||
}
|
||||
@ -129,8 +138,6 @@
|
||||
|
||||
const lastView = $lastViews.get(lastViewId)
|
||||
if ((lastView ?? notifyInstance.modifiedOn) > 0) {
|
||||
// eslint-disable-next-line
|
||||
new Notification(getCurrentLocation().path[1], { tag: notifyInstance._id, icon: '/favicon.png', body: text })
|
||||
await notificationClient.updateLastView(
|
||||
lastViewId,
|
||||
contact.class.Employee,
|
||||
@ -138,5 +145,16 @@
|
||||
lastView === undefined
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
const notification = new Notification(getCurrentLocation().path[1], {
|
||||
tag: notifyInstance._id,
|
||||
icon: '/favicon.png',
|
||||
body: text
|
||||
})
|
||||
|
||||
notification.onclick = () => {
|
||||
showPanel(view.component.EditDoc, notifyInstance.attachedTo, notifyInstance.attachedToClass, 'content')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -49,14 +49,19 @@
|
||||
</script>
|
||||
|
||||
{#if displayTx}
|
||||
{@const isNew = notification.status === NotificationStatus.New}
|
||||
{@const isNew = notification.status !== NotificationStatus.Read}
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<div class="content">
|
||||
<div class="flex-row">
|
||||
<div class="bottom-divider mb-2">
|
||||
<div class="flex-row-center mb-2 mt-2">
|
||||
<div class="notify mr-4" style:color={isNew ? getPlatformColor(11) : '#555555'} />
|
||||
<div class="flex-shrink">
|
||||
<div
|
||||
class="flex-shrink"
|
||||
on:click={() => {
|
||||
changeState(notification, NotificationStatus.Read)
|
||||
}}
|
||||
>
|
||||
<Component
|
||||
is={view.component.ObjectPresenter}
|
||||
props={{
|
||||
@ -82,7 +87,7 @@
|
||||
label={plugin.string.MarkAsRead}
|
||||
size={'medium'}
|
||||
action={() => {
|
||||
changeState(notification, isNew ? NotificationStatus.Read : NotificationStatus.New)
|
||||
changeState(notification, isNew ? NotificationStatus.Read : NotificationStatus.Notified)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -54,6 +54,7 @@ export interface EmailNotification extends Doc {
|
||||
*/
|
||||
export enum NotificationStatus {
|
||||
New,
|
||||
Notified,
|
||||
Read
|
||||
}
|
||||
|
||||
|
@ -13,46 +13,55 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Channel, formatName } from '@anticrm/contact'
|
||||
import attachment from '@anticrm/attachment'
|
||||
import chunter from '@anticrm/chunter'
|
||||
import contact, { Channel, formatName, Person } from '@anticrm/contact'
|
||||
import { ChannelsEditor } from '@anticrm/contact-resources'
|
||||
import { Avatar, createQuery } from '@anticrm/presentation'
|
||||
import type { Candidate } from '@anticrm/recruit'
|
||||
import { Avatar, createQuery, getClient } from '@anticrm/presentation'
|
||||
import { Component, Label, showPanel } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import chunter from '@anticrm/chunter'
|
||||
import attachment from '@anticrm/attachment'
|
||||
import recruit from '../plugin'
|
||||
|
||||
export let candidate: Candidate
|
||||
export let candidate: Person | undefined
|
||||
export let disabled: boolean = false
|
||||
|
||||
let channels: Channel[] = []
|
||||
const channelsQuery = createQuery()
|
||||
channelsQuery.query(
|
||||
contact.class.Channel,
|
||||
{
|
||||
attachedTo: candidate._id
|
||||
},
|
||||
(res) => {
|
||||
channels = res
|
||||
}
|
||||
)
|
||||
$: if (candidate !== undefined) {
|
||||
channelsQuery.query(
|
||||
contact.class.Channel,
|
||||
{
|
||||
attachedTo: candidate._id
|
||||
},
|
||||
(res) => {
|
||||
channels = res
|
||||
}
|
||||
)
|
||||
} else {
|
||||
channelsQuery.unsubscribe()
|
||||
}
|
||||
const client = getClient()
|
||||
</script>
|
||||
|
||||
<div class="flex-col h-full card-container">
|
||||
<div class="flex-col h-full flex-grow card-container">
|
||||
<div class="label uppercase"><Label label={recruit.string.Talent} /></div>
|
||||
<Avatar avatar={candidate.avatar} size={'large'} />
|
||||
<Avatar avatar={candidate?.avatar} size={'large'} />
|
||||
{#if candidate}
|
||||
<div
|
||||
class="name lines-limit-2"
|
||||
class:over-underline={!disabled}
|
||||
on:click={() => {
|
||||
if (!disabled) showPanel(view.component.EditDoc, candidate._id, candidate._class, 'content')
|
||||
if (!disabled && candidate) {
|
||||
showPanel(view.component.EditDoc, candidate._id, candidate._class, 'content')
|
||||
}
|
||||
}}
|
||||
>
|
||||
{formatName(candidate.name)}
|
||||
</div>
|
||||
<div class="description lines-limit-2">{candidate.title ?? ''}</div>
|
||||
{#if client.getHierarchy().hasMixin(candidate, recruit.mixin.Candidate)}
|
||||
{@const cand = client.getHierarchy().as(candidate, recruit.mixin.Candidate)}
|
||||
<div class="description lines-limit-2">{cand.title ?? ''}</div>
|
||||
{/if}
|
||||
<div class="description overflow-label">{candidate.city ?? ''}</div>
|
||||
<div class="footer flex flex-reverse flex-grow">
|
||||
<div class="flex-center flex-wrap">
|
||||
@ -89,6 +98,8 @@
|
||||
transition-timing-function: var(--timing-shadow);
|
||||
transition-duration: 0.15s;
|
||||
user-select: text;
|
||||
min-width: 15rem;
|
||||
min-height: 15rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--board-card-bg-hover);
|
||||
|
@ -15,10 +15,11 @@
|
||||
<script lang="ts">
|
||||
import type { Contact, Employee, Person } from '@anticrm/contact'
|
||||
import contact from '@anticrm/contact'
|
||||
import { Account, Class, Client, Doc, generateId, Ref, SortingOrder, Space } from '@anticrm/core'
|
||||
import ExpandRightDouble from '@anticrm/contact-resources/src/components/icons/ExpandRightDouble.svelte'
|
||||
import { Account, Class, Client, Doc, FindOptions, generateId, Ref, SortingOrder, Space } from '@anticrm/core'
|
||||
import { getResource, OK, Resource, Severity, Status } from '@anticrm/platform'
|
||||
import { Card, createQuery, EmployeeBox, getClient, SpaceSelector, UserBox } from '@anticrm/presentation'
|
||||
import type { Applicant, Candidate } from '@anticrm/recruit'
|
||||
import { Card, createQuery, EmployeeBox, getClient, SpaceSelect, UserBox } from '@anticrm/presentation'
|
||||
import type { Applicant, Candidate, Vacancy } from '@anticrm/recruit'
|
||||
import task, { calcRank, SpaceWithStates, State } from '@anticrm/task'
|
||||
import ui, {
|
||||
Button,
|
||||
@ -34,6 +35,9 @@
|
||||
import view from '@anticrm/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../plugin'
|
||||
import CandidateCard from './CandidateCard.svelte'
|
||||
import VacancyCard from './VacancyCard.svelte'
|
||||
import VacancyOrgPresenter from './VacancyOrgPresenter.svelte'
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let candidate: Ref<Candidate>
|
||||
@ -46,6 +50,8 @@
|
||||
|
||||
let _space = space
|
||||
|
||||
$: _candidate = candidate
|
||||
|
||||
let doc: Applicant = {
|
||||
state: '' as Ref<State>,
|
||||
doneState: null,
|
||||
@ -69,7 +75,7 @@
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
export function canClose (): boolean {
|
||||
return (preserveCandidate || candidate === undefined) && assignee === undefined
|
||||
return (preserveCandidate || _candidate === undefined) && assignee === undefined
|
||||
}
|
||||
|
||||
async function createApplication () {
|
||||
@ -92,7 +98,7 @@
|
||||
)
|
||||
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
|
||||
|
||||
const candidateInstance = await client.findOne(contact.class.Person, { _id: doc.attachedTo as Ref<Person> })
|
||||
const candidateInstance = await client.findOne(contact.class.Person, { _id: _candidate })
|
||||
if (candidateInstance === undefined) {
|
||||
throw new Error('contact not found')
|
||||
}
|
||||
@ -125,13 +131,14 @@
|
||||
|
||||
if (createMore) {
|
||||
// Prepare for next
|
||||
_candidate = '' as Ref<Candidate>
|
||||
doc = {
|
||||
state: selectedState?._id as Ref<State>,
|
||||
doneState: null,
|
||||
number: 0,
|
||||
assignee: assignee,
|
||||
rank: '',
|
||||
attachedTo: candidate,
|
||||
attachedTo: _candidate,
|
||||
attachedToClass: recruit.mixin.Candidate,
|
||||
_class: recruit.class.Applicant,
|
||||
space: _space,
|
||||
@ -152,24 +159,35 @@
|
||||
return await impl({ ...doc, space: _space }, client)
|
||||
}
|
||||
|
||||
async function validate (doc: Applicant, space: Ref<Space>, _class: Ref<Class<Doc>>): Promise<void> {
|
||||
async function validate (
|
||||
doc: Applicant,
|
||||
space: Ref<Space>,
|
||||
_class: Ref<Class<Doc>>,
|
||||
candidate: Ref<Candidate>
|
||||
): Promise<void> {
|
||||
if (doc.attachedTo !== _candidate) {
|
||||
doc.attachedTo = _candidate
|
||||
}
|
||||
const clazz = hierarchy.getClass(_class)
|
||||
const validatorMixin = hierarchy.as(clazz, view.mixin.ObjectValidator)
|
||||
if (validatorMixin?.validator != null) {
|
||||
status = await invokeValidate(validatorMixin.validator)
|
||||
} else if (clazz.extends != null) {
|
||||
await validate(doc, space, clazz.extends)
|
||||
await validate(doc, space, clazz.extends, candidate)
|
||||
} else {
|
||||
status = OK
|
||||
}
|
||||
}
|
||||
|
||||
$: validate(doc, _space, doc._class)
|
||||
$: validate(doc, _space, doc._class, _candidate)
|
||||
|
||||
let states: Array<{ id: number | string; color: number; label: string }> = []
|
||||
let selectedState: State | undefined
|
||||
let rawStates: State[] = []
|
||||
const statesQuery = createQuery()
|
||||
const spaceQuery = createQuery()
|
||||
|
||||
let vacancy: Vacancy | undefined
|
||||
|
||||
$: if (_space) {
|
||||
statesQuery.query(
|
||||
@ -180,6 +198,9 @@
|
||||
},
|
||||
{ sort: { rank: SortingOrder.Ascending } }
|
||||
)
|
||||
spaceQuery.query(recruit.class.Vacancy, { _id: _space }, (res) => {
|
||||
vacancy = res.shift()
|
||||
})
|
||||
}
|
||||
|
||||
$: if (rawStates.findIndex((it) => it._id === selectedState?._id) === -1) {
|
||||
@ -209,6 +230,22 @@
|
||||
}
|
||||
}
|
||||
)
|
||||
const orgOptions: FindOptions<Vacancy> = {
|
||||
lookup: {
|
||||
company: contact.class.Organization
|
||||
}
|
||||
}
|
||||
|
||||
const candidateQuery = createQuery()
|
||||
let _candidateInstance: Person | undefined
|
||||
|
||||
$: if (_candidate !== undefined) {
|
||||
candidateQuery.query(contact.class.Person, { _id: _candidate }, (res) => {
|
||||
_candidateInstance = res.shift()
|
||||
})
|
||||
} else {
|
||||
candidateQuery.unsubscribe()
|
||||
}
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
@ -222,53 +259,60 @@
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<SpaceSelector
|
||||
_class={recruit.class.Vacancy}
|
||||
query={{ archived: false }}
|
||||
label={recruit.string.Vacancy}
|
||||
create={{
|
||||
component: recruit.component.CreateVacancy,
|
||||
label: recruit.string.CreateVacancy
|
||||
}}
|
||||
bind:space={_space}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-row-center gap-2">
|
||||
{#if preserveCandidate}
|
||||
<UserBox
|
||||
readonly
|
||||
_class={contact.class.Person}
|
||||
options={{ sort: { modifiedOn: -1 } }}
|
||||
excluded={existingApplicants}
|
||||
label={recruit.string.Talent}
|
||||
placeholder={recruit.string.Talents}
|
||||
bind:value={doc.attachedTo}
|
||||
kind={'no-border'}
|
||||
size={'small'}
|
||||
/>
|
||||
{/if}
|
||||
<Label label={recruit.string.CreateApplication} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<StatusControl slot="error" {status} />
|
||||
<div class="candidate-vacancy">
|
||||
<div class="flex flex-stretch">
|
||||
<UserBox
|
||||
id={'vacancy.talant.selector'}
|
||||
focusIndex={1}
|
||||
readonly={preserveCandidate}
|
||||
_class={contact.class.Person}
|
||||
options={{ sort: { modifiedOn: -1 } }}
|
||||
excluded={existingApplicants}
|
||||
label={recruit.string.Talent}
|
||||
placeholder={recruit.string.Talents}
|
||||
bind:value={_candidate}
|
||||
kind={'no-border'}
|
||||
size={'small'}
|
||||
width={'100%'}
|
||||
create={{ component: recruit.component.CreateCandidate, label: recruit.string.CreateTalent }}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
<CandidateCard candidate={_candidateInstance} on:click disabled={true} />
|
||||
</svelte:fragment>
|
||||
</UserBox>
|
||||
</div>
|
||||
|
||||
<div class="flex-center">
|
||||
<ExpandRightDouble />
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<SpaceSelect
|
||||
_class={recruit.class.Vacancy}
|
||||
spaceQuery={{ archived: false }}
|
||||
spaceOptions={orgOptions}
|
||||
label={recruit.string.Vacancy}
|
||||
create={{
|
||||
component: recruit.component.CreateVacancy,
|
||||
label: recruit.string.CreateVacancy
|
||||
}}
|
||||
bind:value={_space}
|
||||
component={VacancyOrgPresenter}
|
||||
componentProps={{ inline: true }}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
<VacancyCard {vacancy} disabled={true} />
|
||||
</svelte:fragment>
|
||||
</SpaceSelect>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="pool">
|
||||
{#key doc}
|
||||
{#if !preserveCandidate}
|
||||
<UserBox
|
||||
focusIndex={1}
|
||||
_class={contact.class.Person}
|
||||
options={{ sort: { modifiedOn: -1 } }}
|
||||
excluded={existingApplicants}
|
||||
label={recruit.string.Talent}
|
||||
placeholder={recruit.string.Talents}
|
||||
bind:value={doc.attachedTo}
|
||||
kind={'no-border'}
|
||||
size={'small'}
|
||||
create={{ component: recruit.component.CreateCandidate, label: recruit.string.CreateTalent }}
|
||||
/>
|
||||
{/if}
|
||||
<EmployeeBox
|
||||
focusIndex={2}
|
||||
label={recruit.string.AssignRecruiter}
|
||||
@ -310,22 +354,11 @@
|
||||
</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; }
|
||||
.candidate-vacancy {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr 3fr;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
.color {
|
||||
margin-right: 0.375rem;
|
||||
width: 0.875rem;
|
||||
|
@ -13,41 +13,69 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment from '@anticrm/attachment'
|
||||
import chunter from '@anticrm/chunter'
|
||||
import contact, { Channel, Organization } from '@anticrm/contact'
|
||||
import { ChannelsEditor } from '@anticrm/contact-resources'
|
||||
import { Ref, WithLookup } from '@anticrm/core'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import type { Vacancy } from '@anticrm/recruit'
|
||||
import { closePanel, closePopup, closeTooltip, getCurrentLocation, Label, navigate } from '@anticrm/ui'
|
||||
import VacancyIcon from './icons/Vacancy.svelte'
|
||||
import contact, { Organization } from '@anticrm/contact'
|
||||
import { closePanel, closePopup, closeTooltip, Component, getCurrentLocation, Label, navigate } from '@anticrm/ui'
|
||||
import recruit from '../plugin'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Ref } from '@anticrm/core'
|
||||
import VacancyIcon from './icons/Vacancy.svelte'
|
||||
|
||||
export let vacancy: Vacancy
|
||||
export let vacancy: WithLookup<Vacancy> | undefined
|
||||
export let disabled: boolean = false
|
||||
export let inline: boolean = false
|
||||
let company: Organization | undefined
|
||||
|
||||
$: getOrganization(vacancy?.company)
|
||||
$: getOrganization(vacancy, vacancy?.company)
|
||||
const client = getClient()
|
||||
|
||||
async function getOrganization (_id: Ref<Organization> | undefined): Promise<void> {
|
||||
async function getOrganization (
|
||||
vacancy: WithLookup<Vacancy> | undefined,
|
||||
_id: Ref<Organization> | undefined
|
||||
): Promise<void> {
|
||||
if (vacancy?.$lookup?.company !== undefined) {
|
||||
company = vacancy.$lookup?.company
|
||||
}
|
||||
if (_id === undefined) {
|
||||
company = undefined
|
||||
} else {
|
||||
company = await client.findOne(contact.class.Organization, { _id })
|
||||
}
|
||||
}
|
||||
|
||||
let channels: Channel[] = []
|
||||
const channelsQuery = createQuery()
|
||||
$: if (vacancy?.company !== undefined) {
|
||||
channelsQuery.query(
|
||||
contact.class.Channel,
|
||||
{
|
||||
attachedTo: vacancy?.company
|
||||
},
|
||||
(res) => {
|
||||
channels = res
|
||||
}
|
||||
)
|
||||
} else {
|
||||
channelsQuery.unsubscribe()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-col h-full card-container">
|
||||
<div class="label uppercase"><Label label={recruit.string.Vacancy} /></div>
|
||||
<div class="flex-center logo">
|
||||
<VacancyIcon size={'large'} />
|
||||
</div>
|
||||
<div class="flex-col h-full card-container" class:inline>
|
||||
{#if !inline}
|
||||
<div class="label uppercase"><Label label={recruit.string.Vacancy} /></div>
|
||||
<div class="flex-center logo">
|
||||
<VacancyIcon size={'large'} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if vacancy}
|
||||
<div
|
||||
class="name lines-limit-2"
|
||||
class:over-underline={!disabled}
|
||||
on:click={() => {
|
||||
if (!disabled) {
|
||||
if (!disabled && vacancy) {
|
||||
closeTooltip()
|
||||
closePopup()
|
||||
closePanel()
|
||||
@ -58,12 +86,46 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
{vacancy.name}
|
||||
{#if inline}
|
||||
<div class="flex-row-center">
|
||||
<VacancyIcon size={'small'} />
|
||||
<span class="ml-1">
|
||||
{vacancy.name}
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
{vacancy.name}
|
||||
{/if}
|
||||
</div>
|
||||
{#if company}
|
||||
<span class="label">{company.name}</span>
|
||||
{/if}
|
||||
<div class="description lines-limit-2">{vacancy.description ?? ''}</div>
|
||||
{#if !inline || vacancy.description}
|
||||
<div class="description lines-limit-2">{vacancy.description ?? ''}</div>
|
||||
{/if}
|
||||
|
||||
<div class="footer flex flex-reverse flex-grow">
|
||||
<div class="flex-center flex-wrap">
|
||||
<Component
|
||||
is={chunter.component.CommentsPresenter}
|
||||
props={{ value: vacancy.comments, object: vacancy, size: 'medium', showCounter: true }}
|
||||
/>
|
||||
<Component
|
||||
is={attachment.component.AttachmentsPresenter}
|
||||
props={{ value: vacancy.attachments, object: vacancy, size: 'medium', showCounter: true }}
|
||||
/>
|
||||
</div>
|
||||
{#if channels[0]}
|
||||
<div class="flex flex-grow">
|
||||
<ChannelsEditor
|
||||
attachedTo={channels[0].attachedTo}
|
||||
attachedClass={channels[0].attachedToClass}
|
||||
length={'short'}
|
||||
editable={false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@ -77,6 +139,8 @@
|
||||
transition-timing-function: var(--timing-shadow);
|
||||
transition-duration: 0.15s;
|
||||
user-select: text;
|
||||
min-width: 15rem;
|
||||
min-height: 15rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--board-card-bg-hover);
|
||||
@ -85,8 +149,8 @@
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
color: var(--primary-button-color);
|
||||
background-color: var(--primary-button-enabled);
|
||||
border-radius: 50%;
|
||||
@ -107,5 +171,22 @@
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
|
||||
&.inline {
|
||||
padding: 0.5rem 0.5rem 0.25rem;
|
||||
min-width: 1rem;
|
||||
min-height: 1rem;
|
||||
|
||||
background-color: inherit;
|
||||
border: inherit;
|
||||
border-radius: inherit;
|
||||
.name {
|
||||
margin: 0.25rem 0 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.label {
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,26 @@
|
||||
<!--
|
||||
// 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 { WithLookup } from '@anticrm/core'
|
||||
import type { Vacancy } from '@anticrm/recruit'
|
||||
import VacancyCard from './VacancyCard.svelte'
|
||||
|
||||
export let value: WithLookup<Vacancy>
|
||||
export let inline: boolean = false
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<VacancyCard vacancy={value} disabled {inline} />
|
||||
{/if}
|
@ -30,6 +30,7 @@ export interface Vacancy extends SpaceWithStates {
|
||||
dueTo?: Timestamp
|
||||
location?: string
|
||||
company?: Ref<Organization>
|
||||
comments?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -53,7 +53,6 @@
|
||||
},
|
||||
{
|
||||
sort: { [orderByKey]: issuesSortOrderMap[orderByKey] },
|
||||
limit: 200,
|
||||
lookup: {
|
||||
assignee: contact.class.Employee,
|
||||
status: tracker.class.IssueStatus,
|
||||
|
@ -122,19 +122,25 @@
|
||||
if (presenter === undefined) return
|
||||
|
||||
if (useMixinProxy) {
|
||||
result.push({
|
||||
const newValue = {
|
||||
value: attribute.attributeOf + '.' + attribute.name,
|
||||
label: attribute.label,
|
||||
enabled: false,
|
||||
_class: attribute.attributeOf
|
||||
})
|
||||
}
|
||||
if (result.find((it) => it._class === newValue._class && it.value === newValue.value) === undefined) {
|
||||
result.push(newValue)
|
||||
}
|
||||
} else {
|
||||
result.push({
|
||||
const newValue = {
|
||||
value,
|
||||
label: attribute.label,
|
||||
enabled: false,
|
||||
_class: attribute.attributeOf
|
||||
})
|
||||
}
|
||||
if (result.find((it) => it._class === newValue._class && it.value === newValue.value) === undefined) {
|
||||
result.push(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
function getConfig (viewlet: Viewlet, preference: ViewletPreference | undefined): AttributeConfig[] {
|
||||
|
@ -126,7 +126,7 @@
|
||||
notification.class.Notification,
|
||||
{
|
||||
attachedTo: account.employee,
|
||||
status: NotificationStatus.New
|
||||
status: { $nin: [NotificationStatus.Read] }
|
||||
},
|
||||
(res) => {
|
||||
hasNotification = res.length > 0
|
||||
|
@ -13,25 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Class, Doc, Ref, Space } from '@anticrm/core'
|
||||
import type { Doc, Ref, Space } from '@anticrm/core'
|
||||
import core from '@anticrm/core'
|
||||
import notification from '@anticrm/notification'
|
||||
import { NotificationClientImpl } from '@anticrm/notification-resources'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import {
|
||||
Action,
|
||||
AnyComponent,
|
||||
getCurrentLocation,
|
||||
IconAdd,
|
||||
IconEdit,
|
||||
IconSearch,
|
||||
navigate,
|
||||
showPanel,
|
||||
showPopup
|
||||
} from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import preference from '@anticrm/preference'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Action, getCurrentLocation, IconAdd, IconEdit, IconSearch, navigate, showPopup } from '@anticrm/ui'
|
||||
import { getActions as getContributedActions } from '@anticrm/view-resources'
|
||||
import { SpacesNavModel } from '@anticrm/workbench'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@ -69,16 +58,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
const editSpace: Action = {
|
||||
label: plugin.string.Open,
|
||||
icon: IconEdit,
|
||||
action: async (_id: Ref<Doc>): Promise<void> => {
|
||||
const editor = await getEditor(model.spaceClass)
|
||||
dispatch('open')
|
||||
showPanel(editor ?? plugin.component.SpacePanel, _id, model.spaceClass, 'content')
|
||||
}
|
||||
}
|
||||
|
||||
const starSpace: Action = {
|
||||
label: preference.string.Star,
|
||||
icon: preference.icon.Star,
|
||||
@ -89,27 +68,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function getEditor (_class: Ref<Class<Doc>>): Promise<AnyComponent | undefined> {
|
||||
const hierarchy = client.getHierarchy()
|
||||
const clazz = hierarchy.getClass(_class)
|
||||
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditor)
|
||||
if (editorMixin?.editor == null && clazz.extends != null) return getEditor(clazz.extends)
|
||||
return editorMixin.editor
|
||||
}
|
||||
|
||||
function selectSpace (id: Ref<Space>, spaceSpecial?: string) {
|
||||
dispatch('space', { space: id, spaceSpecial })
|
||||
}
|
||||
|
||||
async function getActions (space: Space): Promise<Action[]> {
|
||||
const result = [editSpace, starSpace]
|
||||
const result = [starSpace]
|
||||
|
||||
const extraActions = await getContributedActions(client, space, core.class.Space)
|
||||
for (const act of extraActions) {
|
||||
result.push({
|
||||
icon: act.icon ?? IconEdit,
|
||||
label: act.label,
|
||||
action: async (evt: Event) => {
|
||||
action: async (ctx: any, evt: Event) => {
|
||||
const impl = await getResource(act.action)
|
||||
await impl(space, evt, act.actionProps)
|
||||
}
|
||||
|
1
tests/sanity/.gitignore
vendored
1
tests/sanity/.gitignore
vendored
@ -1 +1,2 @@
|
||||
playwright-report
|
||||
test-results/*
|
@ -81,7 +81,7 @@ test.describe('recruit tests', () => {
|
||||
// await page.click('.applications-container .flex-row-center .flex-center')
|
||||
await page.click('button[id="appls.add"]')
|
||||
|
||||
await page.click('button[id = "space.selector"]')
|
||||
await page.click('[id = "space.selector"]')
|
||||
|
||||
await page.fill('[placeholder="Search..."]', vacancyId)
|
||||
await page.click(`button:has-text("${vacancyId}")`)
|
||||
@ -110,7 +110,7 @@ test.describe('recruit tests', () => {
|
||||
|
||||
// Create Applicatio n1
|
||||
await page.click('button:has-text("Application")')
|
||||
await page.click('form[id="recruit:string:CreateApplication"] button:has-text("Talent")')
|
||||
await page.click('form[id="recruit:string:CreateApplication"] [id="vacancy.talant.selector"]')
|
||||
await page.click('button:has-text("Alex P.")')
|
||||
await page.click('form[id="recruit:string:CreateApplication"] button:has-text("Create")')
|
||||
await page.waitForSelector('form.antiCard', { state: 'detached' })
|
||||
|
Loading…
Reference in New Issue
Block a user