TSK-1051 TSK-1052 TSK-1061 Inbox actions (#2914)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-04-06 20:25:58 +06:00 committed by GitHub
parent 44de234d01
commit 210d3eebc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 193 additions and 23 deletions

View File

@ -30,6 +30,8 @@
"@hcengineering/ui": "^0.6.3",
"@hcengineering/platform": "^0.6.8",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/view": "^0.6.2",
"@hcengineering/workbench": "^0.6.2",
"@hcengineering/notification": "^0.6.8",
"@hcengineering/setting": "^0.6.3"

View File

@ -36,6 +36,7 @@ import setting from '@hcengineering/setting'
import workbench from '@hcengineering/workbench'
import notification from './plugin'
import { AnyComponent } from '@hcengineering/ui'
import view, { createAction } from '@hcengineering/model-view'
export const DOMAIN_NOTIFICATION = 'notification' as Domain
@ -143,9 +144,12 @@ export class TDocUpdates extends TDoc implements DocUpdates {
@Index(IndexKind.Indexed)
attachedTo!: Ref<Doc>
@Index(IndexKind.Indexed)
hidden!: boolean
attachedToClass!: Ref<Class<Doc>>
lastTx?: Ref<TxCUD<Doc>>
lastTxTime?: Timestamp
lastTx!: Ref<TxCUD<Doc>>
lastTxTime!: Timestamp
txes!: [Ref<TxCUD<Doc>>, Timestamp][]
}
@ -240,6 +244,52 @@ export function createModel (builder: Builder): void {
},
notification.app.Notification
)
createAction(
builder,
{
action: notification.actionImpl.MarkAsUnread,
actionProps: {},
label: notification.string.MarkAsUnread,
icon: notification.icon.Track,
input: 'focus',
visibilityTester: notification.function.HasntNotifications,
category: notification.category.Notification,
target: notification.class.DocUpdates,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
},
notification.action.MarkAsUnread
)
createAction(
builder,
{
action: notification.actionImpl.Hide,
actionProps: {},
label: notification.string.Hide,
icon: notification.icon.Hide,
input: 'focus',
category: notification.category.Notification,
target: notification.class.DocUpdates,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
},
notification.action.Hide
)
createAction(
builder,
{
action: notification.actionImpl.Unsubscribe,
actionProps: {},
label: notification.string.DontTrack,
icon: view.icon.Delete,
input: 'focus',
category: notification.category.Notification,
target: notification.class.DocUpdates,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
},
notification.action.Unsubscribe
)
}
export { notificationOperation } from './migration'

View File

@ -184,11 +184,25 @@ async function fillCollaborators (client: MigrationClient): Promise<void> {
}
}
async function fillDocUpdatesHidder (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_NOTIFICATION,
{
_class: notification.class.DocUpdates,
hidden: { $exists: false }
},
{
hidden: false
}
)
}
export const notificationOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await fillNotificationText(client)
await migrateLastView(client)
await fillCollaborators(client)
await fillDocUpdatesHidder(client)
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
await createSpace(client)

View File

@ -14,11 +14,12 @@
// limitations under the License.
//
import { Ref } from '@hcengineering/core'
import { Application } from '@hcengineering/workbench'
import { Doc, Ref } from '@hcengineering/core'
import notification, { notificationId } from '@hcengineering/notification'
import { IntlString, mergeIds } from '@hcengineering/platform'
import { IntlString, Resource, mergeIds } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui'
import { Action, ActionCategory, ViewAction } from '@hcengineering/view'
import { Application } from '@hcengineering/workbench'
export default mergeIds(notificationId, notification, {
string: {
@ -29,12 +30,30 @@ export default mergeIds(notificationId, notification, {
PlatformNotification: '' as IntlString,
BrowserNotification: '' as IntlString,
EmailNotification: '' as IntlString,
Collaborators: '' as IntlString
Collaborators: '' as IntlString,
Hide: '' as IntlString,
MarkAsUnread: '' as IntlString
},
app: {
Notification: '' as Ref<Application>
},
component: {
NotificationSettings: '' as AnyComponent
},
function: {
HasntNotifications: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>
},
category: {
Notification: '' as Ref<ActionCategory>
},
action: {
Unsubscribe: '' as Ref<Action>,
Hide: '' as Ref<Action>,
MarkAsUnread: '' as Ref<Action>
},
actionImpl: {
Unsubscribe: '' as ViewAction,
Hide: '' as ViewAction,
MarkAsUnread: '' as ViewAction
}
})

View File

@ -10,4 +10,12 @@
<symbol id="donttrack" viewBox="0 0 24 24">
<path d="M21.7,15l-0.2,0.1L21.7,15L21,13.8c-0.6-1.1-1.1-2.3-1.2-3.4l-0.4-2.7C19,3.9,15.8,1,12,1S5,3.9,4.5,7.7l-0.4,2.7 c-0.1,1.2-0.5,2.4-1.2,3.4L2.2,15c-0.7,1.2-1.1,1.8-1,2.6c0.1,0.5,0.4,1,0.7,1.3c0.6,0.5,1.3,0.5,2.7,0.5h3c0.2,1,0.8,1.9,1.5,2.5 C10.1,22.6,11,23,12,23c1,0,2-0.4,2.7-1.1c0.7-0.6,1.2-1.5,1.5-2.5h3c1.4,0,2.1,0,2.7-0.5c0.4-0.4,0.6-0.8,0.7-1.3 C22.9,16.8,22.6,16.2,21.7,15z M14.5,19.4c-0.2,0.5-0.5,0.8-0.8,1.2l0,0c-1,0.8-2.3,0.8-3.2,0c-0.4-0.2-0.6-0.7-0.8-1.2H14.5z M20.9,17.5c-0.1,0.1-1,0.1-1.5,0.1H4.8c-0.6,0-1.4,0-1.5-0.1l-0.1-0.1c0-0.1,0.5-0.8,0.7-1.4l0.7-1.2c0.7-1.3,1.2-2.6,1.4-4.2 l0.4-2.7c0.4-3,2.7-5.1,5.7-5.1s5.4,2.1,5.7,5.1l0.4,2.7c0.1,1.5,0.6,2.9,1.4,4.2l0.7,1.2c0.4,0.6,0.7,1.2,0.7,1.3 C21,17.4,20.9,17.4,20.9,17.5z" />
</symbol>
<symbol id="hide" viewBox="0 0 24 24">
<path
d="M4.5,8.4C6.3,6.9,9,5.2,12,5.2c3,0,5.7,1.6,7.5,3.2c0.9,0.8,1.7,1.6,2.2,2.1c0.3,0.3,0.5,0.5,0.6,0.7c0.1,0.1,0.1,0.2,0.2,0.2c0,0,0,0,0,0.1l0,0l0,0l0,0c0,0,0,0-0.6,0.4c0.6,0.4,0.6,0.4,0.6,0.4l0,0l0,0l0,0c0,0,0,0,0,0.1c0,0-0.1,0.1-0.2,0.2c-0.1,0.2-0.3,0.4-0.6,0.7c-0.5,0.6-1.3,1.4-2.2,2.1c-1.9,1.5-4.5,3.2-7.5,3.2c-3,0-5.7-1.6-7.5-3.2c-0.9-0.8-1.7-1.6-2.2-2.1c-0.3-0.3-0.5-0.5-0.6-0.7c-0.1-0.1-0.1-0.2-0.2-0.2c0,0,0,0,0-0.1l0,0l0,0l0,0c0,0,0,0,0.6-0.4c-0.6-0.4-0.6-0.4-0.6-0.4l0,0l0,0l0,0c0,0,0,0,0-0.1c0,0,0.1-0.1,0.2-0.2c0.1-0.2,0.3-0.4,0.6-0.7C2.8,10,3.5,9.2,4.5,8.4z M2,12l-0.6-0.4c-0.2,0.3-0.2,0.6,0,0.9L2,12z M3,12c0.1,0.1,0.2,0.3,0.4,0.4c0.5,0.5,1.2,1.3,2.1,2c1.8,1.5,4.1,2.8,6.6,2.8c2.5,0,4.8-1.4,6.6-2.8c0.9-0.7,1.6-1.4,2.1-2c0.1-0.2,0.3-0.3,0.4-0.4c-0.1-0.1-0.2-0.3-0.4-0.4c-0.5-0.5-1.2-1.3-2.1-2c-1.8-1.5-4.1-2.8-6.6-2.8c-2.5,0-4.8,1.4-6.6,2.8c-0.9,0.7-1.6,1.4-2.1,2C3.2,11.7,3.1,11.9,3,12z M22,12l0.6,0.4c0.2-0.3,0.2-0.6,0-0.9L22,12z"
/>
<path
d="M8.8,12c0-1.8,1.5-3.2,3.2-3.2s3.2,1.5,3.2,3.2s-1.5,3.2-3.2,3.2S8.8,13.8,8.8,12z M12,10.2c-1,0-1.8,0.8-1.8,1.8s0.8,1.8,1.8,1.8s1.8-0.8,1.8-1.8S13,10.2,12,10.2z"
/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -16,6 +16,8 @@
"RemoveAll": "Delete all notifications",
"MarkAllAsRead": "Mark all notifications as read",
"MarkAsRead": "Mark as read",
"MarkAsUnread": "Mark as unread",
"Hide": "Hide",
"Inbox": "Inbox",
"Collaborators": "Collaborators"
}

View File

@ -16,6 +16,8 @@
"RemoveAll": "Удалить все нотификации",
"MarkAllAsRead": "Отметить все нотификации как прочитанные",
"MarkAsRead": "Отметить нотификация прочитанной",
"MarkAsUnread": "Отметить непрочитанным",
"Hide": "Скрыть",
"Inbox": "Inbox",
"Collaborators": "Участники"
}

View File

@ -20,6 +20,7 @@ const icons = require('../assets/icons.svg') as string // eslint-disable-line
loadMetadata(notification.icon, {
Notifications: `${icons}#notification`,
Track: `${icons}#track`,
Hide: `${icons}#hide`,
DontTrack: `${icons}#donttrack`
})

View File

@ -34,7 +34,8 @@
$: query.query(
notification.class.DocUpdates,
{
user: getCurrentAccount()._id
user: getCurrentAccount()._id,
hidden: false
},
(res) => {
docs = res

View File

@ -21,8 +21,9 @@
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 { AnySvelteComponent, Label, TimeSince, getEventPositionElement, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Menu } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import TxView from './TxView.svelte'
@ -65,6 +66,10 @@
$: docQuery.query(value.attachedToClass, { _id: value.attachedTo }, (res) => ([doc] = res))
$: newTxes = value.txes.length
function showMenu (e: MouseEvent) {
showPopup(Menu, { object: value, baseMenuClass: value._class }, getEventPositionElement(e))
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
@ -72,6 +77,7 @@
<div
class="container cursor-pointer bottom-divider"
class:selected
on:contextmenu|preventDefault={showMenu}
on:click={() => dispatch('click', { _id: value.attachedTo, _class: value.attachedToClass })}
>
<div class="content">

View File

@ -18,7 +18,7 @@ import { Resources } from '@hcengineering/platform'
import Inbox from './components/Inbox.svelte'
import NotificationSettings from './components/NotificationSettings.svelte'
import NotificationPresenter from './components/NotificationPresenter.svelte'
import { NotificationClientImpl } from './utils'
import { NotificationClientImpl, hasntNotifications, hide, markAsUnread, unsubscribe } from './utils'
export * from './utils'
@ -31,6 +31,12 @@ export default async (): Promise<Resources> => ({
NotificationSettings
},
function: {
GetNotificationClient: NotificationClientImpl.getClient
GetNotificationClient: NotificationClientImpl.getClient,
HasntNotifications: hasntNotifications
},
actionImpl: {
Unsubscribe: unsubscribe,
Hide: hide,
MarkAsUnread: markAsUnread
}
})

View File

@ -23,7 +23,6 @@ export default mergeIds(notificationId, notification, {
string: {
NoNotifications: '' as IntlString,
Track: '' as IntlString,
DontTrack: '' as IntlString,
Remove: '' as IntlString,
RemoveAll: '' as IntlString,
MarkAsRead: '' as IntlString,

View File

@ -15,7 +15,7 @@
//
import core, { Account, Class, Doc, getCurrentAccount, Ref, Timestamp } from '@hcengineering/core'
import notification, { LastView, NotificationClient } from '@hcengineering/notification'
import notification, { DocUpdates, LastView, NotificationClient } from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation'
import { get, writable, Writable } from 'svelte/store'
@ -98,3 +98,57 @@ export class NotificationClientImpl implements NotificationClient {
}
}
}
/**
* @public
*/
export async function hasntNotifications (object: DocUpdates): Promise<boolean> {
if (object._class !== notification.class.DocUpdates) return false
return object.txes.length === 0
}
/**
* @public
*/
export async function unsubscribe (object: DocUpdates): Promise<void> {
const me = getCurrentAccount()._id
const client = getClient()
const hierarchy = client.getHierarchy()
const target = await client.findOne(object.attachedToClass, { _id: object.attachedTo })
if (target !== undefined) {
if (hierarchy.hasMixin(target, notification.mixin.Collaborators)) {
const collab = hierarchy.as(target, notification.mixin.Collaborators)
if (collab.collaborators.includes(me)) {
await client.updateMixin(collab._id, collab._class, collab.space, notification.mixin.Collaborators, {
$pull: {
collaborators: me
}
})
}
}
}
await client.remove(object)
}
/**
* @public
*/
export async function hide (object: DocUpdates): Promise<void> {
const client = getClient()
await client.update(object, {
hidden: true
})
}
/**
* @public
*/
export async function markAsUnread (object: DocUpdates): Promise<void> {
const client = getClient()
if (object.txes.length > 0) return
if (object.lastTx !== undefined && object.lastTxTime !== undefined) {
await client.update(object, {
txes: [[object.lastTx, object.lastTxTime]]
})
}
}

View File

@ -136,8 +136,9 @@ export interface DocUpdates extends Doc {
user: Ref<Account>
attachedTo: Ref<Doc>
attachedToClass: Ref<Class<Doc>>
lastTx?: Ref<TxCUD<Doc>>
lastTxTime?: Timestamp
hidden: boolean
lastTx: Ref<TxCUD<Doc>>
lastTxTime: Timestamp
txes: [Ref<TxCUD<Doc>>, Timestamp][]
}
@ -199,7 +200,8 @@ const notification = plugin(notificationId, {
icon: {
Notifications: '' as Asset,
Track: '' as Asset,
DontTrack: '' as Asset
DontTrack: '' as Asset,
Hide: '' as Asset
},
space: {
Notifications: '' as Ref<Space>
@ -207,6 +209,7 @@ const notification = plugin(notificationId, {
string: {
Notification: '' as IntlString,
Notifications: '' as IntlString,
DontTrack: '' as IntlString,
Inbox: '' as IntlString
},
function: {

View File

@ -16,7 +16,7 @@
"Open": "Открыть",
"Assignees": "Исполнители",
"Labels": "Метки",
"ActionPlaceholder": "введите для филтрации...",
"ActionPlaceholder": "введите для фильтрации...",
"General": "Общее",
"Navigation": "Навигация",
"Editor": "Редактор",

View File

@ -52,7 +52,6 @@
}
)
let visibleActions: WithLookup<Action>[] = []
let supportedActions: WithLookup<Action>[] = []
let filteredActions: WithLookup<Action>[] = []
@ -70,16 +69,15 @@
}
}
}
visibleActions = resultActions
return resultActions
}
$: filterVisibleActions(actions, getSelection($focusStore, $selectionStore))
const client = getClient()
$: {
let fActions: WithLookup<Action>[] = visibleActions
async function getSupportedActions (actions: WithLookup<Action>[]) {
const docs = getSelection($focusStore, $selectionStore)
let fActions: WithLookup<Action>[] = actions
for (const d of docs) {
fActions = filterActions(client, d, fActions)
}
@ -91,10 +89,13 @@
(it.$lookup?.category?.visible ?? true) &&
(it.context.application === viewContext.application || it.context.application === undefined)
)
fActions = await filterVisibleActions(fActions, docs)
// Sort by category.
supportedActions = fActions.sort((a, b) => a.category.localeCompare(b.category))
}
$: getSupportedActions(actions)
async function filterSearchActions (actions: WithLookup<Action>[], search: string): Promise<void> {
const res: WithLookup<Action>[] = []
search = search.trim().toLowerCase()

View File

@ -514,7 +514,8 @@ async function createCollabDocInfo (
res.push(
control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, {
lastTx: txId ?? tx._id,
lastTxTime: tx.modifiedOn
lastTxTime: tx.modifiedOn,
hidden: false
})
)
}
@ -525,6 +526,7 @@ async function createCollabDocInfo (
user: target,
attachedTo: objectId,
attachedToClass: objectClass,
hidden: false,
lastTx: txId ?? tx._id,
lastTxTime: tx.modifiedOn,
txes: [[txId ?? tx._id, tx.modifiedOn]]