Tracker assign notification (#2269)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-09-16 09:57:32 +07:00 committed by GitHub
parent 0f3ce39cae
commit 13b6459c95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 599 additions and 171 deletions

View File

@ -146,7 +146,7 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
label: notification.string.BrowserNotification,
default: false
default: true
},
notification.ids.BrowserNotification
)

View File

@ -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
})
}

View File

@ -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
})
}

View File

@ -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
)
}

View File

@ -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

View File

@ -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);

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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
>&nbsp
}}
>{event.title}
</span>
&nbsp
<DateTimePresenter value={event.date} />
{/if}
{/await}

View File

@ -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"
}
}

View File

@ -9,6 +9,10 @@
"PlatformNotification": "в системе",
"Track": "Отслеживать",
"DontTrack": "Не отслеживать",
"BrowserNotification": "в браузере"
"BrowserNotification": "в браузере",
"Remove": "Удалить нотификацию",
"RemoveAll": "Удалить все нотификации",
"MarkAllAsRead": "Отметить все нотификации как прочитанные",
"MarkAsRead": "Отметить нотификация прочитанной"
}
}

View File

@ -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"
}
}

View File

@ -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
)
}

View File

@ -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>

View File

@ -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>

View File

@ -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
}
})

View File

@ -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"
}
}

View File

@ -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>
}
})

View File

@ -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: {

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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"
}
}

View File

@ -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
}
})

View File

@ -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"
}
}

View File

@ -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>
}
})

View File

@ -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"
}
}

View File

@ -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 }
}
)
)
}
}
}

View File

@ -34,6 +34,6 @@ export default plugin(serverRecruitId, {
VacancyTextPresenter: '' as Resource<(doc: Doc) => string>
},
trigger: {
OnVacancyUpdate: '' as Resource<TriggerFunc>
OnRecruitUpdate: '' as Resource<TriggerFunc>
}
})

View File

@ -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"
}
}

View File

@ -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
*/

View File

@ -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"
}
}

View File

@ -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,