UBERF-4514: option for order of activity, pinned first in CommentPopup (#4122)

Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>
This commit is contained in:
Vyacheslav Tumanov 2023-12-01 22:52:31 +05:00 committed by GitHub
parent 84d23c2e86
commit 74e50f0b87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 69 additions and 18 deletions

View File

@ -658,6 +658,11 @@ export function createModel (builder: Builder, options = { addApplication: true
filter: chunter.filter.CommentsFilter filter: chunter.filter.CommentsFilter
}) })
builder.createDoc(activity.class.ActivityFilter, core.space.Model, {
label: chunter.string.FilterPinnedComments,
filter: chunter.filter.PinnedCommentsFilter
})
builder.createDoc(activity.class.ActivityFilter, core.space.Model, { builder.createDoc(activity.class.ActivityFilter, core.space.Model, {
label: chunter.string.FilterBacklinks, label: chunter.string.FilterBacklinks,
filter: chunter.filter.BacklinksFilter filter: chunter.filter.BacklinksFilter

View File

@ -77,6 +77,7 @@ export default mergeIds(chunterId, chunter, {
Reactions: '' as IntlString, Reactions: '' as IntlString,
Emoji: '' as IntlString, Emoji: '' as IntlString,
FilterComments: '' as IntlString, FilterComments: '' as IntlString,
FilterPinnedComments: '' as IntlString,
FilterBacklinks: '' as IntlString, FilterBacklinks: '' as IntlString,
DM: '' as IntlString, DM: '' as IntlString,
DMNotification: '' as IntlString, DMNotification: '' as IntlString,
@ -114,6 +115,7 @@ export default mergeIds(chunterId, chunter, {
}, },
filter: { filter: {
CommentsFilter: '' as Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>, CommentsFilter: '' as Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>,
PinnedCommentsFilter: '' as Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>,
BacklinksFilter: '' as Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean> BacklinksFilter: '' as Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>
} }
}) })

View File

@ -15,10 +15,12 @@
<script lang="ts"> <script lang="ts">
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import Label from './Label.svelte' import Label from './Label.svelte'
import { createEventDispatcher } from 'svelte'
export let label: IntlString | undefined = undefined export let label: IntlString | undefined = undefined
export let on: boolean = false export let on: boolean = false
export let disabled: boolean = false export let disabled: boolean = false
const dispatch = createEventDispatcher()
</script> </script>
<div class="flex-row-center"> <div class="flex-row-center">
@ -34,6 +36,7 @@
on:click={() => { on:click={() => {
if (!disabled) { if (!disabled) {
on = !on on = !on
dispatch('change', on)
} }
}} }}
> >

View File

@ -15,6 +15,7 @@
"Removed": "removed", "Removed": "removed",
"From": "from", "From": "from",
"All": "All", "All": "All",
"Attributes": "Attributes" "Attributes": "Attributes",
"NewestFirst": "Newest first"
} }
} }

View File

@ -15,6 +15,7 @@
"Removed": "удалил(а)", "Removed": "удалил(а)",
"From": "из", "From": "из",
"All": "Все", "All": "Все",
"Attributes": "Атрибуты" "Attributes": "Атрибуты",
"NewestFirst": "Сначала новые"
} }
} }

View File

@ -75,6 +75,7 @@ class ActivityImpl implements Activity {
private readonly hiddenAttributes: Set<string> private readonly hiddenAttributes: Set<string>
private prevObjectId: Ref<Doc> | undefined private prevObjectId: Ref<Doc> | undefined
private prevObjectClass: Ref<Class<Doc>> | undefined private prevObjectClass: Ref<Class<Doc>> | undefined
private prevSort: SortingOrder | undefined
private editable: Map<Ref<Class<Doc>>, boolean> | undefined private editable: Map<Ref<Class<Doc>>, boolean> | undefined
private ownTxes: Array<TxCUD<Doc>> = [] private ownTxes: Array<TxCUD<Doc>> = []
@ -116,11 +117,12 @@ class ActivityImpl implements Activity {
editable: Map<Ref<Class<Doc>>, boolean> editable: Map<Ref<Class<Doc>>, boolean>
): boolean { ): boolean {
this.editable = editable this.editable = editable
if (objectId === this.prevObjectId && objectClass === this.prevObjectClass) { if (objectId === this.prevObjectId && objectClass === this.prevObjectClass && sort === this.prevSort) {
return false return false
} }
this.prevObjectClass = objectClass this.prevObjectClass = objectClass
this.prevObjectId = objectId this.prevObjectId = objectId
this.prevSort = sort
let isAttached = false let isAttached = false
isAttached = this.hierarchy.isDerived(objectClass, core.class.AttachedDoc) isAttached = this.hierarchy.isDerived(objectClass, core.class.AttachedDoc)

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import activity, { ActivityExtension, DisplayTx, TxViewlet } from '@hcengineering/activity' import activity, { ActivityExtension, DisplayTx, TxViewlet } from '@hcengineering/activity'
import core, { Class, Doc, Ref, SortingOrder } from '@hcengineering/core' import core, { Class, Doc, Ref, SortingOrder } from '@hcengineering/core'
import notification, { DocUpdateTx, DocUpdates, Writable } from '@hcengineering/notification' import notification, { DocUpdates, DocUpdateTx, Writable } from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { Grid, Label, Lazy, Spinner } from '@hcengineering/ui' import { Grid, Label, Lazy, Spinner } from '@hcengineering/ui'
@ -84,11 +84,12 @@
} }
let loading = false let loading = false
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
function updateTxes ( function updateTxes (
objectId: Ref<Doc>, objectId: Ref<Doc>,
objectClass: Ref<Class<Doc>>, objectClass: Ref<Class<Doc>>,
editableMap: Map<Ref<Class<Doc>>, boolean> | undefined editableMap: Map<Ref<Class<Doc>>, boolean> | undefined,
activityOrder: boolean
): void { ): void {
loading = true loading = true
const res = activityQuery.update( const res = activityQuery.update(
@ -103,7 +104,7 @@
} }
} }
}, },
SortingOrder.Ascending, activityOrder ? SortingOrder.Descending : SortingOrder.Ascending,
editableMap ?? new Map() editableMap ?? new Map()
) )
if (!res) { if (!res) {
@ -111,7 +112,7 @@
} }
} }
$: updateTxes(object._id, object._class, editableMap) $: updateTxes(object._id, object._class, editableMap, activityOrderNewestFirst)
let filtered: DisplayTx[] = [] let filtered: DisplayTx[] = []
@ -152,6 +153,7 @@
on:update={(e) => { on:update={(e) => {
filtered = e.detail filtered = e.detail
}} }}
bind:activityOrderNewestFirst
/> />
</div> </div>
<div class="p-activity select-text" id={activity.string.Activity}> <div class="p-activity select-text" id={activity.string.Activity}>

View File

@ -31,10 +31,12 @@
const client = getClient() const client = getClient()
let filters: ActivityFilter[] = [] let filters: ActivityFilter[] = []
const saved = localStorage.getItem('activity-filter') const saved = localStorage.getItem('activity-filter')
export let activityOrderNewestFirst = false
let selectedFiltersRefs: Ref<Doc>[] | 'All' = let selectedFiltersRefs: Ref<Doc>[] | 'All' =
saved !== null && saved !== undefined ? (JSON.parse(saved) as Ref<Doc>[] | 'All') : 'All' saved !== null && saved !== undefined ? (JSON.parse(saved) as Ref<Doc>[] | 'All') : 'All'
let selectedFilters: ActivityFilter[] = [] let selectedFilters: ActivityFilter[] = []
$: localStorage.setItem('activity-filter', JSON.stringify(selectedFiltersRefs)) $: localStorage.setItem('activity-filter', JSON.stringify(selectedFiltersRefs))
$: localStorage.setItem('activity-newest-first', JSON.stringify(activityOrderNewestFirst))
client.findAll(activity.class.ActivityFilter, {}).then((res) => { client.findAll(activity.class.ActivityFilter, {}).then((res) => {
filters = res filters = res
if (saved !== null && saved !== undefined) { if (saved !== null && saved !== undefined) {
@ -71,6 +73,10 @@
() => {}, () => {},
(res) => { (res) => {
if (res === undefined) return if (res === undefined) return
if (res.action === 'toggle') {
activityOrderNewestFirst = res.value
return
}
const selected = res.value as Ref<Doc>[] const selected = res.value as Ref<Doc>[]
const isAll = selected.length === filters.length || selected.length === 0 const isAll = selected.length === filters.length || selected.length === 0
if (res.action === 'select') selectedFiltersRefs = isAll ? 'All' : selected if (res.action === 'select') selectedFiltersRefs = isAll ? 'All' : selected
@ -104,6 +110,7 @@
$: updateFilterActions(txes, filters, selectedFiltersRefs) $: updateFilterActions(txes, filters, selectedFiltersRefs)
</script> </script>
<div class="w-4 min-w-4 max-w-4" />
{#if selectedFiltersRefs === 'All'} {#if selectedFiltersRefs === 'All'}
<div class="antiSection-header__tag highlight"> <div class="antiSection-header__tag highlight">
<Label label={activityPlg.string.All} /> <Label label={activityPlg.string.All} />

View File

@ -15,14 +15,14 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { Label, resizeObserver, CheckBox } from '@hcengineering/ui' import { Label, resizeObserver, CheckBox, MiniToggle } from '@hcengineering/ui'
import { Doc, Ref } from '@hcengineering/core' import { Doc, Ref } from '@hcengineering/core'
import { ActivityFilter } from '@hcengineering/activity' import { ActivityFilter } from '@hcengineering/activity'
import activity from '../plugin' import activity from '../plugin'
export let selectedFiltersRefs: Ref<Doc>[] | 'All' = 'All' export let selectedFiltersRefs: Ref<Doc>[] | 'All' = 'All'
export let filters: ActivityFilter[] = [] export let filters: ActivityFilter[] = []
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
interface ActionMenu { interface ActionMenu {
@ -126,6 +126,15 @@
<div class="ap-space" /> <div class="ap-space" />
<div class="ap-scroll"> <div class="ap-scroll">
<div class="ap-box" bind:this={popup}> <div class="ap-box" bind:this={popup}>
<div class="ml-3 mt-2 mb-2 mr-3">
<MiniToggle
bind:on={activityOrderNewestFirst}
label={activity.string.NewestFirst}
on:change={() => {
dispatch('update', { action: 'toggle', value: activityOrderNewestFirst })
}}
/>
</div>
<!-- svelte-ignore a11y-mouse-events-have-key-events --> <!-- svelte-ignore a11y-mouse-events-have-key-events -->
{#each menu as item, i} {#each menu as item, i}
<button <button

View File

@ -149,7 +149,8 @@ export default plugin(activityId, {
Unset: '' as IntlString, Unset: '' as IntlString,
Added: '' as IntlString, Added: '' as IntlString,
From: '' as IntlString, From: '' as IntlString,
Removed: '' as IntlString Removed: '' as IntlString,
NewestFirst: '' as IntlString
}, },
mixin: { mixin: {
ExtraActivityComponent: '' as Ref<Class<ExtraActivityComponent>> ExtraActivityComponent: '' as Ref<Class<ExtraActivityComponent>>

View File

@ -68,6 +68,7 @@
"NoResults": "No results", "NoResults": "No results",
"CopyLink": "Copy link", "CopyLink": "Copy link",
"FilterComments": "Comments", "FilterComments": "Comments",
"FilterPinnedComments": "Pinned comments",
"FilterBacklinks": "Backlinks", "FilterBacklinks": "Backlinks",
"DM": "Direct message", "DM": "Direct message",
"DMNotification": "Sent you a message", "DMNotification": "Sent you a message",

View File

@ -68,6 +68,7 @@
"NoResults": "Нет результатов", "NoResults": "Нет результатов",
"CopyLink": "Копировать ссылку", "CopyLink": "Копировать ссылку",
"FilterComments": "Комментарии", "FilterComments": "Комментарии",
"FilterPinnedComments": "Закрепленные комментарии",
"FilterBacklinks": "Упоминания", "FilterBacklinks": "Упоминания",
"DM": "Личное сообщение", "DM": "Личное сообщение",
"DMNotification": "Отправил сообщение", "DMNotification": "Отправил сообщение",

View File

@ -18,28 +18,29 @@
import chunter, { Comment } from '@hcengineering/chunter' import chunter, { Comment } from '@hcengineering/chunter'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import { Label, resizeObserver, Spinner, closeTooltip, Lazy } from '@hcengineering/ui' import { Label, resizeObserver, Spinner, closeTooltip, Lazy, MiniToggle } from '@hcengineering/ui'
import { DocNavLink, ObjectPresenter } from '@hcengineering/view-resources' import { DocNavLink, ObjectPresenter } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import CommentInput from './CommentInput.svelte' import CommentInput from './CommentInput.svelte'
import CommentPresenter from './CommentPresenter.svelte' import CommentPresenter from './CommentPresenter.svelte'
import activity from '@hcengineering/activity'
export let objectId: Ref<Doc> export let objectId: Ref<Doc>
export let object: Doc export let object: Doc
export let withInput: boolean = true export let withInput: boolean = true
let loading = true let loading = true
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
let comments: Comment[] = [] let comments: Comment[] = []
const query = createQuery() const query = createQuery()
$: query.query( $: query.query(
chunter.class.Comment, chunter.class.Comment,
{ attachedTo: objectId }, { attachedTo: objectId },
(res) => { (res) => {
comments = res comments = res.sort((c) => (c?.pinned ? -1 : 1))
loading = false loading = false
}, },
{ sort: { modifiedOn: SortingOrder.Ascending } } { sort: { modifiedOn: activityOrderNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending } }
) )
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let commentMode = false let commentMode = false
@ -68,6 +69,7 @@
<div class="fs-title mr-2"> <div class="fs-title mr-2">
<Label label={chunter.string.Comments} /> <Label label={chunter.string.Comments} />
</div> </div>
<MiniToggle bind:on={activityOrderNewestFirst} label={activity.string.NewestFirst} />
<DocNavLink {object}> <DocNavLink {object}>
<ObjectPresenter _class={object._class} objectId={object._id} value={object} /> <ObjectPresenter _class={object._class} objectId={object._id} value={object} />
</DocNavLink> </DocNavLink>

View File

@ -20,8 +20,9 @@
import { Avatar, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources' import { Avatar, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { IdMap, Ref } from '@hcengineering/core' import { IdMap, Ref } from '@hcengineering/core'
import { MessageViewer, getClient } from '@hcengineering/presentation' import { MessageViewer, getClient } from '@hcengineering/presentation'
import { ShowMore, TimeSince } from '@hcengineering/ui' import { Icon, ShowMore, TimeSince } from '@hcengineering/ui'
import { LinkPresenter, ObjectPresenter } from '@hcengineering/view-resources' import { LinkPresenter, ObjectPresenter } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
export let value: Comment export let value: Comment
export let inline: boolean = false export let inline: boolean = false
@ -81,7 +82,14 @@
<div class="fs-title"> <div class="fs-title">
{#if employee}{getName(client.getHierarchy(), employee)}{/if} {#if employee}{getName(client.getHierarchy(), employee)}{/if}
</div> </div>
<div class="content-dark-color ml-4"><TimeSince value={value.modifiedOn} /></div> <div class="content-dark-color ml-4 flex-row-center">
{#if value.pinned}
<Icon icon={view.icon.Pin} size={'small'} fill="#3265cb" />
{/if}
<div class="ml-1">
<TimeSince value={value.modifiedOn} />
</div>
</div>
</div> </div>
<ShowMore fixed> <ShowMore fixed>
<MessageViewer message={value.message} /> <MessageViewer message={value.message} />

View File

@ -21,7 +21,8 @@ import chunter, {
type ChunterSpace, type ChunterSpace,
type DirectMessage, type DirectMessage,
type Message, type Message,
type ThreadMessage type ThreadMessage,
type Comment
} from '@hcengineering/chunter' } from '@hcengineering/chunter'
import core, { import core, {
type Data, type Data,
@ -274,6 +275,10 @@ export function commentsFilter (tx: DisplayTx, _class?: Ref<Doc>): boolean {
return tx.tx.objectClass === chunter.class.Comment return tx.tx.objectClass === chunter.class.Comment
} }
export function pinnedCommentsFilter (tx: DisplayTx, _class?: Ref<Doc>): boolean {
return tx.tx.objectClass === chunter.class.Comment && (tx.doc as Comment)?.pinned === true
}
export function backlinksFilter (tx: DisplayTx, _class?: Ref<Doc>): boolean { export function backlinksFilter (tx: DisplayTx, _class?: Ref<Doc>): boolean {
return tx.tx.objectClass === chunter.class.Backlink return tx.tx.objectClass === chunter.class.Backlink
} }
@ -281,6 +286,7 @@ export function backlinksFilter (tx: DisplayTx, _class?: Ref<Doc>): boolean {
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
filter: { filter: {
CommentsFilter: commentsFilter, CommentsFilter: commentsFilter,
PinnedCommentsFilter: pinnedCommentsFilter,
BacklinksFilter: backlinksFilter BacklinksFilter: backlinksFilter
}, },
component: { component: {