mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-08 17:00:47 +00:00
TSK-959 Inbox (part1) (#2885)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
b2fe37e540
commit
0e6243dcea
@ -204,10 +204,34 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
fields: ['members']
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Channel, core.class.Class, notification.mixin.ClassCollaborators, {
|
||||
fields: ['members']
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ObjectPanel, {
|
||||
component: chunter.component.ChannelViewPanel
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectPanel, {
|
||||
component: chunter.component.ChannelViewPanel
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: chunter.component.DmPresenter
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Message, core.class.Class, notification.mixin.NotificationObjectPresenter, {
|
||||
presenter: chunter.component.ThreadParentPresenter
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: chunter.component.MessagePresenter
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Message, core.class.Class, view.mixin.ObjectPanel, {
|
||||
component: chunter.component.ThreadViewPanel
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Message, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: chunter.component.MessagePresenter
|
||||
})
|
||||
|
@ -26,6 +26,7 @@ export default mergeIds(chunterId, chunter, {
|
||||
component: {
|
||||
CommentPresenter: '' as AnyComponent,
|
||||
ChannelPresenter: '' as AnyComponent,
|
||||
DirectMessagePresenter: '' as AnyComponent,
|
||||
MessagePresenter: '' as AnyComponent,
|
||||
DmPresenter: '' as AnyComponent,
|
||||
Threads: '' as AnyComponent,
|
||||
|
@ -108,6 +108,7 @@ export class TContact extends TDoc implements Contact {
|
||||
@UX(contact.string.Channel, contact.icon.Person)
|
||||
export class TChannel extends TAttachedDoc implements Channel {
|
||||
@Prop(TypeRef(contact.class.ChannelProvider), contact.string.ChannelProvider)
|
||||
@Index(IndexKind.Indexed)
|
||||
provider!: Ref<ChannelProvider>
|
||||
|
||||
@Prop(TypeString(), contact.string.Value)
|
||||
|
@ -30,6 +30,7 @@
|
||||
"@hcengineering/ui": "^0.6.3",
|
||||
"@hcengineering/platform": "^0.6.8",
|
||||
"@hcengineering/model-core": "^0.6.0",
|
||||
"@hcengineering/workbench": "^0.6.2",
|
||||
"@hcengineering/notification": "^0.6.8",
|
||||
"@hcengineering/setting": "^0.6.3"
|
||||
}
|
||||
|
@ -14,14 +14,17 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Account, Doc, Domain, DOMAIN_MODEL, IndexKind, Ref, TxCUD } from '@hcengineering/core'
|
||||
import { Account, Class, Doc, Domain, DOMAIN_MODEL, IndexKind, Ref, Timestamp, TxCUD } from '@hcengineering/core'
|
||||
import { ArrOf, Builder, Index, Mixin, Model, Prop, TypeRef, TypeString, UX } from '@hcengineering/model'
|
||||
import core, { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
|
||||
import type {
|
||||
import {
|
||||
AnotherUserNotifications,
|
||||
DocUpdates,
|
||||
EmailNotification,
|
||||
LastView,
|
||||
Notification,
|
||||
notificationId,
|
||||
NotificationObjectPresenter,
|
||||
NotificationProvider,
|
||||
NotificationSetting,
|
||||
NotificationStatus,
|
||||
@ -30,7 +33,9 @@ import type {
|
||||
} from '@hcengineering/notification'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import setting from '@hcengineering/setting'
|
||||
import workbench from '@hcengineering/workbench'
|
||||
import notification from './plugin'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
|
||||
export const DOMAIN_NOTIFICATION = 'notification' as Domain
|
||||
|
||||
@ -121,9 +126,29 @@ export class TTrackedDoc extends TClass {}
|
||||
@UX(notification.string.Collaborators)
|
||||
export class TCollaborators extends TDoc {
|
||||
@Prop(ArrOf(TypeRef(core.class.Account)), notification.string.Collaborators)
|
||||
@Index(IndexKind.Indexed)
|
||||
collaborators!: Ref<Account>[]
|
||||
}
|
||||
|
||||
@Mixin(notification.mixin.NotificationObjectPresenter, core.class.Class)
|
||||
export class TNotificationObjectPresenter extends TClass implements NotificationObjectPresenter {
|
||||
presenter!: AnyComponent
|
||||
}
|
||||
|
||||
@Model(notification.class.DocUpdates, core.class.Doc, DOMAIN_NOTIFICATION)
|
||||
export class TDocUpdates extends TDoc implements DocUpdates {
|
||||
@Index(IndexKind.Indexed)
|
||||
user!: Ref<Account>
|
||||
|
||||
@Index(IndexKind.Indexed)
|
||||
attachedTo!: Ref<Doc>
|
||||
|
||||
attachedToClass!: Ref<Class<Doc>>
|
||||
lastTx?: Ref<TxCUD<Doc>>
|
||||
lastTxTime?: Timestamp
|
||||
txes!: [Ref<TxCUD<Doc>>, Timestamp][]
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(
|
||||
TLastView,
|
||||
@ -136,7 +161,9 @@ export function createModel (builder: Builder): void {
|
||||
TAnotherUserNotifications,
|
||||
TClassCollaborators,
|
||||
TTrackedDoc,
|
||||
TCollaborators
|
||||
TCollaborators,
|
||||
TDocUpdates,
|
||||
TNotificationObjectPresenter
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
@ -165,19 +192,6 @@ export function createModel (builder: Builder): void {
|
||||
notification.ids.DMNotification
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
notification.class.NotificationType,
|
||||
core.space.Model,
|
||||
{
|
||||
label: notification.string.Notification,
|
||||
hidden: true,
|
||||
textTemplate: '',
|
||||
htmlTemplate: '',
|
||||
subjectTemplate: ''
|
||||
},
|
||||
notification.ids.CollaboratorNotification
|
||||
)
|
||||
|
||||
// Temporarily disabled, we should think about it
|
||||
// builder.createDoc(
|
||||
// notification.class.NotificationProvider,
|
||||
@ -213,6 +227,20 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
notification.ids.NotificationSettings
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
workbench.class.Application,
|
||||
core.space.Model,
|
||||
{
|
||||
label: notification.string.Inbox,
|
||||
icon: notification.icon.Notifications,
|
||||
alias: notificationId,
|
||||
position: 'bottom',
|
||||
hidden: false,
|
||||
component: notification.component.Inbox
|
||||
},
|
||||
notification.app.Notification
|
||||
)
|
||||
}
|
||||
|
||||
export { notificationOperation } from './migration'
|
||||
|
@ -14,6 +14,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Application } from '@hcengineering/workbench'
|
||||
import notification, { notificationId } from '@hcengineering/notification'
|
||||
import { IntlString, mergeIds } from '@hcengineering/platform'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
@ -29,6 +31,9 @@ export default mergeIds(notificationId, notification, {
|
||||
EmailNotification: '' as IntlString,
|
||||
Collaborators: '' as IntlString
|
||||
},
|
||||
app: {
|
||||
Notification: '' as Ref<Application>
|
||||
},
|
||||
component: {
|
||||
NotificationSettings: '' as AnyComponent
|
||||
}
|
||||
|
@ -44,11 +44,11 @@ export function createModel (builder: Builder): void {
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.CreateCollaboratorDoc
|
||||
trigger: serverNotification.trigger.CollaboratorDocHandler
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.UpdateCollaboratorDoc
|
||||
trigger: serverNotification.trigger.OnUpdateLastView
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
|
@ -13,31 +13,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, { SortingOrder, TxOperations } from '@hcengineering/core'
|
||||
import telegram from './plugin'
|
||||
import core, { TxOperations } from '@hcengineering/core'
|
||||
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
|
||||
import contact from '@hcengineering/model-contact'
|
||||
|
||||
async function updateChannlLastMessage (client: TxOperations): Promise<void> {
|
||||
const channels = await client.findAll(contact.class.Channel, {
|
||||
provider: contact.channelProvider.Telegram
|
||||
})
|
||||
const targets = channels.filter((p) => p.lastMessage === undefined)
|
||||
for (const channel of targets) {
|
||||
const lastMessage = await client.findOne(
|
||||
telegram.class.Message,
|
||||
{
|
||||
attachedTo: channel._id
|
||||
},
|
||||
{ sort: { sendOn: SortingOrder.Descending } }
|
||||
)
|
||||
if (lastMessage !== undefined) {
|
||||
await client.updateDoc(channel._class, channel.space, channel._id, {
|
||||
lastMessage: lastMessage.sendOn
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
import telegram from './plugin'
|
||||
|
||||
export const telegramOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {},
|
||||
@ -60,7 +38,5 @@ export const telegramOperation: MigrateOperation = {
|
||||
telegram.space.Telegram
|
||||
)
|
||||
}
|
||||
|
||||
await updateChannlLastMessage(tx)
|
||||
}
|
||||
}
|
||||
|
@ -827,6 +827,10 @@ export function createModel (builder: Builder): void {
|
||||
presenter: tracker.component.IssuePresenter
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.Issue, core.class.Class, notification.mixin.NotificationObjectPresenter, {
|
||||
presenter: tracker.component.NotificationIssuePresenter
|
||||
})
|
||||
|
||||
builder.mixin(tracker.class.IssueTemplate, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: tracker.component.IssueTemplatePresenter
|
||||
})
|
||||
|
@ -42,7 +42,8 @@ export default mergeIds(trackerId, tracker, {
|
||||
Nope: '' as AnyComponent,
|
||||
SprintSelector: '' as AnyComponent,
|
||||
IssueStatistics: '' as AnyComponent,
|
||||
TimeSpendReportPopup: '' as AnyComponent
|
||||
TimeSpendReportPopup: '' as AnyComponent,
|
||||
NotificationIssuePresenter: '' as AnyComponent
|
||||
},
|
||||
app: {
|
||||
Tracker: '' as Ref<Application>
|
||||
|
@ -32,6 +32,7 @@ export class TApplication extends TDoc implements Application {
|
||||
label!: IntlString
|
||||
icon!: Asset
|
||||
alias!: string
|
||||
position?: 'top' | 'bottom'
|
||||
hidden!: boolean
|
||||
}
|
||||
|
||||
|
@ -464,7 +464,10 @@ export abstract class TxProcessor implements WithTx {
|
||||
* @public
|
||||
*/
|
||||
export class TxFactory {
|
||||
constructor (readonly account: Ref<Account>) {}
|
||||
private readonly txSpace: Ref<Space>
|
||||
constructor (readonly account: Ref<Account>, readonly isDerived: boolean = false) {
|
||||
this.txSpace = isDerived ? core.space.DerivedTx : core.space.Tx
|
||||
}
|
||||
|
||||
createTxCreateDoc<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
@ -477,7 +480,7 @@ export class TxFactory {
|
||||
return {
|
||||
_id: generateId(),
|
||||
_class: core.class.TxCreateDoc,
|
||||
space: core.space.Tx,
|
||||
space: this.txSpace,
|
||||
objectId: objectId ?? generateId(),
|
||||
objectClass: _class,
|
||||
objectSpace: space,
|
||||
@ -500,7 +503,7 @@ export class TxFactory {
|
||||
return {
|
||||
_id: generateId(),
|
||||
_class: core.class.TxCollectionCUD,
|
||||
space: core.space.Tx,
|
||||
space: this.txSpace,
|
||||
objectId,
|
||||
objectClass: _class,
|
||||
objectSpace: space,
|
||||
@ -523,7 +526,7 @@ export class TxFactory {
|
||||
return {
|
||||
_id: generateId(),
|
||||
_class: core.class.TxUpdateDoc,
|
||||
space: core.space.Tx,
|
||||
space: this.txSpace,
|
||||
modifiedBy: modifiedBy ?? this.account,
|
||||
modifiedOn: modifiedOn ?? Date.now(),
|
||||
objectId,
|
||||
@ -544,7 +547,7 @@ export class TxFactory {
|
||||
return {
|
||||
_id: generateId(),
|
||||
_class: core.class.TxRemoveDoc,
|
||||
space: core.space.Tx,
|
||||
space: this.txSpace,
|
||||
modifiedBy: modifiedBy ?? this.account,
|
||||
modifiedOn: modifiedOn ?? Date.now(),
|
||||
objectId,
|
||||
@ -565,7 +568,7 @@ export class TxFactory {
|
||||
return {
|
||||
_id: generateId(),
|
||||
_class: core.class.TxMixin,
|
||||
space: core.space.Tx,
|
||||
space: this.txSpace,
|
||||
modifiedBy: modifiedBy ?? this.account,
|
||||
modifiedOn: modifiedOn ?? Date.now(),
|
||||
objectId,
|
||||
@ -587,7 +590,7 @@ export class TxFactory {
|
||||
return {
|
||||
_id: generateId(),
|
||||
_class: core.class.TxApplyIf,
|
||||
space: core.space.Tx,
|
||||
space: this.txSpace,
|
||||
modifiedBy: modifiedBy ?? this.account,
|
||||
modifiedOn: modifiedOn ?? Date.now(),
|
||||
objectSpace: space,
|
||||
|
@ -45,6 +45,7 @@
|
||||
export let allowClose = true
|
||||
export let useMaxWidth: boolean | undefined = undefined
|
||||
export let isFullSize = false
|
||||
export let embedded = false
|
||||
</script>
|
||||
|
||||
<Panel
|
||||
@ -56,6 +57,7 @@
|
||||
on:close
|
||||
{allowClose}
|
||||
{floatAside}
|
||||
{embedded}
|
||||
bind:useMaxWidth
|
||||
{isFullSize}
|
||||
>
|
||||
|
@ -341,7 +341,6 @@ export async function getAttributeEditor (
|
||||
_class: Ref<Class<Obj>>,
|
||||
key: KeyedAttribute | string
|
||||
): Promise<AnySvelteComponent | undefined> {
|
||||
console.log('get attribute editor', _class, key)
|
||||
const hierarchy = client.getHierarchy()
|
||||
const attribute = typeof key === 'string' ? hierarchy.getAttribute(_class, key) : key.attr
|
||||
const presenterClass = attribute !== undefined ? getAttributePresenterClass(hierarchy, attribute) : undefined
|
||||
|
@ -61,8 +61,15 @@
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
border-radius: .5rem;
|
||||
box-shadow: var(--popup-panel-shadow);
|
||||
|
||||
&.embedded {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:not(.embedded) {
|
||||
border-radius: .5rem;
|
||||
box-shadow: var(--popup-panel-shadow);
|
||||
}
|
||||
|
||||
.popupPanel-title {
|
||||
display: flex;
|
||||
@ -109,7 +116,9 @@
|
||||
height: 100%;
|
||||
background-color: var(--body-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 0 0 .5rem .5rem;
|
||||
&:not(.embedded) {
|
||||
border-radius: 0 0 .5rem .5rem;
|
||||
}
|
||||
|
||||
&.main {
|
||||
justify-content: stretch;
|
||||
|
@ -34,6 +34,7 @@
|
||||
export let floatAside = false
|
||||
export let allowClose = true
|
||||
export let useMaxWidth: boolean | undefined = undefined
|
||||
export let embedded = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -79,74 +80,77 @@
|
||||
|
||||
<div
|
||||
class="popupPanel"
|
||||
class:embedded
|
||||
use:resizeObserver={(element) => {
|
||||
panelWidth = element.clientWidth
|
||||
checkPanel()
|
||||
}}
|
||||
>
|
||||
<div class="popupPanel-title__bordered {twoRows && !withoutTitle ? 'flex-col flex-no-shrink' : 'flex-row-center'}">
|
||||
<div class="popupPanel-title {twoRows && !withoutTitle ? 'row-top' : 'row'}">
|
||||
{#if allowClose}
|
||||
<Button
|
||||
icon={IconClose}
|
||||
kind={'transparent'}
|
||||
size={'medium'}
|
||||
on:click={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
/>
|
||||
{#if !embedded}
|
||||
<div class="popupPanel-title__bordered {twoRows && !withoutTitle ? 'flex-col flex-no-shrink' : 'flex-row-center'}">
|
||||
<div class="popupPanel-title {twoRows && !withoutTitle ? 'row-top' : 'row'}">
|
||||
{#if allowClose}
|
||||
<Button
|
||||
icon={IconClose}
|
||||
kind={'transparent'}
|
||||
size={'medium'}
|
||||
on:click={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if $$slots.navigator}<slot name="navigator" />{/if}
|
||||
<div class="popupPanel-title__content">
|
||||
{#if !twoRows && !withoutTitle}<slot name="title" />{/if}
|
||||
</div>
|
||||
<div class="buttons-group xsmall-gap">
|
||||
<slot name="utils" />
|
||||
{#if isFullSize || useMaxWidth !== undefined || ($$slots.aside && isAside)}
|
||||
<div class="buttons-divider" />
|
||||
{/if}
|
||||
{#if $$slots.aside && isAside}
|
||||
<Button
|
||||
icon={IconDetails}
|
||||
kind={'transparent'}
|
||||
size={'medium'}
|
||||
selected={asideShown}
|
||||
on:click={() => {
|
||||
asideShown = !asideShown
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if useMaxWidth !== undefined}
|
||||
<Button
|
||||
icon={useMaxWidth ? IconMaxWidth : IconMinWidth}
|
||||
kind={'transparent'}
|
||||
size={'medium'}
|
||||
selected={useMaxWidth}
|
||||
on:click={() => {
|
||||
useMaxWidth = !useMaxWidth
|
||||
dispatch('maxWidth', useMaxWidth)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if isFullSize}
|
||||
<Button
|
||||
icon={fullSize ? IconScale : IconScaleFull}
|
||||
kind={'transparent'}
|
||||
size={'medium'}
|
||||
selected={fullSize}
|
||||
on:click={() => {
|
||||
fullSize = !fullSize
|
||||
dispatch('fullsize')
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if twoRows && !withoutTitle}
|
||||
<div class="popupPanel-title row-bottom"><slot name="title" /></div>
|
||||
{/if}
|
||||
{#if $$slots.navigator}<slot name="navigator" />{/if}
|
||||
<div class="popupPanel-title__content">
|
||||
{#if !twoRows && !withoutTitle}<slot name="title" />{/if}
|
||||
</div>
|
||||
<div class="buttons-group xsmall-gap">
|
||||
<slot name="utils" />
|
||||
{#if isFullSize || useMaxWidth !== undefined || ($$slots.aside && isAside)}
|
||||
<div class="buttons-divider" />
|
||||
{/if}
|
||||
{#if $$slots.aside && isAside}
|
||||
<Button
|
||||
icon={IconDetails}
|
||||
kind={'transparent'}
|
||||
size={'medium'}
|
||||
selected={asideShown}
|
||||
on:click={() => {
|
||||
asideShown = !asideShown
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if useMaxWidth !== undefined}
|
||||
<Button
|
||||
icon={useMaxWidth ? IconMaxWidth : IconMinWidth}
|
||||
kind={'transparent'}
|
||||
size={'medium'}
|
||||
selected={useMaxWidth}
|
||||
on:click={() => {
|
||||
useMaxWidth = !useMaxWidth
|
||||
dispatch('maxWidth', useMaxWidth)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if isFullSize}
|
||||
<Button
|
||||
icon={fullSize ? IconScale : IconScaleFull}
|
||||
kind={'transparent'}
|
||||
size={'medium'}
|
||||
selected={fullSize}
|
||||
on:click={() => {
|
||||
fullSize = !fullSize
|
||||
dispatch('fullsize')
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if twoRows && !withoutTitle}
|
||||
<div class="popupPanel-title row-bottom"><slot name="title" /></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="popupPanel-body {$deviceInfo.isMobile ? 'mobile' : 'main'}" class:asideShown>
|
||||
{/if}
|
||||
<div class="popupPanel-body {$deviceInfo.isMobile ? 'mobile' : 'main'}" class:asideShown class:embedded>
|
||||
{#if $deviceInfo.isMobile}
|
||||
<Scroller horizontal padding={'.5rem .75rem'}>
|
||||
<div
|
||||
|
@ -22,12 +22,6 @@ export default mergeIds(activityId, activity, {
|
||||
DocCreated: '' as IntlString,
|
||||
DocDeleted: '' as IntlString,
|
||||
CollectionUpdated: '' as IntlString,
|
||||
Changed: '' as IntlString,
|
||||
To: '' as IntlString,
|
||||
Unset: '' as IntlString,
|
||||
Added: '' as IntlString,
|
||||
Removed: '' as IntlString,
|
||||
From: '' as IntlString,
|
||||
All: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -41,11 +41,7 @@ async function createPseudoViewlet (
|
||||
label: IntlString,
|
||||
display: 'inline' | 'content' | 'emphasized' = 'inline'
|
||||
): Promise<TxDisplayViewlet> {
|
||||
const doc = dtx.doc
|
||||
if (doc === undefined) {
|
||||
return
|
||||
}
|
||||
const docClass: Class<Doc> = client.getModel().getObject(doc._class)
|
||||
const docClass: Class<Doc> = client.getModel().getObject(dtx.tx.objectClass)
|
||||
|
||||
let trLabel = docClass.label !== undefined ? await translate(docClass.label, {}) : undefined
|
||||
if (dtx.collectionAttribute !== undefined) {
|
||||
@ -56,7 +52,7 @@ async function createPseudoViewlet (
|
||||
}
|
||||
|
||||
// Check if it is attached doc and collection have title override.
|
||||
const presenter = await getObjectPresenter(client, doc._class, { key: 'doc-presenter' })
|
||||
const presenter = await getObjectPresenter(client, dtx.tx.objectClass, { key: 'doc-presenter' })
|
||||
if (presenter !== undefined) {
|
||||
let collection = ''
|
||||
if (dtx.collectionAttribute?.label !== undefined) {
|
||||
|
@ -113,7 +113,13 @@ export default plugin(activityId, {
|
||||
Delete: '' as IntlString,
|
||||
Edit: '' as IntlString,
|
||||
Edited: '' as IntlString,
|
||||
Activity: '' as IntlString
|
||||
Activity: '' as IntlString,
|
||||
Changed: '' as IntlString,
|
||||
To: '' as IntlString,
|
||||
Unset: '' as IntlString,
|
||||
Added: '' as IntlString,
|
||||
From: '' as IntlString,
|
||||
Removed: '' as IntlString
|
||||
},
|
||||
class: {
|
||||
TxViewlet: '' as Ref<Class<TxViewlet>>,
|
||||
|
@ -0,0 +1,24 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, Space } from '@hcengineering/core'
|
||||
import ChannelView from './ChannelView.svelte'
|
||||
|
||||
export let _id: Ref<Space>
|
||||
</script>
|
||||
|
||||
<div class="antiPanel-component">
|
||||
<ChannelView space={_id} />
|
||||
</div>
|
@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { chunterId, DirectMessage, Message } from '@hcengineering/chunter'
|
||||
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import { NavLink } from '@hcengineering/ui'
|
||||
import chunter from '../plugin'
|
||||
import { getDmName } from '../utils'
|
||||
|
||||
export let value: Message
|
||||
const client = getClient()
|
||||
let dm: DirectMessage | undefined
|
||||
const query = createQuery()
|
||||
|
||||
$: query.query(chunter.class.DirectMessage, { _id: value.space }, (result) => {
|
||||
dm = result[0]
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if dm}
|
||||
{#await getDmName(client, dm) then name}
|
||||
<NavLink app={chunterId} space={value.space}>
|
||||
<span class="label">{name}</span>
|
||||
</NavLink>
|
||||
<div><MessageViewer message={value.content} /></div>
|
||||
{/await}
|
||||
{/if}
|
@ -1,25 +1,22 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { chunterId, DirectMessage, Message } from '@hcengineering/chunter'
|
||||
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import { NavLink } from '@hcengineering/ui'
|
||||
import chunter from '../plugin'
|
||||
import { getDmName } from '../utils'
|
||||
import { Message } from '@hcengineering/chunter'
|
||||
import { MessageViewer } from '@hcengineering/presentation'
|
||||
|
||||
export let value: Message
|
||||
const client = getClient()
|
||||
let dm: DirectMessage | undefined
|
||||
const query = createQuery()
|
||||
|
||||
$: query.query(chunter.class.DirectMessage, { _id: value.space }, (result) => {
|
||||
dm = result[0]
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if dm}
|
||||
{#await getDmName(client, dm) then name}
|
||||
<NavLink app={chunterId} space={value.space}>
|
||||
<span class="label">{name}</span>
|
||||
</NavLink>
|
||||
<div><MessageViewer message={value.content} /></div>
|
||||
{/await}
|
||||
{/if}
|
||||
<div><MessageViewer message={value.content} /></div>
|
||||
|
@ -0,0 +1,20 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { Label } from '@hcengineering/ui'
|
||||
import chunter from '../plugin'
|
||||
</script>
|
||||
|
||||
<Label label={chunter.string.Thread} />
|
@ -0,0 +1,35 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { Message } from '@hcengineering/chunter'
|
||||
import ThreadView from './ThreadView.svelte'
|
||||
import { Ref, Space } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import chunter from '../plugin'
|
||||
|
||||
export let _id: Ref<Message>
|
||||
let space: Ref<Space> | undefined = undefined
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(chunter.class.Message, { _id }, (res) => {
|
||||
space = res[0].space
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="antiPanel-component">
|
||||
{#if space}
|
||||
<ThreadView {_id} currentSpace={space} />
|
||||
{/if}
|
||||
</div>
|
@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { TxCreateDoc } from '@hcengineering/core'
|
||||
import { Message } from '@hcengineering/chunter'
|
||||
import { MessageViewer } from '@hcengineering/presentation'
|
||||
|
||||
export let tx: TxCreateDoc<Message>
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
<span>{tx.attributes.content}</span>
|
||||
</div>
|
||||
<MessageViewer message={tx.attributes.content} />
|
||||
|
@ -35,18 +35,22 @@ import TxMessageCreate from './components/activity/TxMessageCreate.svelte'
|
||||
import ChannelHeader from './components/ChannelHeader.svelte'
|
||||
import ChannelPresenter from './components/ChannelPresenter.svelte'
|
||||
import ChannelView from './components/ChannelView.svelte'
|
||||
import ChannelViewPanel from './components/ChannelViewPanel.svelte'
|
||||
import ChunterBrowser from './components/ChunterBrowser.svelte'
|
||||
import CommentInput from './components/CommentInput.svelte'
|
||||
import CommentPopup from './components/CommentPopup.svelte'
|
||||
import CommentPresenter from './components/CommentPresenter.svelte'
|
||||
import CommentsPresenter from './components/CommentsPresenter.svelte'
|
||||
import MessagePresenter from './components/MessagePresenter.svelte'
|
||||
import ThreadParentPresenter from './components/ThreadParentPresenter.svelte'
|
||||
import ThreadViewPanel from './components/ThreadViewPanel.svelte'
|
||||
import ConvertDmToPrivateChannelModal from './components/ConvertDmToPrivateChannel.svelte'
|
||||
import CreateChannel from './components/CreateChannel.svelte'
|
||||
import CreateDirectMessage from './components/CreateDirectMessage.svelte'
|
||||
import DmHeader from './components/DmHeader.svelte'
|
||||
import DmPresenter from './components/DmPresenter.svelte'
|
||||
import EditChannel from './components/EditChannel.svelte'
|
||||
import MessagePresenter from './components/MessagePresenter.svelte'
|
||||
import DirectMessagePresenter from './components/DirectMessagePresenter.svelte'
|
||||
import SavedMessages from './components/SavedMessages.svelte'
|
||||
import Threads from './components/Threads.svelte'
|
||||
import ThreadView from './components/ThreadView.svelte'
|
||||
@ -260,11 +264,15 @@ export default async (): Promise<Resources> => ({
|
||||
CommentInput,
|
||||
CreateChannel,
|
||||
CreateDirectMessage,
|
||||
ThreadParentPresenter,
|
||||
ThreadViewPanel,
|
||||
ChannelHeader,
|
||||
ChannelView,
|
||||
ChannelViewPanel,
|
||||
CommentPresenter,
|
||||
CommentsPresenter,
|
||||
ChannelPresenter,
|
||||
DirectMessagePresenter,
|
||||
MessagePresenter,
|
||||
ChunterBrowser,
|
||||
DmHeader,
|
||||
|
@ -27,6 +27,9 @@ export default mergeIds(chunterId, chunter, {
|
||||
ChannelHeader: '' as AnyComponent,
|
||||
DmHeader: '' as AnyComponent,
|
||||
ChannelView: '' as AnyComponent,
|
||||
ChannelViewPanel: '' as AnyComponent,
|
||||
ThreadViewPanel: '' as AnyComponent,
|
||||
ThreadParentPresenter: '' as AnyComponent,
|
||||
EditChannel: '' as AnyComponent
|
||||
},
|
||||
function: {
|
||||
|
@ -42,7 +42,6 @@
|
||||
"@hcengineering/core": "^0.6.21",
|
||||
"@hcengineering/view": "^0.6.2",
|
||||
"@hcengineering/attachment-resources": "^0.6.0",
|
||||
"@hcengineering/notification-resources": "^0.6.0",
|
||||
"@hcengineering/panel": "^0.6.2",
|
||||
"@hcengineering/view-resources": "^0.6.0",
|
||||
"@hcengineering/attachment": "^0.6.1",
|
||||
|
@ -17,9 +17,8 @@
|
||||
import type { Channel, ChannelProvider } from '@hcengineering/contact'
|
||||
import contact from '@hcengineering/contact'
|
||||
import type { AttachedData, Doc, Ref } from '@hcengineering/core'
|
||||
import { LastView } from '@hcengineering/notification'
|
||||
import { NotificationClientImpl } from '@hcengineering/notification-resources'
|
||||
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||
import notification, { LastView } from '@hcengineering/notification'
|
||||
import { Asset, getResource, IntlString } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import {
|
||||
Action,
|
||||
@ -36,6 +35,7 @@
|
||||
import { ViewAction } from '@hcengineering/view'
|
||||
import { invokeAction } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher, tick } from 'svelte'
|
||||
import { writable, Writable } from 'svelte/store'
|
||||
import { getChannelProviders } from '../utils'
|
||||
import ChannelEditor from './ChannelEditor.svelte'
|
||||
|
||||
@ -50,8 +50,8 @@
|
||||
export let focusIndex = -1
|
||||
export let restricted: Ref<ChannelProvider>[] = []
|
||||
|
||||
const notificationClient = NotificationClientImpl.getClient()
|
||||
const lastViews = notificationClient.getLastViews()
|
||||
let lastViews: Writable<LastView> = writable()
|
||||
getResource(notification.function.GetNotificationClient).then((res) => (lastViews = res().getLastViews()))
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
interface Item {
|
||||
|
@ -16,12 +16,12 @@
|
||||
<script lang="ts">
|
||||
import type { Channel, ChannelProvider } from '@hcengineering/contact'
|
||||
import type { AttachedData, Doc, Ref } from '@hcengineering/core'
|
||||
import { LastView } from '@hcengineering/notification'
|
||||
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||
import notification, { LastView } from '@hcengineering/notification'
|
||||
import { Asset, getResource, IntlString } from '@hcengineering/platform'
|
||||
import type { AnyComponent } from '@hcengineering/ui'
|
||||
import { NotificationClientImpl } from '@hcengineering/notification-resources'
|
||||
import { Button } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Writable, writable } from 'svelte/store'
|
||||
import { getChannelProviders } from '../utils'
|
||||
import ChannelsPopup from './ChannelsPopup.svelte'
|
||||
|
||||
@ -30,8 +30,9 @@
|
||||
export let length: 'short' | 'full' = 'full'
|
||||
export let reverse: boolean = false
|
||||
export let integrations: Set<Ref<Doc>> = new Set<Ref<Doc>>()
|
||||
const notificationClient = NotificationClientImpl.getClient()
|
||||
const lastViews = notificationClient.getLastViews()
|
||||
|
||||
let lastViews: Writable<LastView> = writable()
|
||||
getResource(notification.function.GetNotificationClient).then((res) => (lastViews = res().getLastViews()))
|
||||
|
||||
interface Item {
|
||||
label: IntlString
|
||||
|
@ -61,7 +61,6 @@
|
||||
placeholder={contact.string.PersonFirstNamePlaceholder}
|
||||
bind:value={object.name}
|
||||
on:change={nameChange}
|
||||
focus
|
||||
focusIndex={1}
|
||||
/>
|
||||
</div>
|
||||
|
@ -185,11 +185,21 @@ async function generateLocation (loc: Location, id: Ref<Contact>): Promise<Resol
|
||||
|
||||
export const employeeByIdStore = writable<IdMap<Employee>>(new Map())
|
||||
export const employeesStore = writable<Employee[]>([])
|
||||
const query = createQuery(true)
|
||||
query.query(contact.class.Employee, {}, (res) => {
|
||||
employeesStore.set(res)
|
||||
employeeByIdStore.set(toIdMap(res))
|
||||
})
|
||||
|
||||
function fillStore (): void {
|
||||
const client = getClient()
|
||||
if (client !== undefined) {
|
||||
const query = createQuery(true)
|
||||
query.query(contact.class.Employee, {}, (res) => {
|
||||
employeesStore.set(res)
|
||||
employeeByIdStore.set(toIdMap(res))
|
||||
})
|
||||
} else {
|
||||
setTimeout(() => fillStore(), 500)
|
||||
}
|
||||
}
|
||||
|
||||
fillStore()
|
||||
|
||||
export function getAvatarTypeDropdownItems (hasGravatar: boolean): DropdownIntlItem[] {
|
||||
return [
|
||||
|
@ -41,7 +41,6 @@
|
||||
"@hcengineering/core": "^0.6.21",
|
||||
"@hcengineering/view": "^0.6.2",
|
||||
"@hcengineering/attachment-resources": "^0.6.0",
|
||||
"@hcengineering/notification-resources": "^0.6.0",
|
||||
"@hcengineering/panel": "^0.6.2",
|
||||
"@hcengineering/view-resources": "^0.6.0",
|
||||
"@hcengineering/attachment": "^0.6.1",
|
||||
|
@ -57,6 +57,7 @@
|
||||
|
||||
export let _id: Ref<Document>
|
||||
export let _class: Ref<Class<Document>>
|
||||
export let embedded = false
|
||||
|
||||
let lastId: Ref<Doc> = _id
|
||||
let lastClass: Ref<Class<Doc>> = _class
|
||||
@ -378,6 +379,7 @@
|
||||
isHeader
|
||||
isAside={true}
|
||||
isSub={false}
|
||||
{embedded}
|
||||
bind:innerWidth
|
||||
floatAside={false}
|
||||
useMaxWidth={true}
|
||||
|
@ -6,7 +6,7 @@
|
||||
"NoNotifications": "No notifications",
|
||||
"MentionNotification": "Mentioned",
|
||||
"DM": "Direct message",
|
||||
"DMNotification": "Sent you as message",
|
||||
"DMNotification": "Sent you a message",
|
||||
"EmailNotification": "by email",
|
||||
"PlatformNotification": "in platform",
|
||||
"Track": "Track",
|
||||
@ -16,6 +16,7 @@
|
||||
"RemoveAll": "Delete all notifications",
|
||||
"MarkAllAsRead": "Mark all notifications as read",
|
||||
"MarkAsRead": "Mark as read",
|
||||
"Inbox": "Inbox",
|
||||
"Collaborators": "Collaborators"
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
"RemoveAll": "Удалить все нотификации",
|
||||
"MarkAllAsRead": "Отметить все нотификации как прочитанные",
|
||||
"MarkAsRead": "Отметить нотификация прочитанной",
|
||||
"Inbox": "Inbox",
|
||||
"Collaborators": "Участники"
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@
|
||||
"@hcengineering/activity-resources": "^0.6.1",
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/contact": "^0.6.11",
|
||||
"@hcengineering/contact-resources": "^0.6.0",
|
||||
"@hcengineering/core": "^0.6.21",
|
||||
"@hcengineering/view": "^0.6.2",
|
||||
"@hcengineering/view-resources": "^0.6.0"
|
||||
|
97
plugins/notification-resources/src/components/Inbox.svelte
Normal file
97
plugins/notification-resources/src/components/Inbox.svelte
Normal file
@ -0,0 +1,97 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 activity, { TxViewlet } from '@hcengineering/activity'
|
||||
import { activityKey, ActivityKey } from '@hcengineering/activity-resources'
|
||||
import { Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import notification, { DocUpdates } from '@hcengineering/notification'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { AnyComponent, Component, Label, Loading, Scroller } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import NotificationView from './NotificationView.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const query = createQuery()
|
||||
|
||||
let docs: DocUpdates[] = []
|
||||
let loading = true
|
||||
|
||||
$: query.query(
|
||||
notification.class.DocUpdates,
|
||||
{
|
||||
user: getCurrentAccount()._id
|
||||
},
|
||||
(res) => {
|
||||
docs = res
|
||||
if (loading && docs.length > 0) {
|
||||
select(docs[0].attachedTo, docs[0].attachedToClass)
|
||||
}
|
||||
loading = false
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
lastTxTime: -1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function select (objectId: Ref<Doc>, objectClass: Ref<Class<Doc>>) {
|
||||
const targetClass = hierarchy.getClass(objectClass)
|
||||
const panelComponent = hierarchy.as(targetClass, view.mixin.ObjectPanel)
|
||||
component = panelComponent.component ?? view.component.EditDoc
|
||||
_id = objectId
|
||||
_class = objectClass
|
||||
}
|
||||
|
||||
function selectHandler (e: CustomEvent) {
|
||||
select(e.detail._id, e.detail._class)
|
||||
}
|
||||
|
||||
let component: AnyComponent | undefined
|
||||
let _id: Ref<Doc> | undefined
|
||||
let _class: Ref<Class<Doc>> | undefined
|
||||
|
||||
let viewlets: Map<ActivityKey, TxViewlet>
|
||||
|
||||
const descriptors = createQuery()
|
||||
descriptors.query(activity.class.TxViewlet, {}, (result) => {
|
||||
viewlets = new Map(result.map((r) => [activityKey(r.objectClass, r.txClass), r]))
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="flex h-full">
|
||||
<div class="antiPanel-component border-right filled indent aside">
|
||||
<div class="antiNav-header bottom-divider">
|
||||
<span class="fs-title overflow-label">
|
||||
<Label label={notification.string.Inbox} />
|
||||
</span>
|
||||
</div>
|
||||
<Scroller>
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{:else}
|
||||
{#each docs as doc}
|
||||
<NotificationView value={doc} selected={doc.attachedTo === _id} {viewlets} on:click={selectHandler} />
|
||||
{/each}
|
||||
{/if}
|
||||
</Scroller>
|
||||
</div>
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{:else if component && _id && _class}
|
||||
<Component is={component} props={{ embedded: true, _id, _class }} />
|
||||
{/if}
|
||||
</div>
|
@ -1,6 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 2022 Hardcore Engineering Inc.
|
||||
// Copyright © 2023 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
|
||||
@ -15,199 +14,121 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { TxViewlet } from '@hcengineering/activity'
|
||||
import { ActivityKey, DisplayTx, newDisplayTx, TxView } from '@hcengineering/activity-resources'
|
||||
import core, { Doc, TxCUD, TxProcessor, WithLookup, Ref, Class } from '@hcengineering/core'
|
||||
import { Notification, NotificationStatus } from '@hcengineering/notification'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Button, Component, getPlatformColor, IconBack, IconCheck, IconDelete } from '@hcengineering/ui'
|
||||
import type { AnyComponent } from '@hcengineering/ui'
|
||||
import { ActivityKey } from '@hcengineering/activity-resources'
|
||||
import contact, { EmployeeAccount, getName } from '@hcengineering/contact'
|
||||
import { Avatar, employeeByIdStore } from '@hcengineering/contact-resources'
|
||||
import core, { Doc, Ref, TxCUD, TxProcessor } from '@hcengineering/core'
|
||||
import notification, { DocUpdates } from '@hcengineering/notification'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { AnySvelteComponent, Label, TimeSince } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { getObjectPreview } from '@hcengineering/view-resources'
|
||||
import plugin from '../plugin'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import TxView from './TxView.svelte'
|
||||
|
||||
export let notification: WithLookup<Notification>
|
||||
export let value: DocUpdates
|
||||
export let viewlets: Map<ActivityKey, TxViewlet>
|
||||
export let selected: boolean
|
||||
let doc: Doc | undefined = undefined
|
||||
let tx: TxCUD<Doc> | undefined = undefined
|
||||
|
||||
let account: EmployeeAccount | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
function getDisplayTx (notification: WithLookup<Notification>): DisplayTx | undefined {
|
||||
let tx = notification.$lookup?.tx
|
||||
if (tx) {
|
||||
if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
|
||||
tx = TxProcessor.extractTx(tx) as TxCUD<Doc>
|
||||
$: value.lastTx &&
|
||||
client.findOne(core.class.TxCUD, { _id: value.lastTx }).then((res) => {
|
||||
if (res !== undefined) {
|
||||
tx = TxProcessor.extractTx(res) as TxCUD<Doc>
|
||||
} else {
|
||||
tx = res
|
||||
}
|
||||
return newDisplayTx(tx, hierarchy)
|
||||
}
|
||||
}
|
||||
|
||||
async function changeState (notification: Notification, status: NotificationStatus): Promise<void> {
|
||||
if (notification.status === status) return
|
||||
await client.updateDoc(notification._class, notification.space, notification._id, {
|
||||
status
|
||||
})
|
||||
|
||||
let presenter: AnySvelteComponent | undefined = undefined
|
||||
$: presenterRes =
|
||||
hierarchy.classHierarchyMixin(value.attachedToClass, notification.mixin.NotificationObjectPresenter)?.presenter ??
|
||||
hierarchy.classHierarchyMixin(value.attachedToClass, view.mixin.ObjectPresenter)?.presenter
|
||||
$: if (presenterRes) {
|
||||
getResource(presenterRes).then((res) => (presenter = res))
|
||||
}
|
||||
|
||||
$: displayTx = getDisplayTx(notification)
|
||||
const query = createQuery()
|
||||
$: tx &&
|
||||
query.query(contact.class.EmployeeAccount, { _id: tx.modifiedBy as Ref<EmployeeAccount> }, (r) => ([account] = r))
|
||||
$: employee = account && $employeeByIdStore.get(account.employee)
|
||||
|
||||
let presenter: AnyComponent | undefined
|
||||
let doc: Doc | undefined
|
||||
let visible: boolean = false
|
||||
async function updatePreviewPresenter (ref?: Ref<Class<Doc>>): Promise<void> {
|
||||
presenter = ref !== undefined ? await getObjectPreview(client, ref) : undefined
|
||||
}
|
||||
$: if (displayTx) updatePreviewPresenter(displayTx.tx.objectClass)
|
||||
$: if (presenter !== undefined && displayTx) {
|
||||
client.findOne(displayTx.tx.objectClass, { _id: displayTx.tx.objectId }).then((res) => (doc = res))
|
||||
}
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const docQuery = createQuery()
|
||||
$: docQuery.query(value.attachedToClass, { _id: value.attachedTo }, (res) => ([doc] = res))
|
||||
|
||||
$: newTxes = value.txes.length
|
||||
</script>
|
||||
|
||||
{#if displayTx}
|
||||
{@const isNew = notification.status !== NotificationStatus.Read}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#if doc}
|
||||
<div
|
||||
class="content {isNew ? 'new' : 'readed'} with-document"
|
||||
class:visible
|
||||
style:--color={isNew ? getPlatformColor(11) : '#555555'}
|
||||
on:click|preventDefault|stopPropagation={() => {
|
||||
changeState(notification, NotificationStatus.Read)
|
||||
visible = !visible
|
||||
}}
|
||||
class="container cursor-pointer bottom-divider"
|
||||
class:selected
|
||||
on:click={() => dispatch('click', { _id: value.attachedTo, _class: value.attachedToClass })}
|
||||
>
|
||||
<div class="subheader">
|
||||
<div class="flex-grow">
|
||||
<Component
|
||||
is={view.component.ObjectPresenter}
|
||||
props={{
|
||||
objectId: displayTx.tx.objectId,
|
||||
_class: displayTx.tx.objectClass,
|
||||
value: displayTx.doc,
|
||||
inline: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="buttons-group xsmall-gap">
|
||||
<Button
|
||||
icon={isNew ? IconCheck : IconBack}
|
||||
iconProps={!isNew ? { kind: 'curve' } : {}}
|
||||
kind={'transparent'}
|
||||
showTooltip={{ label: plugin.string.MarkAsRead }}
|
||||
size={'medium'}
|
||||
on:click={() => {
|
||||
if (!isNew && visible) visible = false
|
||||
changeState(notification, isNew ? NotificationStatus.Read : NotificationStatus.Notified)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={IconDelete}
|
||||
kind={'transparent'}
|
||||
showTooltip={{ label: plugin.string.Remove }}
|
||||
size={'medium'}
|
||||
on:click={() => {
|
||||
client.remove(notification)
|
||||
}}
|
||||
/>
|
||||
<div class="content">
|
||||
<div class="header flex">
|
||||
<Avatar avatar={employee?.avatar} size="medium" />
|
||||
<div class="ml-2 w-full">
|
||||
<div class="flex-between mb-1">
|
||||
<div class="caption-color flex">
|
||||
{#if employee}
|
||||
{getName(employee)}
|
||||
{:else}
|
||||
<Label label={core.string.System} />
|
||||
{/if}
|
||||
{#if newTxes > 0}
|
||||
<div class="counter ml-2">
|
||||
{newTxes}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-center">
|
||||
<div class="time ml-2"><TimeSince value={tx?.modifiedOn} /></div>
|
||||
</div>
|
||||
</div>
|
||||
{#if presenter}
|
||||
<svelte:component this={presenter} value={doc} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if tx}
|
||||
<TxView {tx} {viewlets} />
|
||||
{/if}
|
||||
</div>
|
||||
<TxView tx={displayTx} {viewlets} showIcon={false} contentHidden={!visible} />
|
||||
{#if presenter && doc}
|
||||
<div class="document-preview">
|
||||
<Component is={presenter} props={{ object: doc }} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.5rem 0.75rem 0.75rem;
|
||||
min-height: 0;
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: 0.75rem;
|
||||
transition-property: border-color, background-color, height;
|
||||
transition-duration: 0.3s, 0.15s, 0.15s;
|
||||
transition-timing-function: ease-in-out;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
&.new {
|
||||
background-color: var(--popup-bg-hover);
|
||||
}
|
||||
&.readed {
|
||||
background-color: var(--body-accent);
|
||||
}
|
||||
.container {
|
||||
&:hover {
|
||||
border-color: var(--button-border-hover);
|
||||
background-color: var(--board-card-bg-hover);
|
||||
}
|
||||
&.with-document {
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -0.25rem;
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
height: 0.75rem;
|
||||
background-color: var(--body-accent);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 0.5rem;
|
||||
z-index: -1;
|
||||
transition: bottom 0.15s ease-in-out;
|
||||
box-shadow: var(--primary-shadow);
|
||||
}
|
||||
&:hover::before {
|
||||
bottom: -0.4rem;
|
||||
}
|
||||
&.visible::before {
|
||||
bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.subheader {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 0 0.5rem 1.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
min-height: 0;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0.5rem;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
background-color: var(--color);
|
||||
transform: translateY(calc(-50% - 0.25rem));
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
.document-preview {
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
margin-top: -0.5rem;
|
||||
padding: 0;
|
||||
max-height: 0;
|
||||
background-color: var(--body-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 0.5rem;
|
||||
opacity: 0;
|
||||
transition-property: margin-top, max-height, opacity;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 0.15s;
|
||||
}
|
||||
&.visible .document-preview {
|
||||
visibility: visible;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
max-height: max-content;
|
||||
opacity: 1;
|
||||
&.selected {
|
||||
background-color: var(--board-card-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.counter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid;
|
||||
border-color: var(--divider-color);
|
||||
border-radius: 50%;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,114 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 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 activity, { TxViewlet } from '@hcengineering/activity'
|
||||
import { activityKey, ActivityKey } from '@hcengineering/activity-resources'
|
||||
import { EmployeeAccount } from '@hcengineering/contact'
|
||||
import core, { getCurrentAccount, WithLookup } from '@hcengineering/core'
|
||||
import { Notification, NotificationStatus } from '@hcengineering/notification'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Button, IconCheckAll, IconDelete, Scroller, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import notification from '../plugin'
|
||||
import NotificationView from './NotificationView.svelte'
|
||||
|
||||
const query = createQuery()
|
||||
let notifications: WithLookup<Notification>[] = []
|
||||
const client = getClient()
|
||||
|
||||
$: query.query(
|
||||
notification.class.Notification,
|
||||
{
|
||||
attachedTo: (getCurrentAccount() as EmployeeAccount).employee
|
||||
},
|
||||
(res) => {
|
||||
notifications = res
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
'$lookup.tx.modifiedOn': -1
|
||||
},
|
||||
limit: 30,
|
||||
lookup: {
|
||||
tx: core.class.TxCUD
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let viewlets: Map<ActivityKey, TxViewlet>
|
||||
|
||||
const descriptors = createQuery()
|
||||
$: 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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
$: isMobile = $deviceInfo.isMobile
|
||||
</script>
|
||||
|
||||
<div class="notifyPopup" class:justify-center={notifications.length === 0} class:min-w-168={!isMobile}>
|
||||
<div class="header flex-between">
|
||||
<span class="fs-title overflow-label"><Label label={notification.string.Notifications} /></span>
|
||||
{#if notifications.length > 0}
|
||||
<div class="buttons-group xxsmall-gap">
|
||||
<Button
|
||||
icon={IconCheckAll}
|
||||
kind={'list'}
|
||||
showTooltip={{ label: notification.string.MarkAllAsRead }}
|
||||
size={'medium'}
|
||||
on:click={markAsReadNotifications}
|
||||
/>
|
||||
<Button
|
||||
icon={IconDelete}
|
||||
kind={'list'}
|
||||
showTooltip={{ label: notification.string.RemoveAll }}
|
||||
size={'medium'}
|
||||
on:click={deleteNotifications}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if notifications.length > 0}
|
||||
<Scroller padding={'0 .5rem'}>
|
||||
{#each notifications as n}
|
||||
<NotificationView notification={n} {viewlets} />
|
||||
{/each}
|
||||
</Scroller>
|
||||
{:else}
|
||||
<div class="flex-grow flex-center">
|
||||
<Label label={notification.string.NoNotifications} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
302
plugins/notification-resources/src/components/TxView.svelte
Normal file
302
plugins/notification-resources/src/components/TxView.svelte
Normal file
@ -0,0 +1,302 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 { TxViewlet } from '@hcengineering/activity'
|
||||
import {
|
||||
ActivityKey,
|
||||
DisplayTx,
|
||||
getValue,
|
||||
newDisplayTx,
|
||||
TxDisplayViewlet,
|
||||
updateViewlet
|
||||
} from '@hcengineering/activity-resources'
|
||||
import activity from '@hcengineering/activity-resources/src/plugin'
|
||||
import contact, { EmployeeAccount } from '@hcengineering/contact'
|
||||
import core, { AnyAttribute, Doc, Ref, TxCUD } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Component, Label, ShowMore } from '@hcengineering/ui'
|
||||
import type { AttributeModel } from '@hcengineering/view'
|
||||
import { ObjectPresenter } from '@hcengineering/view-resources'
|
||||
|
||||
export let tx: TxCUD<Doc>
|
||||
export let viewlets: Map<ActivityKey, TxViewlet>
|
||||
export let contentHidden: boolean = false
|
||||
const client = getClient()
|
||||
|
||||
let ptx: DisplayTx | undefined
|
||||
let viewlet: TxDisplayViewlet | undefined
|
||||
let props: any
|
||||
|
||||
let employee: EmployeeAccount | undefined
|
||||
let model: AttributeModel[] = []
|
||||
|
||||
$: if (tx._id !== ptx?.tx._id) {
|
||||
ptx = newDisplayTx(tx, client.getHierarchy())
|
||||
if (tx.modifiedBy !== employee?._id) {
|
||||
employee = undefined
|
||||
}
|
||||
props = undefined
|
||||
viewlet = undefined
|
||||
model = []
|
||||
}
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
function getProps (props: any): any {
|
||||
return { ...props, attr: ptx?.collectionAttribute }
|
||||
}
|
||||
|
||||
$: ptx &&
|
||||
updateViewlet(client, viewlets, ptx).then((result) => {
|
||||
if (result.id === tx._id) {
|
||||
viewlet = result.viewlet
|
||||
props = getProps(result.props)
|
||||
model = result.model
|
||||
}
|
||||
})
|
||||
|
||||
$: query.query(
|
||||
contact.class.EmployeeAccount,
|
||||
{ _id: tx.modifiedBy as Ref<EmployeeAccount> },
|
||||
(account) => {
|
||||
;[employee] = account
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
|
||||
function isMessageType (attr?: AnyAttribute): boolean {
|
||||
return attr?.type._class === core.class.TypeMarkup
|
||||
}
|
||||
|
||||
async function updateMessageType (model: AttributeModel[], tx: DisplayTx): Promise<boolean> {
|
||||
for (const m of model) {
|
||||
if (isMessageType(m.attribute)) {
|
||||
return true
|
||||
}
|
||||
const val = await getValue(client, m, tx)
|
||||
if (val.added.length > 1 || val.removed.length > 1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
let hasMessageType = false
|
||||
$: ptx &&
|
||||
updateMessageType(model, ptx).then((res) => {
|
||||
hasMessageType = res
|
||||
})
|
||||
|
||||
$: isComment = viewlet && viewlet?.editable
|
||||
$: isMention = viewlet?.display === 'emphasized' || isMessageType(model[0]?.attribute)
|
||||
$: isColumn = isComment || isMention || hasMessageType
|
||||
</script>
|
||||
|
||||
{#if (viewlet !== undefined && !((viewlet?.hideOnRemove ?? false) && ptx?.removed)) || model.length > 0}
|
||||
<div class="msgactivity-container">
|
||||
<div class="msgactivity-content" class:content={isColumn} class:comment={isComment}>
|
||||
<div class="msgactivity-content__header">
|
||||
<div class="msgactivity-content__title labels-row">
|
||||
{#if viewlet && viewlet?.editable}
|
||||
{#if viewlet.label}
|
||||
<span class="lower"><Label label={viewlet.label} params={viewlet.labelParams ?? {}} /></span>
|
||||
{/if}
|
||||
{#if ptx?.updated}
|
||||
<span class="lower"><Label label={activity.string.Edited} /></span>
|
||||
{/if}
|
||||
{:else if viewlet && viewlet.label}
|
||||
<span class="lower">
|
||||
<Label label={viewlet.label} params={viewlet.labelParams ?? {}} />
|
||||
</span>
|
||||
{#if viewlet.labelComponent}
|
||||
<Component is={viewlet.labelComponent} {props} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if viewlet === undefined && model.length > 0 && ptx?.updateTx}
|
||||
{#each model as m}
|
||||
{#await getValue(client, m, ptx) then value}
|
||||
{#if value.added.length}
|
||||
<span class="lower"><Label label={activity.string.Added} /></span>
|
||||
<span class="lower"><Label label={activity.string.To} /></span>
|
||||
<span class="lower"><Label label={m.label} /></span>
|
||||
{#each value.added as cvalue}
|
||||
{#if value.isObjectAdded}
|
||||
<ObjectPresenter value={cvalue} inline />
|
||||
{:else}
|
||||
<svelte:component this={m.presenter} value={cvalue} inline />
|
||||
{/if}
|
||||
{/each}
|
||||
{:else if value.removed.length}
|
||||
<span class="lower"><Label label={activity.string.Removed} /></span>
|
||||
<span class="lower"><Label label={activity.string.From} /></span>
|
||||
<span class="lower"><Label label={m.label} /></span>
|
||||
{#each value.removed as cvalue}
|
||||
{#if value.isObjectRemoved}
|
||||
<ObjectPresenter value={cvalue} inline />
|
||||
{:else}
|
||||
<svelte:component this={m.presenter} value={cvalue} inline />
|
||||
{/if}
|
||||
{/each}
|
||||
{:else if value.set === null || value.set === undefined || value.set === ''}
|
||||
<span class="lower"><Label label={activity.string.Unset} /></span>
|
||||
<span class="lower"><Label label={m.label} /></span>
|
||||
{:else}
|
||||
<span class="lower"><Label label={activity.string.Changed} /></span>
|
||||
<span class="lower"><Label label={m.label} /></span>
|
||||
<span class="lower"><Label label={activity.string.To} /></span>
|
||||
|
||||
{#if !hasMessageType}
|
||||
<span class="strong">
|
||||
{#if value.isObjectSet}
|
||||
<ObjectPresenter value={value.set} inline />
|
||||
{:else}
|
||||
<svelte:component this={m.presenter} value={value.set} inline />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{/await}
|
||||
{/each}
|
||||
{:else if viewlet === undefined && model.length > 0 && ptx?.mixinTx}
|
||||
{#each model as m}
|
||||
{#await getValue(client, m, ptx) then value}
|
||||
{#if value.set === null || value.set === ''}
|
||||
<span class="lower"><Label label={activity.string.Unset} /></span>
|
||||
<span class="lower"><Label label={m.label} /></span>
|
||||
{:else}
|
||||
<span class="lower"><Label label={activity.string.Changed} /></span>
|
||||
<span class="lower"><Label label={m.label} /></span>
|
||||
<span class="lower"><Label label={activity.string.To} /></span>
|
||||
|
||||
{#if !hasMessageType}
|
||||
<div class="strong">
|
||||
{#if value.isObjectSet}
|
||||
<ObjectPresenter value={value.set} inline />
|
||||
{:else}
|
||||
<svelte:component this={m.presenter} value={value.set} inline />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/await}
|
||||
{/each}
|
||||
{:else if viewlet && viewlet.display === 'inline' && viewlet.component}
|
||||
{#if typeof viewlet.component === 'string'}
|
||||
<Component is={viewlet.component} {props} inline />
|
||||
{:else}
|
||||
<svelte:component this={viewlet.component} {...props} inline />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if viewlet && viewlet.display !== 'inline'}
|
||||
<div class="activity-content content" class:contentHidden>
|
||||
<ShowMore>
|
||||
{#if typeof viewlet.component === 'string'}
|
||||
<Component is={viewlet.component} {props} />
|
||||
{:else}
|
||||
<svelte:component this={viewlet.component} {...props} />
|
||||
{/if}
|
||||
</ShowMore>
|
||||
</div>
|
||||
{:else if hasMessageType && model.length > 0 && (ptx?.updateTx || ptx?.mixinTx)}
|
||||
{#await getValue(client, model[0], ptx) then value}
|
||||
<div class="activity-content content" class:contentHidden>
|
||||
<ShowMore>
|
||||
{#if value.isObjectSet}
|
||||
<ObjectPresenter value={value.set} inline />
|
||||
{:else}
|
||||
<svelte:component this={model[0].presenter} value={value.set} inline />
|
||||
{/if}
|
||||
</ShowMore>
|
||||
</div>
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.msgactivity-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.msgactivity-content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-right: 1rem;
|
||||
color: var(--content-color);
|
||||
|
||||
.msgactivity-content__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.msgactivity-content__title {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.content {
|
||||
flex-direction: column;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
&.comment {
|
||||
.activity-content {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
&:not(.comment) {
|
||||
.msgactivity-content__header {
|
||||
min-height: 1.75rem;
|
||||
}
|
||||
}
|
||||
&:not(.content) {
|
||||
align-items: center;
|
||||
|
||||
.msgactivity-content__header {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.msgactivity-container + .msgactivity-container::before) {
|
||||
content: '';
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
overflow: hidden;
|
||||
visibility: visible;
|
||||
margin-top: 0.125rem;
|
||||
max-height: max-content;
|
||||
opacity: 1;
|
||||
transition-property: max-height, opacity;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 0.15s;
|
||||
|
||||
&.contentHidden {
|
||||
visibility: hidden;
|
||||
padding: 0;
|
||||
margin-top: -0.5rem;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -15,7 +15,7 @@
|
||||
//
|
||||
|
||||
import { Resources } from '@hcengineering/platform'
|
||||
import NotificationsPopup from './components/NotificationsPopup.svelte'
|
||||
import Inbox from './components/Inbox.svelte'
|
||||
import NotificationSettings from './components/NotificationSettings.svelte'
|
||||
import NotificationPresenter from './components/NotificationPresenter.svelte'
|
||||
import { NotificationClientImpl } from './utils'
|
||||
@ -26,7 +26,7 @@ export { default as BrowserNotificatator } from './components/BrowserNotificatat
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
NotificationsPopup,
|
||||
Inbox,
|
||||
NotificationPresenter,
|
||||
NotificationSettings
|
||||
},
|
||||
|
@ -115,6 +115,13 @@ export interface ClassCollaborators extends Class<Doc> {
|
||||
*/
|
||||
export interface TrackedDoc extends Class<Doc> {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface NotificationObjectPresenter extends Class<Doc> {
|
||||
presenter: AnyComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -122,6 +129,18 @@ export interface Collaborators extends Doc {
|
||||
collaborators: Ref<Account>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DocUpdates extends Doc {
|
||||
user: Ref<Account>
|
||||
attachedTo: Ref<Doc>
|
||||
attachedToClass: Ref<Class<Doc>>
|
||||
lastTx?: Ref<TxCUD<Doc>>
|
||||
lastTxTime?: Timestamp
|
||||
txes: [Ref<TxCUD<Doc>>, Timestamp][]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -150,7 +169,8 @@ const notification = plugin(notificationId, {
|
||||
AnotherUserNotifications: '' as Ref<Mixin<AnotherUserNotifications>>,
|
||||
ClassCollaborators: '' as Ref<Mixin<ClassCollaborators>>,
|
||||
Collaborators: '' as Ref<Mixin<Collaborators>>,
|
||||
TrackedDoc: '' as Ref<Mixin<TrackedDoc>>
|
||||
TrackedDoc: '' as Ref<Mixin<TrackedDoc>>,
|
||||
NotificationObjectPresenter: '' as Ref<Mixin<NotificationObjectPresenter>>
|
||||
},
|
||||
class: {
|
||||
LastView: '' as Ref<Class<LastView>>,
|
||||
@ -158,12 +178,12 @@ const notification = plugin(notificationId, {
|
||||
EmailNotification: '' as Ref<Class<EmailNotification>>,
|
||||
NotificationType: '' as Ref<Class<NotificationType>>,
|
||||
NotificationProvider: '' as Ref<Class<NotificationProvider>>,
|
||||
NotificationSetting: '' as Ref<Class<NotificationSetting>>
|
||||
NotificationSetting: '' as Ref<Class<NotificationSetting>>,
|
||||
DocUpdates: '' as Ref<Class<DocUpdates>>
|
||||
},
|
||||
ids: {
|
||||
MentionNotification: '' as Ref<NotificationType>,
|
||||
DMNotification: '' as Ref<NotificationType>,
|
||||
CollaboratorNotification: '' as Ref<NotificationType>,
|
||||
PlatformNotification: '' as Ref<NotificationProvider>,
|
||||
BrowserNotification: '' as Ref<NotificationProvider>,
|
||||
EmailNotification: '' as Ref<NotificationProvider>,
|
||||
@ -173,7 +193,7 @@ const notification = plugin(notificationId, {
|
||||
MobileApp: '' as Ref<IntegrationType>
|
||||
},
|
||||
component: {
|
||||
NotificationsPopup: '' as AnyComponent,
|
||||
Inbox: '' as AnyComponent,
|
||||
NotificationPresenter: '' as AnyComponent
|
||||
},
|
||||
icon: {
|
||||
@ -186,7 +206,8 @@ const notification = plugin(notificationId, {
|
||||
},
|
||||
string: {
|
||||
Notification: '' as IntlString,
|
||||
Notifications: '' as IntlString
|
||||
Notifications: '' as IntlString,
|
||||
Inbox: '' as IntlString
|
||||
},
|
||||
function: {
|
||||
GetNotificationClient: '' as Resource<NotificationClientFactoy>
|
||||
|
@ -28,6 +28,7 @@
|
||||
import VacancyApplications from './VacancyApplications.svelte'
|
||||
|
||||
export let _id: Ref<Vacancy>
|
||||
export let embedded = false
|
||||
|
||||
let object: Required<Vacancy>
|
||||
let rawName: string = ''
|
||||
@ -82,6 +83,7 @@
|
||||
title={object.name}
|
||||
isHeader={true}
|
||||
isAside={true}
|
||||
{embedded}
|
||||
{object}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
@ -100,15 +102,11 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="attributes" let:direction={dir}>
|
||||
{#if dir === 'column'}
|
||||
<div class="ac-subtitle">
|
||||
<div class="ac-subtitle-content">
|
||||
<DocAttributeBar
|
||||
{object}
|
||||
{mixins}
|
||||
ignoreKeys={['name', 'description', 'fullDescription', 'private', 'archived']}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DocAttributeBar
|
||||
{object}
|
||||
{mixins}
|
||||
ignoreKeys={['name', 'description', 'fullDescription', 'private', 'archived']}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
|
@ -48,7 +48,6 @@
|
||||
"@hcengineering/task": "^0.6.3",
|
||||
"@hcengineering/chunter": "^0.6.2",
|
||||
"@hcengineering/notification": "^0.6.8",
|
||||
"@hcengineering/notification-resources": "^0.6.0",
|
||||
"@hcengineering/contact": "^0.6.11",
|
||||
"@hcengineering/contact-resources": "^0.6.0",
|
||||
"@hcengineering/view-resources": "^0.6.0",
|
||||
|
@ -0,0 +1,53 @@
|
||||
<!--
|
||||
// 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 { createQuery, statusStore } from '@hcengineering/presentation'
|
||||
import type { Issue, Project } from '@hcengineering/tracker'
|
||||
import { Icon } from '@hcengineering/ui'
|
||||
import tracker from '../../plugin'
|
||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||
|
||||
export let value: Issue
|
||||
|
||||
const spaceQuery = createQuery()
|
||||
let currentProject: Project | undefined = undefined
|
||||
|
||||
spaceQuery.query(tracker.class.Project, { _id: value.space }, (res) => ([currentProject] = res))
|
||||
|
||||
$: title = currentProject ? `${currentProject.identifier}-${value?.number}` : `${value?.number}`
|
||||
$: status = $statusStore.byId.get(value.status)
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="flex-between">
|
||||
<div class="flex-center">
|
||||
{#if currentProject}
|
||||
{#if currentProject.icon}
|
||||
<Icon icon={currentProject.icon ?? tracker.icon.Home} size="inline" />
|
||||
{/if}
|
||||
<div class="ml-1 mr-1">
|
||||
{currentProject.name}
|
||||
</div>
|
||||
{/if}
|
||||
{title}
|
||||
{value.title}
|
||||
</div>
|
||||
<div>
|
||||
{#if status}
|
||||
<IssueStatusIcon value={status} size="small" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
@ -44,6 +44,7 @@
|
||||
|
||||
export let _id: Ref<Issue>
|
||||
export let _class: Ref<Class<Issue>>
|
||||
export let embedded = false
|
||||
|
||||
let lastId: Ref<Doc> = _id
|
||||
let lastClass: Ref<Class<Doc>> = _class
|
||||
@ -145,6 +146,7 @@
|
||||
isAside={true}
|
||||
isSub={false}
|
||||
withoutActivity={false}
|
||||
{embedded}
|
||||
withoutTitle
|
||||
bind:innerWidth
|
||||
on:close={() => dispatch('close')}
|
||||
|
@ -72,6 +72,7 @@ import SprintLeadPresenter from './components/sprints/SprintLeadPresenter.svelte
|
||||
import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte'
|
||||
import Views from './components/views/Views.svelte'
|
||||
import Statuses from './components/workflow/Statuses.svelte'
|
||||
import NotificationIssuePresenter from './components/issues/NotificationIssuePresenter.svelte'
|
||||
|
||||
import {
|
||||
getIssueId,
|
||||
@ -418,7 +419,8 @@ export default async (): Promise<Resources> => ({
|
||||
TimeSpendReportPopup,
|
||||
SprintComponentEditor,
|
||||
SprintDatePresenter,
|
||||
SprintLeadPresenter
|
||||
SprintLeadPresenter,
|
||||
NotificationIssuePresenter
|
||||
},
|
||||
completion: {
|
||||
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
||||
|
@ -39,6 +39,7 @@
|
||||
|
||||
export let _id: Ref<Doc>
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let embedded = false
|
||||
|
||||
let realObjectClass: Ref<Class<Doc>> = _class
|
||||
let lastId: Ref<Doc> = _id
|
||||
@ -277,6 +278,7 @@
|
||||
{icon}
|
||||
{title}
|
||||
{object}
|
||||
{embedded}
|
||||
isHeader={mainEditor?.pinned ?? false}
|
||||
isAside={true}
|
||||
bind:panelWidth
|
||||
|
@ -120,13 +120,8 @@ export async function getListItemPresenter (client: Client, _class: Ref<Class<Ob
|
||||
* @public
|
||||
*/
|
||||
export async function getObjectPreview (client: Client, _class: Ref<Class<Obj>>): Promise<AnyComponent | undefined> {
|
||||
const clazz = client.getHierarchy().getClass(_class)
|
||||
const presenterMixin = client.getHierarchy().as(clazz, view.mixin.PreviewPresenter)
|
||||
if (presenterMixin.presenter === undefined) {
|
||||
if (clazz.extends !== undefined) {
|
||||
return await getObjectPreview(client, clazz.extends)
|
||||
}
|
||||
}
|
||||
const hierarchy = client.getHierarchy()
|
||||
const presenterMixin = hierarchy.classHierarchyMixin(_class, view.mixin.PreviewPresenter)
|
||||
return presenterMixin?.presenter
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,7 @@
|
||||
location,
|
||||
Location,
|
||||
navigate,
|
||||
NavLink,
|
||||
openPanel,
|
||||
PanelInstance,
|
||||
Popup,
|
||||
@ -61,12 +62,12 @@
|
||||
import SpaceView from './SpaceView.svelte'
|
||||
import login from '@hcengineering/login'
|
||||
import { workspacesStore } from '../utils'
|
||||
import App from './App.svelte'
|
||||
|
||||
let contentPanel: HTMLElement
|
||||
let shownMenu: boolean = false
|
||||
|
||||
const { setTheme } = getContext('theme') as any
|
||||
NotificationClientImpl.createClient()
|
||||
|
||||
let currentAppAlias: string | undefined
|
||||
let currentSpace: Ref<Space> | undefined
|
||||
@ -86,6 +87,7 @@
|
||||
const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? []
|
||||
|
||||
const client = getClient()
|
||||
NotificationClientImpl.createClient()
|
||||
|
||||
let apps: Application[] | Promise<Application[]> = client
|
||||
.findAll(workbench.class.Application, { hidden: false, _id: { $nin: excludedApps } })
|
||||
@ -546,7 +548,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<Applications
|
||||
apps={getApps(apps)}
|
||||
apps={getApps(apps).filter((p) => p.position !== 'bottom')}
|
||||
active={currentApplication?._id}
|
||||
direction={appsDirection}
|
||||
bind:shown={shownMenu}
|
||||
@ -570,15 +572,11 @@
|
||||
}}
|
||||
notify={false}
|
||||
/>
|
||||
<AppItem
|
||||
icon={notification.icon.Notifications}
|
||||
label={notification.string.Notifications}
|
||||
selected={false}
|
||||
action={async () => {
|
||||
showPopup(notification.component.NotificationsPopup, {}, popupPosition)
|
||||
}}
|
||||
notify={hasNotification}
|
||||
/>
|
||||
{#each getApps(apps).filter((p) => p.position === 'bottom') as app}
|
||||
<NavLink app={app.alias}>
|
||||
<App selected={app._id === currentApplication?._id} icon={app.icon} label={app.label} />
|
||||
</NavLink>
|
||||
{/each}
|
||||
<div class="flex-center" class:mt-2={appsDirection === 'vertical'} class:ml-2={appsDirection === 'horizontal'}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
|
@ -28,6 +28,7 @@ export interface Application extends Doc {
|
||||
alias: string
|
||||
icon: Asset
|
||||
hidden: boolean
|
||||
position?: 'top' | 'bottom'
|
||||
navigatorModel?: NavigatorModel
|
||||
locationResolver?: Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
|
||||
|
||||
|
@ -23,38 +23,41 @@ import core, {
|
||||
AttachedDoc,
|
||||
Class,
|
||||
Doc,
|
||||
generateId,
|
||||
DocumentUpdate,
|
||||
Hierarchy,
|
||||
IdMap,
|
||||
MixinUpdate,
|
||||
Ref,
|
||||
RefTo,
|
||||
Space,
|
||||
Timestamp,
|
||||
toIdMap,
|
||||
Tx,
|
||||
TxCUD,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxMixin,
|
||||
TxProcessor,
|
||||
TxUpdateDoc
|
||||
TxRemoveDoc,
|
||||
TxUpdateDoc,
|
||||
generateId
|
||||
} from '@hcengineering/core'
|
||||
import notification, {
|
||||
ClassCollaborators,
|
||||
Collaborators,
|
||||
DocUpdates,
|
||||
EmailNotification,
|
||||
LastView,
|
||||
NotificationProvider,
|
||||
NotificationType
|
||||
} from '@hcengineering/notification'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import type { TriggerControl } from '@hcengineering/server-core'
|
||||
import serverNotification, {
|
||||
HTMLPresenter,
|
||||
TextPresenter,
|
||||
createLastViewTx,
|
||||
getEmployeeAccount,
|
||||
getEmployeeAccountById,
|
||||
getUpdateLastViewTx,
|
||||
HTMLPresenter,
|
||||
TextPresenter
|
||||
getUpdateLastViewTx
|
||||
} from '@hcengineering/server-notification'
|
||||
import { Content } from './types'
|
||||
import { replaceAll } from './utils'
|
||||
@ -378,6 +381,29 @@ export async function UpdateLastView (tx: Tx, control: TriggerControl): Promise<
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnUpdateLastView (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxUpdateDoc<LastView>
|
||||
if (actualTx._class !== core.class.TxUpdateDoc) return []
|
||||
if (actualTx.objectClass !== notification.class.LastView) return []
|
||||
const result: Tx[] = []
|
||||
for (const key in actualTx.operations) {
|
||||
const docs = await control.findAll(notification.class.DocUpdates, { attachedTo: key as Ref<Doc> })
|
||||
for (const doc of docs) {
|
||||
const txes = doc.txes.filter((p) => p[1] > actualTx.operations[key])
|
||||
result.push(
|
||||
control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, {
|
||||
txes
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function getBacklink (ptx: TxCollectionCUD<Doc, Backlink>): Backlink {
|
||||
return TxProcessor.createDoc2Doc(ptx.tx as TxCreateDoc<Backlink>)
|
||||
}
|
||||
@ -394,20 +420,16 @@ async function getBacklinkDoc (backlink: Backlink, control: TriggerControl): Pro
|
||||
)[0]
|
||||
}
|
||||
|
||||
async function getValueCollaborators (
|
||||
value: any,
|
||||
attr: AnyAttribute,
|
||||
control: TriggerControl
|
||||
): Promise<EmployeeAccount[]> {
|
||||
async function getValueCollaborators (value: any, attr: AnyAttribute, control: TriggerControl): Promise<Ref<Account>[]> {
|
||||
const hierarchy = control.hierarchy
|
||||
if (attr.type._class === core.class.RefTo) {
|
||||
const to = (attr.type as RefTo<Doc>).to
|
||||
if (hierarchy.isDerived(to, contact.class.Employee)) {
|
||||
const acc = await getEmployeeAccount(value, control)
|
||||
return acc !== undefined ? [acc] : []
|
||||
return acc !== undefined ? [acc._id] : []
|
||||
} else if (hierarchy.isDerived(to, core.class.Account)) {
|
||||
const acc = await getEmployeeAccountById(value, control)
|
||||
return acc !== undefined ? [acc] : []
|
||||
return acc !== undefined ? [acc._id] : []
|
||||
}
|
||||
} else if (attr.type._class === core.class.ArrOf) {
|
||||
const arrOf = (attr.type as ArrOf<RefTo<Doc>>).of
|
||||
@ -417,12 +439,12 @@ async function getValueCollaborators (
|
||||
const employeeAccounts = await control.modelDb.findAll(contact.class.EmployeeAccount, {
|
||||
employee: { $in: Array.isArray(value) ? value : [value] }
|
||||
})
|
||||
return employeeAccounts
|
||||
return employeeAccounts.map((p) => p._id)
|
||||
} else if (hierarchy.isDerived(to, core.class.Account)) {
|
||||
const employeeAccounts = await control.modelDb.findAll(contact.class.EmployeeAccount, {
|
||||
_id: { $in: Array.isArray(value) ? value : [value] }
|
||||
})
|
||||
return employeeAccounts
|
||||
return employeeAccounts.map((p) => p._id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -434,7 +456,7 @@ async function getKeyCollaborators (
|
||||
value: any,
|
||||
field: string,
|
||||
control: TriggerControl
|
||||
): Promise<EmployeeAccount[] | undefined> {
|
||||
): Promise<Ref<Account>[] | undefined> {
|
||||
if (value !== undefined && value !== null) {
|
||||
const attr = control.hierarchy.findAttribute(doc._class, field)
|
||||
if (attr !== undefined) {
|
||||
@ -447,84 +469,180 @@ async function getDocCollaborators (
|
||||
doc: Doc,
|
||||
mixin: ClassCollaborators,
|
||||
control: TriggerControl
|
||||
): Promise<EmployeeAccount[]> {
|
||||
const collaborators: IdMap<EmployeeAccount> = new Map()
|
||||
): Promise<Ref<Account>[]> {
|
||||
const collaborators: Set<Ref<Account>> = new Set()
|
||||
for (const field of mixin.fields) {
|
||||
const value = (doc as any)[field]
|
||||
const newCollaborators = await getKeyCollaborators(doc, value, field, control)
|
||||
if (newCollaborators !== undefined) {
|
||||
for (const newCollaborator of newCollaborators) {
|
||||
collaborators.set(newCollaborator._id, newCollaborator)
|
||||
collaborators.add(newCollaborator)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(collaborators.values())
|
||||
}
|
||||
|
||||
async function createCollabDocInfo (
|
||||
collaborators: Ref<Account>[],
|
||||
tx: TxCUD<Doc>,
|
||||
objectId: Ref<Doc>,
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
control: TriggerControl,
|
||||
txId?: Ref<TxCUD<Doc>>
|
||||
): Promise<TxCUD<DocUpdates>[]> {
|
||||
const res: TxCUD<DocUpdates>[] = []
|
||||
const targets = new Set(collaborators)
|
||||
const docs = await control.findAll(notification.class.DocUpdates, { attachedTo: objectId })
|
||||
for (const doc of docs) {
|
||||
if (tx.modifiedBy === doc.user || !targets.delete(doc.user)) continue
|
||||
res.push(
|
||||
control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, {
|
||||
$push: {
|
||||
txes: [txId ?? tx._id, tx.modifiedOn]
|
||||
}
|
||||
})
|
||||
)
|
||||
res.push(
|
||||
control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, {
|
||||
lastTx: txId ?? tx._id,
|
||||
lastTxTime: tx.modifiedOn
|
||||
})
|
||||
)
|
||||
}
|
||||
for (const target of targets) {
|
||||
if (tx.modifiedBy === target) continue
|
||||
res.push(
|
||||
control.txFactory.createTxCreateDoc(notification.class.DocUpdates, notification.space.Notifications, {
|
||||
user: target,
|
||||
attachedTo: objectId,
|
||||
attachedToClass: objectClass,
|
||||
lastTx: txId ?? tx._id,
|
||||
lastTxTime: tx.modifiedOn,
|
||||
txes: [[txId ?? tx._id, tx.modifiedOn]]
|
||||
})
|
||||
)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
function getMixinTx (
|
||||
actualTx: TxCUD<Doc>,
|
||||
control: TriggerControl,
|
||||
collaborators: EmployeeAccount[]
|
||||
collaborators: Ref<Account>[]
|
||||
): TxMixin<Doc, Collaborators> {
|
||||
return control.txFactory.createTxMixin(
|
||||
const tx = control.txFactory.createTxMixin(
|
||||
actualTx.objectId,
|
||||
actualTx.objectClass,
|
||||
actualTx.objectSpace,
|
||||
notification.mixin.Collaborators,
|
||||
{
|
||||
collaborators: collaborators.map((p) => p._id)
|
||||
collaborators
|
||||
}
|
||||
)
|
||||
return tx
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function CreateCollaboratorDoc (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
export async function createCollaboratorDoc (
|
||||
tx: TxCreateDoc<Doc>,
|
||||
control: TriggerControl,
|
||||
txId?: Ref<TxCUD<Doc>>
|
||||
): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
const hierarchy = control.hierarchy
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
|
||||
|
||||
if (actualTx._class !== core.class.TxCreateDoc) return []
|
||||
const mixin = hierarchy.classHierarchyMixin(actualTx.objectClass, notification.mixin.ClassCollaborators)
|
||||
const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators)
|
||||
if (mixin !== undefined) {
|
||||
const doc = TxProcessor.createDoc2Doc(actualTx)
|
||||
const doc = TxProcessor.createDoc2Doc(tx)
|
||||
const collaborators = await getDocCollaborators(doc, mixin, control)
|
||||
|
||||
const mixinTx = getMixinTx(actualTx, control, collaborators)
|
||||
const mixinTx = getMixinTx(tx, control, collaborators)
|
||||
const notificationTxes = await createCollabDocInfo(collaborators, tx, doc._id, doc._class, control, txId)
|
||||
res.push(mixinTx)
|
||||
res.push(...notificationTxes)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function collaboratorDocHandler (tx: Tx, control: TriggerControl, txId?: Ref<TxCUD<Doc>>): Promise<Tx[]> {
|
||||
switch (tx._class) {
|
||||
case core.class.TxCreateDoc:
|
||||
if (tx.space === core.space.DerivedTx) return []
|
||||
return await createCollaboratorDoc(tx as TxCreateDoc<Doc>, control, txId)
|
||||
case core.class.TxUpdateDoc:
|
||||
case core.class.TxMixin:
|
||||
if (tx.space === core.space.DerivedTx) return []
|
||||
return await updateCollaboratorDoc(tx as TxUpdateDoc<Doc>, control, txId)
|
||||
case core.class.TxRemoveDoc:
|
||||
return await removeCollaboratorDoc(tx as TxRemoveDoc<Doc>, control)
|
||||
case core.class.TxCollectionCUD:
|
||||
return await collectionCollabDoc(tx as TxCollectionCUD<Doc, AttachedDoc>, control)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async function collectionCollabDoc (tx: TxCollectionCUD<Doc, AttachedDoc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
let res = await collaboratorDocHandler(actualTx, control, tx._id)
|
||||
const doc = (await control.findAll(tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
|
||||
if (doc !== undefined) {
|
||||
if (control.hierarchy.hasMixin(doc, notification.mixin.Collaborators)) {
|
||||
const collabMixin = control.hierarchy.as(doc, notification.mixin.Collaborators)
|
||||
res = res.concat(
|
||||
await createCollabDocInfo(collabMixin.collaborators, tx, tx.objectId, tx.objectClass, control, tx._id)
|
||||
)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async function removeCollaboratorDoc (tx: TxRemoveDoc<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
const hierarchy = control.hierarchy
|
||||
const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators)
|
||||
if (mixin !== undefined) {
|
||||
const docUpdates = await control.findAll(notification.class.DocUpdates, { attachedTo: tx.objectId })
|
||||
for (const doc of docUpdates) {
|
||||
res.push(control.txFactory.createTxRemoveDoc(doc._class, doc.space, doc._id))
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async function getNewCollaborators (
|
||||
actualTx: TxUpdateDoc<Doc>,
|
||||
ops: DocumentUpdate<Doc> | MixinUpdate<Doc, Doc>,
|
||||
mixin: ClassCollaborators,
|
||||
doc: Doc,
|
||||
control: TriggerControl
|
||||
): Promise<EmployeeAccount[]> {
|
||||
const newCollaborators: IdMap<EmployeeAccount> = new Map()
|
||||
if (actualTx.operations.$push !== undefined) {
|
||||
for (const key in actualTx.operations.$push) {
|
||||
): Promise<Ref<Account>[]> {
|
||||
const newCollaborators: Set<Ref<Account>> = new Set()
|
||||
if (ops.$push !== undefined) {
|
||||
for (const key in ops.$push) {
|
||||
if (mixin.fields.includes(key)) {
|
||||
const value = (actualTx.operations.$push as any)[key]
|
||||
const value = (ops.$push as any)[key]
|
||||
const newCollabs = await getKeyCollaborators(doc, value, key, control)
|
||||
if (newCollabs !== undefined) {
|
||||
for (const newCollab of newCollabs) {
|
||||
newCollaborators.set(newCollab._id, newCollab)
|
||||
newCollaborators.add(newCollab)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key in actualTx.operations) {
|
||||
for (const key in ops) {
|
||||
if (key.startsWith('$')) continue
|
||||
if (mixin.fields.includes(key)) {
|
||||
const value = (actualTx.operations as any)[key]
|
||||
const value = (ops as any)[key]
|
||||
const newCollabs = await getKeyCollaborators(doc, value, key, control)
|
||||
if (newCollabs !== undefined) {
|
||||
for (const newCollab of newCollabs) {
|
||||
newCollaborators.set(newCollab._id, newCollab)
|
||||
newCollaborators.add(newCollab)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -532,54 +650,57 @@ async function getNewCollaborators (
|
||||
return Array.from(newCollaborators.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function UpdateCollaboratorDoc (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const hierarchy = control.hierarchy
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxUpdateDoc<Doc>
|
||||
function isMixinTx (tx: TxUpdateDoc<Doc> | TxMixin<Doc, Doc>): tx is TxMixin<Doc, Doc> {
|
||||
return tx._class === core.class.TxMixin
|
||||
}
|
||||
|
||||
if (actualTx._class !== core.class.TxUpdateDoc) return []
|
||||
const clazz = hierarchy.getClass(actualTx.objectClass)
|
||||
if (!hierarchy.hasMixin(clazz, notification.mixin.ClassCollaborators)) return []
|
||||
const mixin = hierarchy.as(clazz, notification.mixin.ClassCollaborators)
|
||||
const doc = (await control.findAll(actualTx.objectClass, { _id: actualTx.objectId }, { limit: 1 }))[0]
|
||||
async function updateCollaboratorDoc (
|
||||
tx: TxUpdateDoc<Doc> | TxMixin<Doc, Doc>,
|
||||
control: TriggerControl,
|
||||
txId?: Ref<TxCUD<Doc>>
|
||||
): Promise<Tx[]> {
|
||||
const hierarchy = control.hierarchy
|
||||
let res: Tx[] = []
|
||||
const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators)
|
||||
if (mixin === undefined) return []
|
||||
const doc = (await control.findAll(tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
|
||||
if (doc === undefined) return []
|
||||
let collaborators: EmployeeAccount[] = []
|
||||
let mixinTx: TxMixin<Doc, Collaborators> | undefined
|
||||
if (hierarchy.hasMixin(doc, notification.mixin.Collaborators)) {
|
||||
// we should handle change field and subscribe new collaborators
|
||||
const collabMixin = hierarchy.as(doc, notification.mixin.Collaborators)
|
||||
const oldCollaborators = await control.modelDb.findAll(contact.class.EmployeeAccount, {
|
||||
_id: { $in: collabMixin.collaborators as Ref<EmployeeAccount>[] }
|
||||
})
|
||||
const collabs = toIdMap(oldCollaborators)
|
||||
const newCollaborators = (await getNewCollaborators(actualTx, mixin, doc, control)).filter(
|
||||
(p) => !collabs.has(p._id)
|
||||
)
|
||||
const collabs = new Set(collabMixin.collaborators)
|
||||
const ops = isMixinTx(tx) ? tx.attributes : tx.operations
|
||||
const newCollaborators = (await getNewCollaborators(ops, mixin, doc, control)).filter((p) => !collabs.has(p))
|
||||
|
||||
if (newCollaborators.length > 0) {
|
||||
mixinTx = control.txFactory.createTxMixin(
|
||||
actualTx.objectId,
|
||||
actualTx.objectClass,
|
||||
actualTx.objectSpace,
|
||||
notification.mixin.Collaborators,
|
||||
{
|
||||
res.push(
|
||||
control.txFactory.createTxMixin(tx.objectId, tx.objectClass, tx.objectSpace, notification.mixin.Collaborators, {
|
||||
$push: {
|
||||
collaborators: {
|
||||
$each: newCollaborators.map((p) => p._id),
|
||||
$each: newCollaborators,
|
||||
$position: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
res = res.concat(
|
||||
await createCollabDocInfo(
|
||||
[...collabMixin.collaborators, ...newCollaborators],
|
||||
tx,
|
||||
doc._id,
|
||||
doc._class,
|
||||
control,
|
||||
txId
|
||||
)
|
||||
)
|
||||
} else {
|
||||
collaborators = await getDocCollaborators(doc, mixin, control)
|
||||
mixinTx = getMixinTx(actualTx, control, collaborators)
|
||||
const collaborators = await getDocCollaborators(doc, mixin, control)
|
||||
res.push(getMixinTx(tx, control, collaborators))
|
||||
res = res.concat(await createCollabDocInfo(collaborators, tx, doc._id, doc._class, control, txId))
|
||||
}
|
||||
|
||||
return mixinTx !== undefined ? [mixinTx] : []
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
@ -626,9 +747,9 @@ export * from './types'
|
||||
export default async () => ({
|
||||
trigger: {
|
||||
OnBacklinkCreate,
|
||||
CollaboratorDocHandler: collaboratorDocHandler,
|
||||
OnUpdateLastView,
|
||||
UpdateLastView,
|
||||
CreateCollaboratorDoc,
|
||||
UpdateCollaboratorDoc,
|
||||
OnAddCollborator
|
||||
}
|
||||
})
|
||||
|
@ -15,9 +15,9 @@
|
||||
//
|
||||
|
||||
import contact, { Employee, EmployeeAccount } from '@hcengineering/contact'
|
||||
import core, { Account, Class, Doc, Mixin, Ref, TxCreateDoc, TxFactory, TxUpdateDoc } from '@hcengineering/core'
|
||||
import { Account, Class, Doc, Mixin, Ref, TxCreateDoc, TxFactory, TxUpdateDoc } from '@hcengineering/core'
|
||||
import notification, { LastView } from '@hcengineering/notification'
|
||||
import { Plugin, plugin, Resource } from '@hcengineering/platform'
|
||||
import { Plugin, Resource, plugin } from '@hcengineering/platform'
|
||||
import type { TriggerControl, TriggerFunc } from '@hcengineering/server-core'
|
||||
|
||||
/**
|
||||
@ -43,7 +43,7 @@ export async function getUpdateLastViewTx (
|
||||
{ limit: 1 }
|
||||
)
|
||||
)[0]
|
||||
const factory = new TxFactory(user)
|
||||
const factory = new TxFactory(user, true)
|
||||
if (current !== undefined) {
|
||||
if (current[attachedTo] === -1 || current[attachedTo] >= lastView) {
|
||||
return
|
||||
@ -51,14 +51,12 @@ export async function getUpdateLastViewTx (
|
||||
const u = factory.createTxUpdateDoc(current._class, current.space, current._id, {
|
||||
[attachedTo]: lastView
|
||||
})
|
||||
u.space = core.space.DerivedTx
|
||||
return u
|
||||
} else {
|
||||
const u = factory.createTxCreateDoc(notification.class.LastView, notification.space.Notifications, {
|
||||
user,
|
||||
[attachedTo]: lastView
|
||||
})
|
||||
u.space = core.space.DerivedTx
|
||||
return u
|
||||
}
|
||||
}
|
||||
@ -134,19 +132,17 @@ export async function createLastViewTx (
|
||||
{ limit: 1 }
|
||||
)
|
||||
)[0]
|
||||
const factory = new TxFactory(user)
|
||||
const factory = new TxFactory(user, true)
|
||||
if (current === undefined) {
|
||||
const u = factory.createTxCreateDoc(notification.class.LastView, notification.space.Notifications, {
|
||||
user,
|
||||
[attachedTo]: 1
|
||||
})
|
||||
u.space = core.space.DerivedTx
|
||||
return u
|
||||
} else if (current[attachedTo] === undefined) {
|
||||
const u = factory.createTxUpdateDoc(current._class, current.space, current._id, {
|
||||
[attachedTo]: 1
|
||||
})
|
||||
u.space = core.space.DerivedTx
|
||||
return u
|
||||
}
|
||||
}
|
||||
@ -181,8 +177,8 @@ export default plugin(serverNotificationId, {
|
||||
trigger: {
|
||||
OnBacklinkCreate: '' as Resource<TriggerFunc>,
|
||||
UpdateLastView: '' as Resource<TriggerFunc>,
|
||||
CreateCollaboratorDoc: '' as Resource<TriggerFunc>,
|
||||
UpdateCollaboratorDoc: '' as Resource<TriggerFunc>,
|
||||
OnUpdateLastView: '' as Resource<TriggerFunc>,
|
||||
CollaboratorDocHandler: '' as Resource<TriggerFunc>,
|
||||
OnAddCollborator: '' as Resource<TriggerFunc>
|
||||
}
|
||||
})
|
||||
|
@ -44,7 +44,7 @@ import type { FullTextAdapter, IndexedDoc, WithFind } from './types'
|
||||
* @public
|
||||
*/
|
||||
export class FullTextIndex implements WithFind {
|
||||
txFactory = new TxFactory(core.account.System)
|
||||
txFactory = new TxFactory(core.account.System, true)
|
||||
|
||||
consistency: Promise<void> | undefined
|
||||
|
||||
|
@ -101,7 +101,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
}
|
||||
|
||||
async markRemove (doc: DocIndexState): Promise<void> {
|
||||
const ops = new TxFactory(core.account.System)
|
||||
const ops = new TxFactory(core.account.System, true)
|
||||
await this.storage.tx(
|
||||
ops.createTxUpdateDoc(doc._class, doc.space, doc._id, {
|
||||
removed: true
|
||||
|
@ -183,7 +183,7 @@ export async function loadIndexStageStage (
|
||||
const newIndex = ((attributes.index as number) ?? 0) + 1
|
||||
result = `${newIndex}`
|
||||
|
||||
const ops = new TxFactory(core.account.System)
|
||||
const ops = new TxFactory(core.account.System, true)
|
||||
const data = {
|
||||
stageId,
|
||||
attributes: {
|
||||
|
@ -26,7 +26,7 @@ export class AsyncTriggerProcessor {
|
||||
|
||||
classes: Ref<Class<Doc>>[] = []
|
||||
|
||||
factory = new TxFactory(core.account.System)
|
||||
factory = new TxFactory(core.account.System, true)
|
||||
|
||||
functions: AsyncTriggerFunc[] = []
|
||||
|
||||
|
@ -209,7 +209,7 @@ class TServerStorage implements ServerStorage {
|
||||
attachedTo: D,
|
||||
update: DocumentUpdate<D>
|
||||
): Promise<Tx> {
|
||||
const txFactory = new TxFactory(modifiedBy)
|
||||
const txFactory = new TxFactory(modifiedBy, true)
|
||||
const baseClass = this.hierarchy.getBaseClass(_class)
|
||||
if (baseClass !== _class) {
|
||||
// Mixin operation is required.
|
||||
@ -413,7 +413,7 @@ class TServerStorage implements ServerStorage {
|
||||
|
||||
private deleteObject (ctx: MeasureContext, object: Doc, removedMap: Map<Ref<Doc>, Doc>): Tx[] {
|
||||
const result: Tx[] = []
|
||||
const factory = new TxFactory(object.modifiedBy)
|
||||
const factory = new TxFactory(object.modifiedBy, true)
|
||||
if (this.hierarchy.isDerived(object._class, core.class.AttachedDoc)) {
|
||||
const adoc = object as AttachedDoc
|
||||
const nestedTx = factory.createTxRemoveDoc(adoc._class, adoc.space, adoc._id)
|
||||
@ -468,7 +468,7 @@ class TServerStorage implements ServerStorage {
|
||||
if (rtx.operations.space === undefined || rtx.operations.space === rtx.objectSpace) {
|
||||
continue
|
||||
}
|
||||
const factory = new TxFactory(tx.modifiedBy)
|
||||
const factory = new TxFactory(tx.modifiedBy, true)
|
||||
for (const [, attribute] of this.hierarchy.getAllAttributes(rtx.objectClass)) {
|
||||
if (!this.hierarchy.isDerived(attribute.type._class, core.class.Collection)) {
|
||||
continue
|
||||
|
@ -43,7 +43,7 @@ export class Triggers {
|
||||
}
|
||||
|
||||
async apply (account: Ref<Account>, tx: Tx, ctrl: Omit<TriggerControl, 'txFactory'>): Promise<Tx[]> {
|
||||
const derived = this.triggers.map((trigger) => trigger(tx, { ...ctrl, txFactory: new TxFactory(account) }))
|
||||
const derived = this.triggers.map((trigger) => trigger(tx, { ...ctrl, txFactory: new TxFactory(account, true) }))
|
||||
const result = await Promise.all(derived)
|
||||
return result.flatMap((x) => x)
|
||||
}
|
||||
|
@ -175,18 +175,16 @@ class SessionManager {
|
||||
)[0]
|
||||
if (user === undefined) return
|
||||
const status = (await session.findAll(ctx, core.class.UserStatus, { modifiedBy: user._id }, { limit: 1 }))[0]
|
||||
const txFactory = new TxFactory(user._id)
|
||||
const txFactory = new TxFactory(user._id, true)
|
||||
if (status === undefined) {
|
||||
const tx = txFactory.createTxCreateDoc(core.class.UserStatus, user._id as string as Ref<Space>, {
|
||||
online
|
||||
})
|
||||
tx.space = core.space.DerivedTx
|
||||
await session.tx(ctx, tx)
|
||||
} else if (status.online !== online) {
|
||||
const tx = txFactory.createTxUpdateDoc(status._class, status.space, status._id, {
|
||||
online
|
||||
})
|
||||
tx.space = core.space.DerivedTx
|
||||
await session.tx(ctx, tx)
|
||||
}
|
||||
} catch {}
|
||||
|
Loading…
Reference in New Issue
Block a user