mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
Tracker assign notification (#2269)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
0f3ce39cae
commit
13b6459c95
@ -146,7 +146,7 @@ export function createModel (builder: Builder): void {
|
||||
core.space.Model,
|
||||
{
|
||||
label: notification.string.BrowserNotification,
|
||||
default: false
|
||||
default: true
|
||||
},
|
||||
notification.ids.BrowserNotification
|
||||
)
|
||||
|
@ -19,6 +19,7 @@ import core from '@anticrm/core'
|
||||
import lead from '@anticrm/lead'
|
||||
import view from '@anticrm/view'
|
||||
import serverLead from '@anticrm/server-lead'
|
||||
import serverCore from '@anticrm/server-core'
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.mixin(lead.class.Lead, core.class.Class, view.mixin.HTMLPresenter, {
|
||||
@ -28,4 +29,8 @@ export function createModel (builder: Builder): void {
|
||||
builder.mixin(lead.class.Lead, core.class.Class, view.mixin.TextPresenter, {
|
||||
presenter: serverLead.function.LeadTextPresenter
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverLead.trigger.OnLeadUpdate
|
||||
})
|
||||
}
|
||||
|
@ -39,6 +39,6 @@ export function createModel (builder: Builder): void {
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverRecruit.trigger.OnVacancyUpdate
|
||||
trigger: serverRecruit.trigger.OnRecruitUpdate
|
||||
})
|
||||
}
|
||||
|
@ -565,4 +565,13 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
task.action.ArchiveState
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
notification.class.NotificationType,
|
||||
core.space.Model,
|
||||
{
|
||||
label: task.string.Assigned
|
||||
},
|
||||
task.ids.AssigneedNotification
|
||||
)
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export enum Severity {
|
||||
* Status of an operation
|
||||
* @public
|
||||
*/
|
||||
export class Status<P = {}> {
|
||||
export class Status<P extends Record<string, any> = {}> {
|
||||
readonly severity: Severity
|
||||
readonly code: StatusCode<P>
|
||||
readonly params: P
|
||||
|
@ -418,8 +418,8 @@
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
padding-bottom: 0.5rem;
|
||||
min-width: 32rem;
|
||||
max-width: 32rem;
|
||||
min-width: 42rem;
|
||||
max-width: 42rem;
|
||||
min-height: 22rem;
|
||||
max-height: 22rem;
|
||||
background: var(--popup-bg-color);
|
||||
|
@ -23,6 +23,7 @@
|
||||
export let labelProps: any = undefined
|
||||
export let direction: TooltipAlignment | undefined = undefined
|
||||
export let icon: Asset | AnySvelteComponent
|
||||
export let iconProps: any | undefined = undefined
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
export let action: (ev: MouseEvent) => Promise<void> | void = async () => {}
|
||||
export let invisible: boolean = false
|
||||
@ -35,7 +36,7 @@
|
||||
on:click|stopPropagation|preventDefault={action}
|
||||
>
|
||||
<div class="icon {size}" class:invisible>
|
||||
<Icon {icon} {size} />
|
||||
<Icon {icon} {size} {iconProps} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
@ -19,6 +19,7 @@
|
||||
export let icon: Asset | AnySvelteComponent
|
||||
export let size: IconSize
|
||||
export let fill = 'currentColor'
|
||||
export let iconProps: any | undefined = undefined
|
||||
|
||||
function isAsset (icon: Asset | AnySvelteComponent): boolean {
|
||||
return typeof icon === 'string'
|
||||
@ -36,6 +37,6 @@
|
||||
<svg class="svg-{size}" {fill}>
|
||||
<use href={url} />
|
||||
</svg>
|
||||
{:else}
|
||||
<svelte:component this={icon} {size} {fill} />
|
||||
{:else if typeof icon !== 'string'}
|
||||
<svelte:component this={icon} {size} {fill} {...iconProps} />
|
||||
{/if}
|
||||
|
@ -16,8 +16,17 @@
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
export let kind: 'strong' | 'curve' = 'strong'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="10.6,14.4 4.3,8 10.6,1.6 11.4,2.4 5.7,8 11.4,13.6 " />
|
||||
</svg>
|
||||
{#if kind === 'strong'}
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<polygon points="10.6,14.4 4.3,8 10.6,1.6 11.4,2.4 5.7,8 11.4,13.6 " />
|
||||
</svg>
|
||||
{:else if kind === 'curve'}
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.66601 5.33333L2.31246 5.68688L1.95891 5.33333L2.31246 4.97978L2.66601 5.33333ZM5.99935 13.1667C5.72321 13.1667 5.49935 12.9428 5.49935 12.6667C5.49935 12.3905 5.72321 12.1667 5.99935 12.1667L5.99935 13.1667ZM5.6458 9.02022L2.31246 5.68688L3.01957 4.97978L6.3529 8.31311L5.6458 9.02022ZM2.31246 4.97978L5.6458 1.64644L6.3529 2.35355L3.01957 5.68688L2.31246 4.97978ZM2.66601 4.83333L9.66601 4.83333L9.66601 5.83333L2.66602 5.83333L2.66601 4.83333ZM9.66602 13.1667L5.99935 13.1667L5.99935 12.1667L9.66602 12.1667L9.66602 13.1667ZM13.8327 9C13.8327 11.3012 11.9672 13.1667 9.66602 13.1667L9.66602 12.1667C11.4149 12.1667 12.8327 10.7489 12.8327 9L13.8327 9ZM9.66601 4.83333C11.9672 4.83333 13.8327 6.69881 13.8327 9L12.8327 9C12.8327 7.2511 11.4149 5.83333 9.66601 5.83333L9.66601 4.83333Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
|
@ -42,6 +42,7 @@
|
||||
export let viewlets: Map<ActivityKey, TxViewlet>
|
||||
export let showIcon: boolean = true
|
||||
export let isNew: boolean = false
|
||||
export let showDocument = false
|
||||
|
||||
let ptx: DisplayTx | undefined
|
||||
|
||||
@ -212,8 +213,8 @@
|
||||
{/if}
|
||||
<div class="strong">
|
||||
<div class="flex flex-wrap gap-2" class:emphasized={value.added.length > 1}>
|
||||
{#each value.added as value}
|
||||
<svelte:component this={m.presenter} {value} />
|
||||
{#each value.added as cvalue}
|
||||
<svelte:component this={m.presenter} value={cvalue} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@ -231,8 +232,8 @@
|
||||
{/if}
|
||||
<div class="strong">
|
||||
<div class="flex flex-wrap gap-2 flex-grow" class:emphasized={value.removed.length > 1}>
|
||||
{#each value.removed as value}
|
||||
<svelte:component this={m.presenter} {value} />
|
||||
{#each value.removed as cvalue}
|
||||
<svelte:component this={m.presenter} value={cvalue} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,17 +18,20 @@
|
||||
import view from '@anticrm/view'
|
||||
|
||||
export let value: Event
|
||||
export let inline: boolean = false
|
||||
|
||||
function click (): void {
|
||||
showPanel(view.component.EditDoc, value._id, value._class, 'content')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiSelect w-full cursor-pointer flex-between" on:click={click}>
|
||||
<div class="antiSelect w-full cursor-pointer flex-center flex-between" on:click={click}>
|
||||
{#if value}
|
||||
<div class="mr-4">
|
||||
{value.title}
|
||||
</div>
|
||||
<DateTimeRangePresenter value={value.date} />
|
||||
{#if !inline}
|
||||
<DateTimeRangePresenter value={value.date} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -37,11 +37,13 @@
|
||||
{#await getEvent(tx.objectId) then event}
|
||||
{#if event}
|
||||
<span
|
||||
class="over-underline caption-color"
|
||||
class="over-underline caption-color flex-row-center"
|
||||
on:click={() => {
|
||||
click(event)
|
||||
}}>{event.title}</span
|
||||
> 
|
||||
}}
|
||||
>{event.title}
|
||||
</span>
|
||||
 
|
||||
<DateTimePresenter value={event.date} />
|
||||
{/if}
|
||||
{/await}
|
||||
|
@ -9,6 +9,10 @@
|
||||
"PlatformNotification": "in platform",
|
||||
"Track": "Track",
|
||||
"DontTrack": "Don't track",
|
||||
"BrowserNotification": "in browser"
|
||||
"BrowserNotification": "in browser",
|
||||
"Remove": "Delete notification",
|
||||
"RemoveAll": "Delete all notifications",
|
||||
"MarkAllAsRead": "Mark all notifications as read",
|
||||
"MarkAsRead": "Mark as read"
|
||||
}
|
||||
}
|
@ -9,6 +9,10 @@
|
||||
"PlatformNotification": "в системе",
|
||||
"Track": "Отслеживать",
|
||||
"DontTrack": "Не отслеживать",
|
||||
"BrowserNotification": "в браузере"
|
||||
"BrowserNotification": "в браузере",
|
||||
"Remove": "Удалить нотификацию",
|
||||
"RemoveAll": "Удалить все нотификации",
|
||||
"MarkAllAsRead": "Отметить все нотификации как прочитанные",
|
||||
"MarkAsRead": "Отметить нотификация прочитанной"
|
||||
}
|
||||
}
|
@ -39,6 +39,7 @@
|
||||
"@anticrm/activity-resources": "~0.6.0",
|
||||
"@anticrm/activity": "~0.6.0",
|
||||
"@anticrm/contact": "~0.6.5",
|
||||
"@anticrm/core": "~0.6.16"
|
||||
"@anticrm/core": "~0.6.16",
|
||||
"@anticrm/view": "~0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
NotificationType
|
||||
} from '@anticrm/notification'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { getCurrentLocation } from '@anticrm/ui'
|
||||
import notification from '../plugin'
|
||||
import { NotificationClientImpl } from '../utils'
|
||||
|
||||
@ -39,7 +40,7 @@
|
||||
let settings: Map<Ref<NotificationType>, NotificationSetting> = new Map<Ref<NotificationType>, NotificationSetting>()
|
||||
let provider: NotificationProvider | undefined
|
||||
|
||||
const enabled = 'Notification' in window && Notification.permission !== 'denied'
|
||||
$: enabled = 'Notification' in window && Notification?.permission !== 'denied'
|
||||
|
||||
$: enabled &&
|
||||
providersQuery.query(
|
||||
@ -66,6 +67,8 @@
|
||||
}
|
||||
)
|
||||
|
||||
const alreadyShown = new Set<Ref<PlatformNotification>>()
|
||||
|
||||
$: enabled &&
|
||||
settingsReceived &&
|
||||
provider !== undefined &&
|
||||
@ -76,7 +79,12 @@
|
||||
status: NotificationStatus.New
|
||||
},
|
||||
(res) => {
|
||||
process(res)
|
||||
process(res.reverse())
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
modifiedOn: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -93,25 +101,40 @@
|
||||
const enabled = setting?.enabled ?? provider?.default
|
||||
if (!enabled) return
|
||||
if ((setting?.modifiedOn ?? notification.modifiedOn) < 0) return
|
||||
if (Notification.permission === 'granted') {
|
||||
|
||||
if (Notification?.permission !== 'granted') {
|
||||
await Notification?.requestPermission()
|
||||
}
|
||||
|
||||
if (Notification?.permission === 'granted') {
|
||||
await notify(text, notification)
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
const permission = await Notification.requestPermission()
|
||||
if (permission === 'granted') {
|
||||
await notify(text, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function notify (text: string, notification: PlatformNotification): Promise<void> {
|
||||
let clearTimer: number | undefined
|
||||
|
||||
async function notify (text: string, notifyInstance: PlatformNotification): Promise<void> {
|
||||
if (alreadyShown.has(notifyInstance._id)) {
|
||||
return
|
||||
}
|
||||
alreadyShown.add(notifyInstance._id)
|
||||
|
||||
if (clearTimer) {
|
||||
clearTimeout(clearTimer)
|
||||
}
|
||||
|
||||
clearTimer = setTimeout(() => {
|
||||
alreadyShown.clear()
|
||||
}, 5000)
|
||||
|
||||
const lastView = $lastViews.get(lastViewId)
|
||||
if ((lastView ?? notification.modifiedOn) > 0) {
|
||||
if ((lastView ?? notifyInstance.modifiedOn) > 0) {
|
||||
// eslint-disable-next-line
|
||||
new Notification(text, { tag: notification._id })
|
||||
new Notification(getCurrentLocation().path[1], { tag: notifyInstance._id, icon: '/favicon.png', body: text })
|
||||
await notificationClient.updateLastView(
|
||||
lastViewId,
|
||||
contact.class.Employee,
|
||||
notification.modifiedOn,
|
||||
notifyInstance.modifiedOn,
|
||||
lastView === undefined
|
||||
)
|
||||
}
|
||||
|
@ -16,46 +16,82 @@
|
||||
<script lang="ts">
|
||||
import { TxViewlet } from '@anticrm/activity'
|
||||
import { ActivityKey, DisplayTx, getCollectionTx, newDisplayTx, TxView } from '@anticrm/activity-resources'
|
||||
import core, { AttachedDoc, Doc, TxCollectionCUD } from '@anticrm/core'
|
||||
import core, { AttachedDoc, Doc, TxCollectionCUD, WithLookup } from '@anticrm/core'
|
||||
import { Notification, NotificationStatus } from '@anticrm/notification'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { ActionIcon, Component, getPlatformColor, IconBack, IconCheck, IconDelete } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import plugin from '../plugin'
|
||||
|
||||
export let notification: Notification
|
||||
export let notification: WithLookup<Notification>
|
||||
export let viewlets: Map<ActivityKey, TxViewlet>
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
async function getDisplayTx (notification: Notification): Promise<DisplayTx | undefined> {
|
||||
let tx = await client.findOne(core.class.TxCUD, { _id: notification.tx })
|
||||
if (tx === undefined) return
|
||||
if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
|
||||
tx = getCollectionTx(tx as TxCollectionCUD<Doc, AttachedDoc>)
|
||||
function getDisplayTx (notification: WithLookup<Notification>): DisplayTx | undefined {
|
||||
let tx = notification.$lookup?.tx
|
||||
if (tx) {
|
||||
if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
|
||||
tx = getCollectionTx(tx as TxCollectionCUD<Doc, AttachedDoc>)
|
||||
}
|
||||
return newDisplayTx(tx, hierarchy)
|
||||
}
|
||||
return newDisplayTx(tx, hierarchy)
|
||||
}
|
||||
|
||||
async function read (notification: Notification): Promise<void> {
|
||||
if (notification.status === NotificationStatus.Read) return
|
||||
async function changeState (notification: Notification, status: NotificationStatus): Promise<void> {
|
||||
if (notification.status === status) return
|
||||
await client.updateDoc(notification._class, notification.space, notification._id, {
|
||||
status: NotificationStatus.Read
|
||||
status
|
||||
})
|
||||
}
|
||||
|
||||
$: displayTx = getDisplayTx(notification)
|
||||
</script>
|
||||
|
||||
{#await getDisplayTx(notification) then displayTx}
|
||||
{#if displayTx}
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<div
|
||||
class="content"
|
||||
class:isNew={notification.status !== NotificationStatus.Read}
|
||||
on:mouseover|once={() => {
|
||||
read(notification)
|
||||
}}
|
||||
>
|
||||
{#if displayTx}
|
||||
{@const isNew = notification.status === NotificationStatus.New}
|
||||
<!-- 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">
|
||||
<Component
|
||||
is={view.component.ObjectPresenter}
|
||||
props={{
|
||||
objectId: displayTx.tx.objectId,
|
||||
_class: displayTx.tx.objectClass,
|
||||
value: displayTx.doc,
|
||||
inline: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-reverse flex-gap-3 flex-grow">
|
||||
<ActionIcon
|
||||
icon={IconDelete}
|
||||
label={plugin.string.Remove}
|
||||
size={'medium'}
|
||||
action={() => {
|
||||
client.remove(notification)
|
||||
}}
|
||||
/>
|
||||
<ActionIcon
|
||||
icon={isNew ? IconCheck : IconBack}
|
||||
iconProps={!isNew ? { kind: 'curve' } : {}}
|
||||
label={plugin.string.MarkAsRead}
|
||||
size={'medium'}
|
||||
action={() => {
|
||||
changeState(notification, isNew ? NotificationStatus.Read : NotificationStatus.New)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TxView tx={displayTx} {viewlets} showIcon={false} />
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
@ -63,7 +99,14 @@
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.isNew {
|
||||
border: 1px solid var(--theme-bg-focused-border);
|
||||
.notify {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
outline: 1px solid transparent;
|
||||
outline-offset: 2px;
|
||||
transition: all 0.1s ease-in-out;
|
||||
z-index: -1;
|
||||
background-color: currentColor;
|
||||
}
|
||||
</style>
|
||||
|
@ -17,16 +17,17 @@
|
||||
import activity, { TxViewlet } from '@anticrm/activity'
|
||||
import { activityKey, ActivityKey } from '@anticrm/activity-resources'
|
||||
import { EmployeeAccount } from '@anticrm/contact'
|
||||
import { getCurrentAccount, SortingOrder } from '@anticrm/core'
|
||||
import type { Notification } from '@anticrm/notification'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { Scroller } from '@anticrm/ui'
|
||||
import core, { getCurrentAccount, WithLookup } from '@anticrm/core'
|
||||
import { Notification, NotificationStatus } from '@anticrm/notification'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { ActionIcon, IconCheck, IconDelete, Scroller } from '@anticrm/ui'
|
||||
import Label from '@anticrm/ui/src/components/Label.svelte'
|
||||
import notification from '../plugin'
|
||||
import NotificationView from './NotificationView.svelte'
|
||||
|
||||
const query = createQuery()
|
||||
let notifications: Notification[] = []
|
||||
let notifications: WithLookup<Notification>[] = []
|
||||
const client = getClient()
|
||||
|
||||
$: query.query(
|
||||
notification.class.Notification,
|
||||
@ -37,7 +38,13 @@
|
||||
notifications = res
|
||||
},
|
||||
{
|
||||
sort: { status: SortingOrder.Ascending, modifiedOn: SortingOrder.Descending }
|
||||
sort: {
|
||||
'$lookup.tx.modifiedOn': -1
|
||||
},
|
||||
limit: 30,
|
||||
lookup: {
|
||||
tx: core.class.TxCUD
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -47,16 +54,51 @@
|
||||
$: descriptors.query(activity.class.TxViewlet, {}, (result) => {
|
||||
viewlets = new Map(result.map((r) => [activityKey(r.objectClass, r.txClass), r]))
|
||||
})
|
||||
|
||||
const deleteNotifications = async () => {
|
||||
const allNotifications = await client.findAll(notification.class.Notification, {
|
||||
attachedTo: (getCurrentAccount() as EmployeeAccount).employee
|
||||
})
|
||||
for (const n of allNotifications) {
|
||||
await client.remove(n)
|
||||
}
|
||||
}
|
||||
const markAsReadNotifications = async () => {
|
||||
const allNotifications = await client.findAll(notification.class.Notification, {
|
||||
attachedTo: (getCurrentAccount() as EmployeeAccount).employee
|
||||
})
|
||||
for (const n of allNotifications) {
|
||||
if (n.status !== NotificationStatus.Read) {
|
||||
await client.updateDoc(n._class, n.space, n._id, {
|
||||
status: NotificationStatus.Read
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="notifyPopup" class:justify-center={notifications.length === 0}>
|
||||
<div class="header">
|
||||
<div class="header flex-between">
|
||||
<span class="fs-title overflow-label"><Label label={notification.string.Notifications} /></span>
|
||||
<div class="flex flex-gap-2">
|
||||
<ActionIcon
|
||||
icon={IconCheck}
|
||||
label={notification.string.MarkAllAsRead}
|
||||
size={'medium'}
|
||||
action={markAsReadNotifications}
|
||||
/>
|
||||
<ActionIcon
|
||||
icon={IconDelete}
|
||||
label={notification.string.RemoveAll}
|
||||
size={'medium'}
|
||||
action={deleteNotifications}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if notifications.length > 0}
|
||||
<Scroller>
|
||||
<div class="px-2 clear-mins">
|
||||
{#each notifications as n (n._id)}
|
||||
{#each notifications as n}
|
||||
<NotificationView notification={n} {viewlets} />
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -23,6 +23,10 @@ export default mergeIds(notificationId, notification, {
|
||||
string: {
|
||||
NoNotifications: '' as IntlString,
|
||||
Track: '' as IntlString,
|
||||
DontTrack: '' as IntlString
|
||||
DontTrack: '' as IntlString,
|
||||
Remove: '' as IntlString,
|
||||
RemoveAll: '' as IntlString,
|
||||
MarkAsRead: '' as IntlString,
|
||||
MarkAllAsRead: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -31,6 +31,7 @@
|
||||
"@anticrm/contact": "~0.6.5",
|
||||
"@anticrm/view": "~0.6.0",
|
||||
"@anticrm/ui": "~0.6.0",
|
||||
"lexorank": "~1.0.4"
|
||||
"lexorank": "~1.0.4",
|
||||
"@anticrm/notification": "~0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import { plugin } from '@anticrm/platform'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
import { ViewletDescriptor } from '@anticrm/view'
|
||||
import { genRanks } from './utils'
|
||||
import { NotificationType } from '@anticrm/notification'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -270,6 +271,9 @@ const task = plugin(taskId, {
|
||||
KanbanTemplateEditor: '' as AnyComponent,
|
||||
KanbanTemplateSelector: '' as AnyComponent,
|
||||
TodoItemsPopup: '' as AnyComponent
|
||||
},
|
||||
ids: {
|
||||
AssigneedNotification: '' as Ref<NotificationType>
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -12,11 +12,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { Client, Doc, Ref } from '@anticrm/core'
|
||||
import type { IntlString, Resource } from '@anticrm/platform'
|
||||
import { mergeIds } from '@anticrm/platform'
|
||||
import tracker, { trackerId } from '../../tracker/lib'
|
||||
import { AnyComponent } from '@anticrm/ui'
|
||||
import { Client, Doc, Ref } from '@anticrm/core'
|
||||
import tracker, { trackerId } from '../../tracker/lib'
|
||||
|
||||
export default mergeIds(trackerId, tracker, {
|
||||
string: {
|
||||
|
@ -15,11 +15,11 @@
|
||||
|
||||
import { Employee } from '@anticrm/contact'
|
||||
import type { AttachedDoc, Class, Doc, Markup, Ref, RelatedDocument, Space, Timestamp, Type } from '@anticrm/core'
|
||||
import { Action, ActionCategory } from '@anticrm/view'
|
||||
import type { Asset, IntlString, Plugin, Resource } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import { AnyComponent, Location } from '@anticrm/ui'
|
||||
import type { TagCategory } from '@anticrm/tags'
|
||||
import { AnyComponent, Location } from '@anticrm/ui'
|
||||
import { Action, ActionCategory } from '@anticrm/view'
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, FindResult, getObjectValue, Ref, RefTo, SortingOrder } from '@anticrm/core'
|
||||
import { Doc, FindResult, getObjectValue, RefTo, SortingOrder } from '@anticrm/core'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import presentation, { getClient } from '@anticrm/presentation'
|
||||
import type { State } from '@anticrm/task'
|
||||
@ -24,7 +24,6 @@
|
||||
import view from '../../plugin'
|
||||
import { buildConfigLookup, getPresenter } from '../../utils'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let filter: Filter
|
||||
export let onChange: (e: Filter) => void
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
* path segment in the "$schema" field for all your Rush config files. This will ensure
|
||||
* correct error-underlining and tab-completion for editors such as VS Code.
|
||||
*/
|
||||
"rushVersion": "5.71.0",
|
||||
"rushVersion": "5.77.3",
|
||||
|
||||
/**
|
||||
* The next field selects which package manager should be installed and determines its version.
|
||||
@ -26,7 +26,7 @@
|
||||
* Specify one of: "pnpmVersion", "npmVersion", or "yarnVersion". See the Rush documentation
|
||||
* for details about these alternatives.
|
||||
*/
|
||||
"pnpmVersion": "6.32.19",
|
||||
"pnpmVersion": "6.34.0",
|
||||
|
||||
// "npmVersion": "4.5.0",
|
||||
// "yarnVersion": "1.9.4",
|
||||
|
@ -32,6 +32,8 @@
|
||||
"@anticrm/lead": "~0.6.0",
|
||||
"@anticrm/view": "~0.6.0",
|
||||
"@anticrm/login": "~0.6.1",
|
||||
"@anticrm/workbench": "~0.6.1"
|
||||
"@anticrm/workbench": "~0.6.1",
|
||||
"@anticrm/contact": "~0.6.5",
|
||||
"@anticrm/server-task-resources": "~0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -13,12 +13,23 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Doc } from '@anticrm/core'
|
||||
import { leadId, Lead } from '@anticrm/lead'
|
||||
import core, {
|
||||
AttachedDoc,
|
||||
Doc,
|
||||
Tx,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxProcessor,
|
||||
TxUpdateDoc
|
||||
} from '@anticrm/core'
|
||||
import lead, { leadId, Lead } from '@anticrm/lead'
|
||||
import login from '@anticrm/login'
|
||||
import { getMetadata } from '@anticrm/platform'
|
||||
import { TriggerControl } from '@anticrm/server-core'
|
||||
import view from '@anticrm/view'
|
||||
import { workbenchId } from '@anticrm/workbench'
|
||||
import { addAssigneeNotification } from '@anticrm/server-task-resources'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -34,7 +45,64 @@ export function leadHTMLPresenter (doc: Doc): string {
|
||||
*/
|
||||
export function leadTextPresenter (doc: Doc): string {
|
||||
const lead = doc as Lead
|
||||
return `${lead.title}`
|
||||
return `LEAD-${lead.number}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnLeadUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
|
||||
const res: Tx[] = []
|
||||
|
||||
const cud = actualTx as TxCUD<Doc>
|
||||
|
||||
if (actualTx._class === core.class.TxCreateDoc) {
|
||||
await handleLeadCreate(control, cud, res, tx)
|
||||
}
|
||||
|
||||
if (actualTx._class === core.class.TxUpdateDoc) {
|
||||
await handleLeadUpdate(control, cud, res, tx)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async function handleLeadCreate (control: TriggerControl, cud: TxCUD<Doc>, res: Tx[], tx: Tx): Promise<void> {
|
||||
if (control.hierarchy.isDerived(cud.objectClass, lead.class.Lead)) {
|
||||
const createTx = cud as TxCreateDoc<Lead>
|
||||
const leadValue = TxProcessor.createDoc2Doc(createTx)
|
||||
if (leadValue.assignee != null) {
|
||||
await addAssigneeNotification(
|
||||
control,
|
||||
res,
|
||||
leadValue,
|
||||
leadTextPresenter(leadValue),
|
||||
leadValue.assignee,
|
||||
tx as TxCollectionCUD<Lead, AttachedDoc>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLeadUpdate (control: TriggerControl, cud: TxCUD<Doc>, res: Tx[], tx: Tx): Promise<void> {
|
||||
if (control.hierarchy.isDerived(cud.objectClass, lead.class.Lead)) {
|
||||
const updateTx = cud as TxUpdateDoc<Lead>
|
||||
if (updateTx.operations.assignee != null) {
|
||||
const leadValue = (await control.findAll(lead.class.Lead, { _id: updateTx.objectId }, { limit: 1 })).shift()
|
||||
|
||||
if (leadValue?.assignee != null) {
|
||||
await addAssigneeNotification(
|
||||
control,
|
||||
res,
|
||||
leadValue,
|
||||
leadTextPresenter(leadValue),
|
||||
leadValue.assignee,
|
||||
tx as TxCollectionCUD<Lead, AttachedDoc>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
@ -42,5 +110,8 @@ export default async () => ({
|
||||
function: {
|
||||
LeadHTMLPresenter: leadHTMLPresenter,
|
||||
LeadTextPresenter: leadTextPresenter
|
||||
},
|
||||
trigger: {
|
||||
OnLeadUpdate
|
||||
}
|
||||
})
|
||||
|
@ -29,6 +29,8 @@
|
||||
"dependencies": {
|
||||
"@anticrm/core": "~0.6.16",
|
||||
"@anticrm/platform": "~0.6.6",
|
||||
"@anticrm/server-core": "~0.6.1"
|
||||
"@anticrm/server-core": "~0.6.1",
|
||||
"@anticrm/contact": "~0.6.5",
|
||||
"@anticrm/server-task-resources": "~0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,10 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Resource, Plugin } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import { Doc } from '@anticrm/core'
|
||||
import type { Plugin, Resource } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import { TriggerFunc } from '@anticrm/server-core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -29,5 +30,8 @@ export default plugin(serverLeadId, {
|
||||
function: {
|
||||
LeadHTMLPresenter: '' as Resource<(doc: Doc) => string>,
|
||||
LeadTextPresenter: '' as Resource<(doc: Doc) => string>
|
||||
},
|
||||
trigger: {
|
||||
OnLeadUpdate: '' as Resource<TriggerFunc>
|
||||
}
|
||||
})
|
||||
|
@ -33,6 +33,7 @@
|
||||
"@anticrm/view": "~0.6.0",
|
||||
"@anticrm/login": "~0.6.1",
|
||||
"@anticrm/workbench": "~0.6.1",
|
||||
"@anticrm/contact": "~0.6.5"
|
||||
"@anticrm/contact": "~0.6.5",
|
||||
"@anticrm/server-task-resources": "~0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,22 @@
|
||||
//
|
||||
|
||||
import contact from '@anticrm/contact'
|
||||
import core, { Doc, Tx, TxCreateDoc, TxProcessor, TxRemoveDoc, TxUpdateDoc } from '@anticrm/core'
|
||||
import core, {
|
||||
AttachedDoc,
|
||||
Doc,
|
||||
Tx,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxProcessor,
|
||||
TxRemoveDoc,
|
||||
TxUpdateDoc
|
||||
} from '@anticrm/core'
|
||||
import login from '@anticrm/login'
|
||||
import { getMetadata } from '@anticrm/platform'
|
||||
import recruit, { Applicant, recruitId, Vacancy } from '@anticrm/recruit'
|
||||
import { TriggerControl } from '@anticrm/server-core'
|
||||
import { addAssigneeNotification } from '@anticrm/server-task-resources'
|
||||
import view from '@anticrm/view'
|
||||
import { workbenchId } from '@anticrm/workbench'
|
||||
|
||||
@ -59,83 +70,50 @@ export function applicationTextPresenter (doc: Doc): string {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnVacancyUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
export async function OnRecruitUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
|
||||
const res: Tx[] = []
|
||||
|
||||
const cud = actualTx as TxCUD<Doc>
|
||||
|
||||
if (actualTx._class === core.class.TxCreateDoc) {
|
||||
const createTx = actualTx as TxCreateDoc<Vacancy>
|
||||
if (control.hierarchy.isDerived(createTx.objectClass, recruit.class.Vacancy)) {
|
||||
const vacancy = TxProcessor.createDoc2Doc(createTx)
|
||||
const res: Tx[] = []
|
||||
if (vacancy.company !== undefined) {
|
||||
return [
|
||||
control.txFactory.createTxMixin(
|
||||
vacancy.company,
|
||||
contact.class.Organization,
|
||||
contact.space.Contacts,
|
||||
recruit.mixin.VacancyList,
|
||||
{
|
||||
$inc: { vacancies: 1 }
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
return res
|
||||
}
|
||||
handleVacancyCreate(control, cud, actualTx, res)
|
||||
await handleApplicantCreate(control, cud, res, tx)
|
||||
}
|
||||
|
||||
if (actualTx._class === core.class.TxUpdateDoc) {
|
||||
const updateTx = actualTx as TxUpdateDoc<Vacancy>
|
||||
if (control.hierarchy.isDerived(updateTx.objectClass, recruit.class.Vacancy)) {
|
||||
if (updateTx.operations.company !== undefined) {
|
||||
// It could be null or new value
|
||||
const txes = await control.findAll(core.class.TxCUD, {
|
||||
objectId: updateTx.objectId,
|
||||
_id: { $nin: [updateTx._id] }
|
||||
})
|
||||
const vacancy = TxProcessor.buildDoc2Doc(txes) as Vacancy
|
||||
const res: Tx[] = []
|
||||
if (vacancy.company != null) {
|
||||
// We have old value
|
||||
res.push(
|
||||
control.txFactory.createTxMixin(
|
||||
vacancy.company,
|
||||
contact.class.Organization,
|
||||
contact.space.Contacts,
|
||||
recruit.mixin.VacancyList,
|
||||
{
|
||||
$inc: { vacancies: -1 }
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
if (updateTx.operations.company !== null) {
|
||||
res.push(
|
||||
control.txFactory.createTxMixin(
|
||||
updateTx.operations.company,
|
||||
contact.class.Organization,
|
||||
contact.space.Contacts,
|
||||
recruit.mixin.VacancyList,
|
||||
{
|
||||
$inc: { vacancies: 1 }
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
await handleVacancyUpdate(control, cud, res)
|
||||
await handleApplicantUpdate(control, cud, res, tx)
|
||||
}
|
||||
if (actualTx._class === core.class.TxRemoveDoc) {
|
||||
const removeTx = actualTx as TxRemoveDoc<Vacancy>
|
||||
if (control.hierarchy.isDerived(removeTx.objectClass, recruit.class.Vacancy)) {
|
||||
await handleVacancyRemove(control, cud, actualTx)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export default async () => ({
|
||||
function: {
|
||||
VacancyHTMLPresenter: vacancyHTMLPresenter,
|
||||
VacancyTextPresenter: vacancyTextPresenter,
|
||||
ApplicationHTMLPresenter: applicationHTMLPresenter,
|
||||
ApplicationTextPresenter: applicationTextPresenter
|
||||
},
|
||||
trigger: {
|
||||
OnRecruitUpdate
|
||||
}
|
||||
})
|
||||
async function handleVacancyUpdate (control: TriggerControl, cud: TxCUD<Doc>, res: Tx[]): Promise<void> {
|
||||
if (control.hierarchy.isDerived(cud.objectClass, recruit.class.Vacancy)) {
|
||||
const updateTx = cud as TxUpdateDoc<Vacancy>
|
||||
if (updateTx.operations.company !== undefined) {
|
||||
// It could be null or new value
|
||||
const txes = await control.findAll(core.class.TxCUD, {
|
||||
objectId: removeTx.objectId,
|
||||
_id: { $nin: [removeTx._id] }
|
||||
objectId: updateTx.objectId,
|
||||
_id: { $nin: [updateTx._id] }
|
||||
})
|
||||
const vacancy = TxProcessor.buildDoc2Doc(txes) as Vacancy
|
||||
const res: Tx[] = []
|
||||
if (vacancy.company != null) {
|
||||
// We have old value
|
||||
res.push(
|
||||
@ -150,21 +128,105 @@ export async function OnVacancyUpdate (tx: Tx, control: TriggerControl): Promise
|
||||
)
|
||||
)
|
||||
}
|
||||
return []
|
||||
if (updateTx.operations.company !== null) {
|
||||
res.push(
|
||||
control.txFactory.createTxMixin(
|
||||
updateTx.operations.company,
|
||||
contact.class.Organization,
|
||||
contact.space.Contacts,
|
||||
recruit.mixin.VacancyList,
|
||||
{
|
||||
$inc: { vacancies: 1 }
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export default async () => ({
|
||||
function: {
|
||||
VacancyHTMLPresenter: vacancyHTMLPresenter,
|
||||
VacancyTextPresenter: vacancyTextPresenter,
|
||||
ApplicationHTMLPresenter: applicationHTMLPresenter,
|
||||
ApplicationTextPresenter: applicationTextPresenter
|
||||
},
|
||||
trigger: {
|
||||
OnVacancyUpdate
|
||||
async function handleVacancyRemove (control: TriggerControl, cud: TxCUD<Doc>, actualTx: Tx): Promise<void> {
|
||||
if (control.hierarchy.isDerived(cud.objectClass, recruit.class.Vacancy)) {
|
||||
const removeTx = actualTx as TxRemoveDoc<Vacancy>
|
||||
// It could be null or new value
|
||||
const txes = await control.findAll(core.class.TxCUD, {
|
||||
objectId: removeTx.objectId,
|
||||
_id: { $nin: [removeTx._id] }
|
||||
})
|
||||
const vacancy = TxProcessor.buildDoc2Doc(txes) as Vacancy
|
||||
const res: Tx[] = []
|
||||
if (vacancy.company != null) {
|
||||
// We have old value
|
||||
res.push(
|
||||
control.txFactory.createTxMixin(
|
||||
vacancy.company,
|
||||
contact.class.Organization,
|
||||
contact.space.Contacts,
|
||||
recruit.mixin.VacancyList,
|
||||
{
|
||||
$inc: { vacancies: -1 }
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleApplicantUpdate (control: TriggerControl, cud: TxCUD<Doc>, res: Tx[], tx: Tx): Promise<void> {
|
||||
if (control.hierarchy.isDerived(cud.objectClass, recruit.class.Applicant)) {
|
||||
const updateTx = cud as TxUpdateDoc<Applicant>
|
||||
if (updateTx.operations.assignee != null) {
|
||||
const applicant = (
|
||||
await control.findAll(recruit.class.Applicant, { _id: updateTx.objectId }, { limit: 1 })
|
||||
).shift()
|
||||
|
||||
if (applicant?.assignee != null) {
|
||||
await addAssigneeNotification(
|
||||
control,
|
||||
res,
|
||||
applicant,
|
||||
applicationTextPresenter(applicant),
|
||||
applicant.assignee,
|
||||
tx as TxCollectionCUD<Applicant, AttachedDoc>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApplicantCreate (control: TriggerControl, cud: TxCUD<Doc>, res: Tx[], tx: Tx): Promise<void> {
|
||||
if (control.hierarchy.isDerived(cud.objectClass, recruit.class.Applicant)) {
|
||||
const createTx = cud as TxCreateDoc<Applicant>
|
||||
const applicant = TxProcessor.createDoc2Doc(createTx)
|
||||
if (applicant.assignee != null) {
|
||||
await addAssigneeNotification(
|
||||
control,
|
||||
res,
|
||||
applicant,
|
||||
applicationTextPresenter(applicant),
|
||||
applicant.assignee,
|
||||
tx as TxCollectionCUD<Applicant, AttachedDoc>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleVacancyCreate (control: TriggerControl, cud: TxCUD<Doc>, actualTx: Tx, res: Tx[]): void {
|
||||
if (control.hierarchy.isDerived(cud.objectClass, recruit.class.Vacancy)) {
|
||||
const createTx = actualTx as TxCreateDoc<Vacancy>
|
||||
const vacancy = TxProcessor.createDoc2Doc(createTx)
|
||||
if (vacancy.company !== undefined) {
|
||||
res.push(
|
||||
control.txFactory.createTxMixin(
|
||||
vacancy.company,
|
||||
contact.class.Organization,
|
||||
contact.space.Contacts,
|
||||
recruit.mixin.VacancyList,
|
||||
{
|
||||
$inc: { vacancies: 1 }
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,6 @@ export default plugin(serverRecruitId, {
|
||||
VacancyTextPresenter: '' as Resource<(doc: Doc) => string>
|
||||
},
|
||||
trigger: {
|
||||
OnVacancyUpdate: '' as Resource<TriggerFunc>
|
||||
OnRecruitUpdate: '' as Resource<TriggerFunc>
|
||||
}
|
||||
})
|
||||
|
@ -33,6 +33,8 @@
|
||||
"@anticrm/task": "~0.6.0",
|
||||
"@anticrm/view": "~0.6.0",
|
||||
"@anticrm/login": "~0.6.1",
|
||||
"@anticrm/workbench": "~0.6.1"
|
||||
"@anticrm/workbench": "~0.6.1",
|
||||
"@anticrm/notification": "~0.6.0",
|
||||
"@anticrm/contact": "~0.6.5"
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,20 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, { Doc, Tx, TxProcessor, TxUpdateDoc } from '@anticrm/core'
|
||||
import contact, { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
|
||||
import core, {
|
||||
Account,
|
||||
AttachedDoc,
|
||||
Data,
|
||||
Doc,
|
||||
generateId,
|
||||
Ref,
|
||||
Tx,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxProcessor,
|
||||
TxUpdateDoc
|
||||
} from '@anticrm/core'
|
||||
import login from '@anticrm/login'
|
||||
import { getMetadata } from '@anticrm/platform'
|
||||
import { TriggerControl } from '@anticrm/server-core'
|
||||
@ -21,6 +34,7 @@ import { getUpdateLastViewTx } from '@anticrm/server-notification'
|
||||
import task, { Issue, Task, taskId } from '@anticrm/task'
|
||||
import view from '@anticrm/view'
|
||||
import { workbenchId } from '@anticrm/workbench'
|
||||
import notification, { Notification, NotificationStatus } from '@anticrm/notification'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -39,6 +53,79 @@ export function issueTextPresenter (doc: Doc): string {
|
||||
return `Task-${issue.number}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function getEmployeeAccount (
|
||||
employee: Ref<Account>,
|
||||
control: TriggerControl
|
||||
): Promise<EmployeeAccount | undefined> {
|
||||
const account = (
|
||||
await control.modelDb.findAll(
|
||||
contact.class.EmployeeAccount,
|
||||
{
|
||||
_id: employee as Ref<EmployeeAccount>
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
)[0]
|
||||
return account
|
||||
}
|
||||
|
||||
async function getEmployee (employee: Ref<Employee>, control: TriggerControl): Promise<Employee | undefined> {
|
||||
const account = (
|
||||
await control.findAll(
|
||||
contact.class.Employee,
|
||||
{
|
||||
_id: employee
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
)[0]
|
||||
return account
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function addAssigneeNotification (
|
||||
control: TriggerControl,
|
||||
res: Tx[],
|
||||
issue: Doc,
|
||||
issueName: string,
|
||||
assignee: Ref<Employee>,
|
||||
ptx: TxCollectionCUD<AttachedDoc, AttachedDoc>
|
||||
): Promise<void> {
|
||||
const sender = await getEmployeeAccount(ptx.modifiedBy, control)
|
||||
if (sender === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const target = await getEmployee(assignee, control)
|
||||
if (target === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const createTx: TxCreateDoc<Notification> = {
|
||||
objectClass: notification.class.Notification,
|
||||
objectSpace: notification.space.Notifications,
|
||||
objectId: generateId(),
|
||||
modifiedOn: ptx.modifiedOn,
|
||||
modifiedBy: ptx.modifiedBy,
|
||||
space: ptx.space,
|
||||
_id: generateId(),
|
||||
_class: core.class.TxCreateDoc,
|
||||
attributes: {
|
||||
tx: ptx._id,
|
||||
status: NotificationStatus.New,
|
||||
type: task.ids.AssigneedNotification,
|
||||
text: `${issueName} was assigned to you by ${formatName(sender.name)}`
|
||||
} as unknown as Data<Notification>
|
||||
}
|
||||
|
||||
res.push(control.txFactory.createTxCollectionCUD(target._class, target._id, target.space, 'notifications', createTx))
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -29,6 +29,10 @@
|
||||
"@anticrm/core": "~0.6.16",
|
||||
"@anticrm/platform": "~0.6.6",
|
||||
"@anticrm/server-core": "~0.6.1",
|
||||
"@anticrm/tracker": "~0.6.0"
|
||||
"@anticrm/tracker": "~0.6.0",
|
||||
"@anticrm/contact": "~0.6.5",
|
||||
"@anticrm/notification": "~0.6.0",
|
||||
"@anticrm/task": "~0.6.0",
|
||||
"@anticrm/server-task-resources": "~0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Employee } from '@anticrm/contact'
|
||||
import core, {
|
||||
AttachedDoc,
|
||||
DocumentUpdate,
|
||||
Ref,
|
||||
Space,
|
||||
@ -27,6 +29,7 @@ import core, {
|
||||
WithLookup
|
||||
} from '@anticrm/core'
|
||||
import { TriggerControl } from '@anticrm/server-core'
|
||||
import { addAssigneeNotification } from '@anticrm/server-task-resources'
|
||||
import tracker, { Issue, IssueParentInfo, TimeSpendReport } from '@anticrm/tracker'
|
||||
|
||||
async function updateSubIssues (
|
||||
@ -42,6 +45,22 @@ async function updateSubIssues (
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function addTrackerAssigneeNotification (
|
||||
control: TriggerControl,
|
||||
res: Tx[],
|
||||
issue: Issue,
|
||||
assignee: Ref<Employee>,
|
||||
ptx: TxCollectionCUD<Issue, AttachedDoc>
|
||||
): Promise<void> {
|
||||
const team = (await control.findAll(tracker.class.Team, { _id: issue.space })).shift()
|
||||
const issueName = `${team?.identifier ?? '?'}-${issue.number}`
|
||||
|
||||
await addAssigneeNotification(control, res, issue, issueName, assignee, ptx)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -66,6 +85,16 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
|
||||
const issue = TxProcessor.createDoc2Doc(createTx)
|
||||
const res: Tx[] = []
|
||||
await updateIssueParentEstimations(issue, res, control, [], issue.parents)
|
||||
|
||||
if (issue.assignee != null) {
|
||||
await addTrackerAssigneeNotification(
|
||||
control,
|
||||
res,
|
||||
issue,
|
||||
issue.assignee,
|
||||
tx as TxCollectionCUD<Issue, AttachedDoc>
|
||||
)
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
@ -73,7 +102,7 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
|
||||
if (actualTx._class === core.class.TxUpdateDoc) {
|
||||
const updateTx = actualTx as TxUpdateDoc<Issue>
|
||||
if (control.hierarchy.isDerived(updateTx.objectClass, tracker.class.Issue)) {
|
||||
return await doIssueUpdate(updateTx, control)
|
||||
return await doIssueUpdate(updateTx, control, tx as TxCollectionCUD<Issue, AttachedDoc>)
|
||||
}
|
||||
}
|
||||
if (actualTx._class === core.class.TxRemoveDoc) {
|
||||
@ -176,7 +205,11 @@ async function doTimeReportUpdate (cud: TxCUD<TimeSpendReport>, tx: Tx, control:
|
||||
return []
|
||||
}
|
||||
|
||||
async function doIssueUpdate (updateTx: TxUpdateDoc<Issue>, control: TriggerControl): Promise<Tx[]> {
|
||||
async function doIssueUpdate (
|
||||
updateTx: TxUpdateDoc<Issue>,
|
||||
control: TriggerControl,
|
||||
tx: TxCollectionCUD<Issue, AttachedDoc>
|
||||
): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
|
||||
let currentIssue: WithLookup<Issue> | undefined
|
||||
@ -190,6 +223,10 @@ async function doIssueUpdate (updateTx: TxUpdateDoc<Issue>, control: TriggerCont
|
||||
return currentIssue
|
||||
}
|
||||
|
||||
if (updateTx.operations.assignee != null) {
|
||||
await addTrackerAssigneeNotification(control, res, await getCurrentIssue(), updateTx.operations.assignee, tx)
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(updateTx.operations, 'attachedTo')) {
|
||||
const [newParent] = await control.findAll(
|
||||
tracker.class.Issue,
|
||||
|
Loading…
Reference in New Issue
Block a user