mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-29 21:15:06 +02:00
New Activity - squashed (#4032)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> Co-authored-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
co-authored by
Andrey Sobolev
parent
119d65653a
commit
ea3eb4af83
@@ -13,161 +13,83 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import activity, { ActivityExtension, DisplayTx, TxViewlet } from '@hcengineering/activity'
|
||||
import core, { Class, Doc, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import notification, { DocUpdates, DocUpdateTx, Writable } from '@hcengineering/notification'
|
||||
import activity from '@hcengineering/activity'
|
||||
import { Doc, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import notification, { DisplayActivityMessage, ActivityMessage } from '@hcengineering/notification'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Grid, Label, Lazy, Spinner } from '@hcengineering/ui'
|
||||
import { ActivityKey, activityKey, newActivity } from '../activity'
|
||||
import { filterCollectionTxes, getExtensions } from '../utils'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Component, Grid, Label, Lazy, Spinner } from '@hcengineering/ui'
|
||||
|
||||
import ActivityFilter from './ActivityFilter.svelte'
|
||||
import TxView from './TxView.svelte'
|
||||
import ActivityExtensionComponent from './ActivityExtensionComponent.svelte'
|
||||
|
||||
export let object: Doc
|
||||
export let showCommenInput: boolean = true
|
||||
export let transparent: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let focusIndex: number = -1
|
||||
export let boundary: HTMLElement | undefined = undefined
|
||||
|
||||
let extensions: ActivityExtension[] = []
|
||||
const activityMessagesQuery = createQuery()
|
||||
|
||||
$: if (object) {
|
||||
getExtensions(client, object._class).then((res?: ActivityExtension[]) => {
|
||||
extensions = res || []
|
||||
})
|
||||
}
|
||||
let filteredMessages: DisplayActivityMessage[] = []
|
||||
let activityMessages: ActivityMessage[] = []
|
||||
let isLoading = false
|
||||
|
||||
getResource(notification.function.GetNotificationClient).then((res) => {
|
||||
updatesStore = res().docUpdatesStore
|
||||
})
|
||||
let updatesStore: Writable<Map<Ref<Doc>, DocUpdates>> | undefined
|
||||
let isNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
|
||||
|
||||
$: updates = $updatesStore?.get(object._id)
|
||||
$: newTxes = updates?.txes ?? []
|
||||
async function updateActivityMessages (objectId: Ref<Doc>, order: SortingOrder): Promise<void> {
|
||||
isLoading = true
|
||||
const combineMessagesFn = await getResource(notification.function.CombineActivityMessages)
|
||||
|
||||
let txes: DisplayTx[] = []
|
||||
|
||||
const client = getClient()
|
||||
const attrs = client.getHierarchy().getAllAttributes(object._class)
|
||||
|
||||
const activityQuery = newActivity(client, attrs)
|
||||
|
||||
let viewlets = new Map<ActivityKey, TxViewlet[]>()
|
||||
|
||||
let allViewlets: TxViewlet[] = []
|
||||
let editableMap: Map<Ref<Class<Doc>>, boolean> | undefined = undefined
|
||||
|
||||
const descriptors = createQuery()
|
||||
$: descriptors.query(activity.class.TxViewlet, {}, (result) => {
|
||||
allViewlets = result
|
||||
editableMap = new Map(
|
||||
allViewlets
|
||||
.filter((tx) => tx.txClass === core.class.TxCreateDoc)
|
||||
.map((it) => [it.objectClass, it.editable ?? false])
|
||||
)
|
||||
})
|
||||
|
||||
$: viewlets = buildViewletsMap(allViewlets)
|
||||
|
||||
function buildViewletsMap (allViewlets: TxViewlet[]): Map<ActivityKey, TxViewlet[]> {
|
||||
const viewlets = new Map()
|
||||
for (const res of allViewlets) {
|
||||
const key = activityKey(res.objectClass, res.txClass)
|
||||
const arr = viewlets.get(key) ?? []
|
||||
arr.push(res)
|
||||
viewlets.set(key, arr)
|
||||
}
|
||||
return viewlets
|
||||
}
|
||||
|
||||
let loading = false
|
||||
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
|
||||
function updateTxes (
|
||||
objectId: Ref<Doc>,
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
editableMap: Map<Ref<Class<Doc>>, boolean> | undefined,
|
||||
activityOrder: boolean
|
||||
): void {
|
||||
loading = true
|
||||
const res = activityQuery.update(
|
||||
objectId,
|
||||
objectClass,
|
||||
(_id, result) => {
|
||||
if (_id === objectId) {
|
||||
txes = filterCollectionTxes(result)
|
||||
|
||||
if (txes.length > 0) {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
const res = activityMessagesQuery.query(
|
||||
notification.class.ActivityMessage,
|
||||
{ attachedTo: objectId },
|
||||
(result: ActivityMessage[]) => {
|
||||
activityMessages = combineMessagesFn(result, order)
|
||||
isLoading = false
|
||||
},
|
||||
activityOrder ? SortingOrder.Descending : SortingOrder.Ascending,
|
||||
editableMap ?? new Map()
|
||||
{
|
||||
sort: {
|
||||
createdOn: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
if (!res) {
|
||||
loading = false
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
$: updateTxes(object._id, object._class, editableMap, activityOrderNewestFirst)
|
||||
|
||||
let filtered: DisplayTx[] = []
|
||||
|
||||
let newTxIndexes: number[] = []
|
||||
$: newTxIndexes = getNewTxes(filtered, newTxes)
|
||||
|
||||
function getNewTxes (filtered: DisplayTx[], newTxes: DocUpdateTx[]): number[] {
|
||||
const res: number[] = []
|
||||
for (let i = 0; i < filtered.length; i++) {
|
||||
if (isNew(filtered[i], newTxes)) {
|
||||
res.push(i)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
function isNew (tx: DisplayTx | undefined, newTxes: DocUpdateTx[]): boolean {
|
||||
if (tx === undefined) return false
|
||||
const index = newTxes.findIndex((p) => p._id === tx.originTx._id && p.isNew)
|
||||
return index !== -1
|
||||
}
|
||||
|
||||
$: scrollIndex = shouldScroll ? newTxIndexes[0] ?? filtered.length - 1 : -1
|
||||
$: updateActivityMessages(object._id, isNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending)
|
||||
</script>
|
||||
|
||||
<div class="antiSection-header high mt-9" class:invisible={transparent}>
|
||||
<span class="antiSection-header__title flex-row-center">
|
||||
<Label label={activity.string.Activity} />
|
||||
{#if loading}
|
||||
{#if isLoading}
|
||||
<div class="ml-1">
|
||||
<Spinner size={'small'} />
|
||||
<Spinner size="small" />
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
<ActivityFilter
|
||||
{txes}
|
||||
messages={activityMessages}
|
||||
{object}
|
||||
on:update={(e) => {
|
||||
filtered = e.detail
|
||||
filteredMessages = e.detail
|
||||
}}
|
||||
bind:activityOrderNewestFirst
|
||||
bind:isNewestFirst
|
||||
/>
|
||||
</div>
|
||||
<div class="p-activity select-text" id={activity.string.Activity}>
|
||||
{#if filtered}
|
||||
<Grid column={1} rowGap={0.75}>
|
||||
{#each filtered as tx, i}
|
||||
{#if filteredMessages.length}
|
||||
<Grid column={1} rowGap={0}>
|
||||
{#each filteredMessages as message}
|
||||
<Lazy>
|
||||
<TxView
|
||||
{tx}
|
||||
{viewlets}
|
||||
isNew={newTxIndexes.includes(i)}
|
||||
isNextNew={newTxIndexes.includes(i + 1)}
|
||||
shouldScroll={i === scrollIndex}
|
||||
{boundary}
|
||||
<Component
|
||||
is={notification.component.ActivityMessagePresenter}
|
||||
props={{
|
||||
value: message,
|
||||
boundary
|
||||
}}
|
||||
/>
|
||||
</Lazy>
|
||||
{/each}
|
||||
@@ -176,7 +98,7 @@
|
||||
</div>
|
||||
{#if showCommenInput}
|
||||
<div class="ref-input">
|
||||
<ActivityExtensionComponent {extensions} kind="input" props={{ object, focusIndex, boundary }} />
|
||||
<Component is={notification.component.ChatMessageInput} props={{ object, boundary, focusIndex }} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -186,14 +108,18 @@
|
||||
margin-top: 1.75rem;
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.p-activity {
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(.grid .msgactivity-container.showIcon:last-child::after) {
|
||||
content: none;
|
||||
} // Remove the line in the last Activity message
|
||||
}
|
||||
|
||||
// Remove the line in the last Activity message
|
||||
</style>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<!--
|
||||
// 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 { Component } from '@hcengineering/ui'
|
||||
import { ActivityExtension, ActivityExtensionKind } from '@hcengineering/activity'
|
||||
|
||||
export let kind: ActivityExtensionKind
|
||||
export let extensions: ActivityExtension[] = []
|
||||
export let props: Record<string, any> = {}
|
||||
</script>
|
||||
|
||||
{#each extensions as extension}
|
||||
{@const component = extension.components?.[kind]}
|
||||
{#if component}
|
||||
<Component is={component} {props} />
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -13,32 +13,36 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import activity, { ActivityFilter, DisplayTx } from '@hcengineering/activity'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { getClient, hasResource } from '@hcengineering/presentation'
|
||||
import { ActionIcon, AnyComponent, Icon, Label, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import activityPlg from '../plugin'
|
||||
import { Doc, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { ActionIcon, eventToHTMLElement, Icon, Label, showPopup } from '@hcengineering/ui'
|
||||
import notification, { ActivityMessage, ActivityMessagesFilter } from '@hcengineering/notification'
|
||||
|
||||
import activity from '../plugin'
|
||||
import FilterPopup from './FilterPopup.svelte'
|
||||
import IconClose from './icons/Close.svelte'
|
||||
import IconFilter from './icons/Filter.svelte'
|
||||
|
||||
export let txes: DisplayTx[]
|
||||
export let messages: ActivityMessage[]
|
||||
export let object: Doc
|
||||
export let isNewestFirst = false
|
||||
|
||||
let filtered: DisplayTx[]
|
||||
let filtered: ActivityMessage[]
|
||||
const client = getClient()
|
||||
let filters: ActivityFilter[] = []
|
||||
let filters: ActivityMessagesFilter[] = []
|
||||
const saved = localStorage.getItem('activity-filter')
|
||||
export let activityOrderNewestFirst = false
|
||||
let selectedFiltersRefs: Ref<Doc>[] | 'All' =
|
||||
saved !== null && saved !== undefined ? (JSON.parse(saved) as Ref<Doc>[] | 'All') : 'All'
|
||||
let selectedFilters: ActivityFilter[] = []
|
||||
let selectedFilters: ActivityMessagesFilter[] = []
|
||||
|
||||
$: localStorage.setItem('activity-filter', JSON.stringify(selectedFiltersRefs))
|
||||
$: localStorage.setItem('activity-newest-first', JSON.stringify(activityOrderNewestFirst))
|
||||
client.findAll(activity.class.ActivityFilter, {}).then((res) => {
|
||||
$: localStorage.setItem('activity-newest-first', JSON.stringify(isNewestFirst))
|
||||
|
||||
client.findAll(notification.class.ActivityMessagesFilter, {}).then((res) => {
|
||||
filters = res
|
||||
|
||||
if (saved !== null && saved !== undefined) {
|
||||
const temp: Ref<Doc>[] | 'All' = JSON.parse(saved)
|
||||
if (temp !== 'All' && Array.isArray(temp)) {
|
||||
@@ -52,20 +56,7 @@
|
||||
}
|
||||
})
|
||||
|
||||
function getAdditionalComponent (_class: Ref<Class<Doc>>): AnyComponent | undefined {
|
||||
const hierarchy = client.getHierarchy()
|
||||
const mixin = hierarchy.classHierarchyMixin(_class, activity.mixin.ExtraActivityComponent, (m) =>
|
||||
hasResource(m.component)
|
||||
)
|
||||
if (mixin !== undefined) {
|
||||
return mixin.component
|
||||
}
|
||||
}
|
||||
|
||||
let extraComponent = getAdditionalComponent(object._class)
|
||||
$: extraComponent = getAdditionalComponent(object._class)
|
||||
|
||||
const handleOptions = (ev: MouseEvent) => {
|
||||
const handleOptions = (ev: MouseEvent): void => {
|
||||
showPopup(
|
||||
FilterPopup,
|
||||
{ selectedFiltersRefs, filters },
|
||||
@@ -74,7 +65,7 @@
|
||||
(res) => {
|
||||
if (res === undefined) return
|
||||
if (res.action === 'toggle') {
|
||||
activityOrderNewestFirst = res.value
|
||||
isNewestFirst = res.value
|
||||
return
|
||||
}
|
||||
const selected = res.value as Ref<Doc>[]
|
||||
@@ -87,33 +78,45 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function updateFilterActions (
|
||||
txes: DisplayTx[],
|
||||
filters: ActivityFilter[],
|
||||
selected: Ref<Doc>[] | 'All'
|
||||
messages: ActivityMessage[],
|
||||
filters: ActivityMessagesFilter[],
|
||||
selected: Ref<Doc>[] | 'All',
|
||||
sortOrder: SortingOrder
|
||||
): Promise<void> {
|
||||
const txesSorted = txes.sort((tx) => ((tx.doc as any)?.pinned ? -1 : 1))
|
||||
const sortMessagesFn = await getResource(notification.function.SortActivityMessages)
|
||||
|
||||
const sortedMessages = sortMessagesFn(messages, sortOrder).sort(({ isPinned }) =>
|
||||
isPinned && sortOrder === SortingOrder.Ascending ? -1 : 1
|
||||
)
|
||||
|
||||
if (selected === 'All') {
|
||||
filtered = txesSorted
|
||||
filtered = sortedMessages
|
||||
|
||||
dispatch('update', filtered)
|
||||
} else {
|
||||
selectedFilters = filters.filter((filter) => selected.includes(filter._id))
|
||||
const filterActions: ((tx: DisplayTx, _class?: Ref<Doc>) => boolean)[] = []
|
||||
const filterActions: ((message: ActivityMessage, _class?: Ref<Doc>) => boolean)[] = []
|
||||
for (const filter of selectedFilters) {
|
||||
const fltr = await getResource(filter.filter)
|
||||
filterActions.push(fltr)
|
||||
}
|
||||
filtered = txesSorted.filter((it) => filterActions.some((f) => f(it, object._class)))
|
||||
filtered = messages.filter((message) => filterActions.some((f) => f(message, object._class)))
|
||||
dispatch('update', filtered)
|
||||
}
|
||||
}
|
||||
|
||||
$: updateFilterActions(txes, filters, selectedFiltersRefs)
|
||||
$: updateFilterActions(
|
||||
messages,
|
||||
filters,
|
||||
selectedFiltersRefs,
|
||||
isNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="w-4 min-w-4 max-w-4" />
|
||||
{#if selectedFiltersRefs === 'All'}
|
||||
<div class="antiSection-header__tag highlight">
|
||||
<Label label={activityPlg.string.All} />
|
||||
<Label label={activity.string.All} />
|
||||
</div>
|
||||
{:else}
|
||||
{#each selectedFilters as filter}
|
||||
@@ -137,10 +140,3 @@
|
||||
{/if}
|
||||
<div class="w-4 min-w-4 max-w-4" />
|
||||
<ActionIcon icon={IconFilter} size={'medium'} action={handleOptions} />
|
||||
{#if extraComponent}
|
||||
{#await getResource(extraComponent) then comp}
|
||||
{#if comp}
|
||||
<svelte:component this={comp} {filtered} {object} on:update />
|
||||
{/if}
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
@@ -17,14 +17,17 @@
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Label, resizeObserver, CheckBox, MiniToggle } from '@hcengineering/ui'
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import { ActivityFilter } from '@hcengineering/activity'
|
||||
import notification, { ActivityMessagesFilter } from '@hcengineering/notification'
|
||||
|
||||
import activity from '../plugin'
|
||||
|
||||
export let selectedFiltersRefs: Ref<Doc>[] | 'All' = 'All'
|
||||
export let filters: ActivityFilter[] = []
|
||||
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
|
||||
export let filters: ActivityMessagesFilter[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
|
||||
|
||||
interface ActionMenu {
|
||||
label: IntlString
|
||||
checked: boolean
|
||||
@@ -129,7 +132,7 @@
|
||||
<div class="ml-3 mt-2 mb-2 mr-3">
|
||||
<MiniToggle
|
||||
bind:on={activityOrderNewestFirst}
|
||||
label={activity.string.NewestFirst}
|
||||
label={notification.string.NewestFirst}
|
||||
on:change={() => {
|
||||
dispatch('update', { action: 'toggle', value: activityOrderNewestFirst })
|
||||
}}
|
||||
|
||||
@@ -1,550 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 { tick } from 'svelte'
|
||||
import type { ActivityExtension, DisplayTx, TxViewlet } from '@hcengineering/activity'
|
||||
import attachment from '@hcengineering/attachment'
|
||||
import contact, { Person, PersonAccount, getName } from '@hcengineering/contact'
|
||||
import core, { AnyAttribute, Doc, Ref, TxCUD, getCurrentAccount } from '@hcengineering/core'
|
||||
import { Asset } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import ui, {
|
||||
ActionIcon,
|
||||
AnyComponent,
|
||||
Component,
|
||||
Icon,
|
||||
IconActivityEdit,
|
||||
IconEdit,
|
||||
IconMoreH,
|
||||
Label,
|
||||
ShowMore,
|
||||
TimeSince,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import type { AttributeModel } from '@hcengineering/view'
|
||||
import { Menu, ObjectPresenter } from '@hcengineering/view-resources'
|
||||
|
||||
import TxViewTx from './TxViewTx.svelte'
|
||||
import ActivityExtensionComponent from './ActivityExtensionComponent.svelte'
|
||||
|
||||
import { ActivityKey } from '../activity'
|
||||
import activity from '../plugin'
|
||||
import { TxDisplayViewlet, getPrevValue, getValue, updateViewlet, getExtensions } from '../utils'
|
||||
|
||||
export let tx: DisplayTx
|
||||
export let viewlets: Map<ActivityKey, TxViewlet[]>
|
||||
export let showIcon: boolean = true
|
||||
export let isNew: boolean = false
|
||||
export let isNextNew: boolean = false
|
||||
export let contentHidden: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let boundary: HTMLElement | undefined = undefined
|
||||
// export let showDocument = false
|
||||
|
||||
let ptx: DisplayTx | undefined
|
||||
|
||||
let viewlet: TxDisplayViewlet | undefined
|
||||
let props: any
|
||||
let account: PersonAccount | undefined
|
||||
let person: Person | undefined
|
||||
let model: AttributeModel[] = []
|
||||
let modelIcon: Asset | undefined = undefined
|
||||
let iconComponent: AnyComponent | undefined = undefined
|
||||
let extensions: ActivityExtension[] = []
|
||||
|
||||
let edit: boolean = false
|
||||
let showDiff: boolean = false
|
||||
|
||||
const currentAccount = getCurrentAccount() as PersonAccount
|
||||
|
||||
$: if (tx.doc?._class) {
|
||||
getExtensions(client, tx.doc._class).then((res?: ActivityExtension[]) => {
|
||||
extensions = res || []
|
||||
})
|
||||
}
|
||||
|
||||
$: if (tx.tx._id !== ptx?.tx._id) {
|
||||
if (tx.tx.modifiedBy !== account?._id) {
|
||||
account = undefined
|
||||
person = undefined
|
||||
}
|
||||
viewlet = undefined
|
||||
props = undefined
|
||||
model = []
|
||||
ptx = tx
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
const query = createQuery()
|
||||
const employeeQuery = createQuery()
|
||||
|
||||
function getProps (props: any, edit: boolean): any {
|
||||
return { ...props, edit, attr: tx.collectionAttribute, boundary }
|
||||
}
|
||||
|
||||
$: updateViewlet(client, viewlets, tx).then((result) => {
|
||||
if (result.id === tx.tx._id) {
|
||||
viewlet = result.viewlet
|
||||
model = result.model
|
||||
modelIcon = result.modelIcon
|
||||
iconComponent = result.iconComponent
|
||||
props = getProps(result.props, edit)
|
||||
if (shouldScroll) {
|
||||
tick().then(scrollIntoView)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$: query.query(
|
||||
contact.class.PersonAccount,
|
||||
{ _id: tx.tx.modifiedBy as Ref<PersonAccount> },
|
||||
(res) => {
|
||||
;[account] = res
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
|
||||
$: account &&
|
||||
employeeQuery.query(
|
||||
contact.class.Person,
|
||||
{ _id: account.person },
|
||||
(res) => {
|
||||
;[person] = res
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
|
||||
const showMenu = async (ev: MouseEvent): Promise<void> => {
|
||||
showPopup(
|
||||
Menu,
|
||||
{
|
||||
object: tx.doc as Doc,
|
||||
actions: [
|
||||
{
|
||||
label: activity.string.Edit,
|
||||
icon: IconEdit,
|
||||
action: () => {
|
||||
edit = true
|
||||
props = getProps(props, edit)
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
ev.target as HTMLElement
|
||||
)
|
||||
}
|
||||
const onCancelEdit = () => {
|
||||
edit = false
|
||||
props = getProps(props, edit)
|
||||
}
|
||||
|
||||
function isMessageType (attr?: AnyAttribute): boolean {
|
||||
return attr?.type._class === core.class.TypeMarkup
|
||||
}
|
||||
|
||||
function isAttachment (tx: TxCUD<Doc>): boolean {
|
||||
return tx.objectClass === attachment.class.Attachment && tx._class === core.class.TxCreateDoc
|
||||
}
|
||||
|
||||
function isMention (extensions: ActivityExtension[]): boolean {
|
||||
return extensions.some(({ isMention }) => !!isMention)
|
||||
}
|
||||
|
||||
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
|
||||
$: updateMessageType(model, tx).then((res) => {
|
||||
hasMessageType = res
|
||||
})
|
||||
$: isComment = viewlet && viewlet?.editable
|
||||
$: isAttached = isAttachment(tx.tx)
|
||||
$: isMentioned = isMention(extensions)
|
||||
$: withAvatar = isComment || isMentioned || isAttached
|
||||
$: isEmphasized = viewlet?.display === 'emphasized' || model.every((m) => isMessageType(m.attribute))
|
||||
$: isColumn = isComment || isEmphasized || hasMessageType
|
||||
|
||||
let htmlElement: HTMLDivElement
|
||||
|
||||
function scrollIntoView () {
|
||||
htmlElement?.scrollIntoView({ behavior: 'auto', block: 'start' })
|
||||
shouldScroll = false
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if (viewlet !== undefined && !((viewlet?.hideOnRemove ?? false) && tx.removed)) || model.length > 0}
|
||||
<div
|
||||
class="msgactivity-container"
|
||||
bind:this={htmlElement}
|
||||
class:showIcon
|
||||
class:withAvatar
|
||||
class:isNew
|
||||
class:isNextNew
|
||||
>
|
||||
{#if showIcon}
|
||||
{#if withAvatar}
|
||||
<div class="msgactivity-avatar">
|
||||
<Component
|
||||
is={contact.component.Avatar}
|
||||
props={{ avatar: person?.avatar, size: 'medium', name: person?.name }}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="msgactivity-icon">
|
||||
{#if iconComponent}
|
||||
<Component is={iconComponent} {props} />
|
||||
{:else if viewlet}
|
||||
<Icon icon={viewlet.icon} size="small" />
|
||||
{:else if viewlet === undefined && model.length > 0}
|
||||
<Icon icon={modelIcon !== undefined ? modelIcon : IconActivityEdit} size="small" />
|
||||
{:else}
|
||||
<Icon icon={IconActivityEdit} size="small" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="msgactivity-content" class:content={isColumn} class:comment={withAvatar}>
|
||||
<div class="msgactivity-content__header">
|
||||
<div class="msgactivity-content__title labels-row">
|
||||
<span class={withAvatar ? 'bold' : 'strong'}>
|
||||
{#if person}
|
||||
{getName(client.getHierarchy(), person)}
|
||||
{:else}
|
||||
<Label label={core.string.System} />
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if viewlet && viewlet?.editable}
|
||||
{#if viewlet.label}
|
||||
<span class="lower"><Label label={viewlet.label} params={viewlet.labelParams ?? {}} /></span>
|
||||
{/if}
|
||||
{#if tx.updated}
|
||||
<span class="lower"><Label label={activity.string.Edited} /></span>
|
||||
{/if}
|
||||
<span class="time ml-1"><TimeSince value={tx.tx.modifiedOn} /></span>
|
||||
{:else if viewlet && viewlet.label}
|
||||
<span class="lower whitespace-nowrap">
|
||||
<Label label={viewlet.label} params={viewlet.labelParams ?? {}} />
|
||||
</span>
|
||||
{/if}
|
||||
{#if viewlet && viewlet.labelComponent}
|
||||
<Component is={viewlet.labelComponent} {props} />
|
||||
{/if}
|
||||
|
||||
{#if viewlet === undefined && model.length > 0 && tx.updateTx}
|
||||
{#each model as m}
|
||||
{#await getValue(client, m, tx) 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} accent />
|
||||
{:else}
|
||||
<svelte:component this={m.presenter} value={cvalue} accent />
|
||||
{/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} accent />
|
||||
{:else}
|
||||
<svelte:component this={m.presenter} value={cvalue} accent />
|
||||
{/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>
|
||||
|
||||
{#if !hasMessageType}
|
||||
<span class="lower"><Label label={activity.string.To} /></span>
|
||||
<span class="strong overflow-label">
|
||||
{#if value.isObjectSet}
|
||||
<ObjectPresenter value={value.set} accent />
|
||||
{:else}
|
||||
<svelte:component this={m.presenter} value={value.set} accent />
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<span class="show-diff" on:click={() => (showDiff = !showDiff)}>
|
||||
<Label label={showDiff ? ui.string.ShowLess : ui.string.ShowMore} />
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{/await}
|
||||
{/each}
|
||||
{:else if viewlet === undefined && model.length > 0 && tx.mixinTx}
|
||||
{#each model as m}
|
||||
{#await getValue(client, m, tx) 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 overflow-label">
|
||||
{#if value.isObjectSet}
|
||||
<ObjectPresenter value={value.set} accent />
|
||||
{:else}
|
||||
<svelte:component this={m.presenter} value={value.set} accent />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/await}
|
||||
{/each}
|
||||
{:else if viewlet && viewlet.display === 'inline' && viewlet.component}
|
||||
{#if tx.collectionAttribute !== undefined && (tx.txDocIds?.size ?? 0) > 1}
|
||||
<TxViewTx {tx} {onCancelEdit} {edit} {viewlet} />
|
||||
{:else if typeof viewlet.component === 'string'}
|
||||
<Component is={viewlet.component} {props} on:close={onCancelEdit} />
|
||||
{:else}
|
||||
<svelte:component this={viewlet.component} {...props} on:close={onCancelEdit} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if isComment}
|
||||
<div class="buttons-group">
|
||||
<ActivityExtensionComponent {extensions} kind="action" props={{ object: tx.doc }} />
|
||||
<!-- <Like /> -->
|
||||
{#if account?.person === currentAccount?.person}
|
||||
<ActionIcon icon={IconMoreH} size={'small'} action={showMenu} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if hasMessageType || isColumn}
|
||||
<span class="time top ml-4"><TimeSince value={tx.tx.modifiedOn} /></span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !isColumn}
|
||||
<span class="time top ml-4"><TimeSince value={tx.tx.modifiedOn} /></span>
|
||||
{/if}
|
||||
|
||||
{#if viewlet && viewlet.display !== 'inline'}
|
||||
<div class="activity-content content" class:indent={isAttached} class:contentHidden>
|
||||
<ShowMore ignore={edit}>
|
||||
{#if tx.collectionAttribute !== undefined && (tx.txDocIds?.size ?? 0) > 1}
|
||||
<div class="flex-row-center flex-grow flex-wrap m--1 gap-around-2 clear-mins">
|
||||
<TxViewTx {tx} {onCancelEdit} {edit} {viewlet} presentersOnly={isAttached} />
|
||||
</div>
|
||||
{:else if typeof viewlet.component === 'string'}
|
||||
<Component is={viewlet.component} {props} on:close={onCancelEdit} />
|
||||
{:else}
|
||||
<svelte:component this={viewlet.component} {...props} on:close={onCancelEdit} />
|
||||
{/if}
|
||||
</ShowMore>
|
||||
</div>
|
||||
{:else if hasMessageType && model.length > 0 && (tx.updateTx || tx.mixinTx)}
|
||||
{#await getValue(client, model[0], tx) then value}
|
||||
{@const prevValue = getPrevValue(client, model[0], tx)}
|
||||
<div class="activity-content content" class:indent={isAttached} class:contentHidden>
|
||||
<ShowMore ignore={edit || prevValue !== undefined}>
|
||||
{#if value.isObjectSet}
|
||||
<ObjectPresenter value={value.set} />
|
||||
{:else if showDiff}
|
||||
<svelte:component this={model[0].presenter} value={value.set} {prevValue} showOnlyDiff />
|
||||
{/if}
|
||||
</ShowMore>
|
||||
</div>
|
||||
{/await}
|
||||
{/if}
|
||||
<ActivityExtensionComponent {extensions} kind="footer" props={{ object: tx.doc }} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.msgactivity-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
|
||||
.msgactivity-icon,
|
||||
.msgactivity-avatar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 1rem;
|
||||
width: 2.25rem;
|
||||
min-width: 2.25rem;
|
||||
color: var(--theme-darker-color);
|
||||
}
|
||||
|
||||
.msgactivity-icon {
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
.msgactivity-avatar {
|
||||
height: 2.25rem;
|
||||
// background-color: var(--theme-darker-color);
|
||||
border: 1px dashed var(--divider-trans-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.msgactivity-content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-right: 1rem;
|
||||
min-width: 0;
|
||||
color: var(--theme-dark-color);
|
||||
|
||||
.msgactivity-content__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.msgactivity-content__title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&.content {
|
||||
flex-direction: column;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
&:not(.comment) {
|
||||
.msgactivity-content__header {
|
||||
min-height: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.content) {
|
||||
align-items: center;
|
||||
|
||||
.msgactivity-content__header {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.showIcon {
|
||||
&::after,
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 1.125rem;
|
||||
width: 1px;
|
||||
background-color: var(--divider-trans-color);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.isNew::before {
|
||||
background-color: var(--highlight-red);
|
||||
}
|
||||
|
||||
&.isNextNew {
|
||||
&::after {
|
||||
background-color: var(--highlight-red);
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
&.withAvatar::after {
|
||||
content: '';
|
||||
top: 2.25rem;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&:not(.withAvatar)::after {
|
||||
content: '';
|
||||
top: 1.75rem;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.msgactivity-container + .msgactivity-container::before) {
|
||||
content: '';
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-trans-color);
|
||||
|
||||
&.top {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
visibility: visible;
|
||||
min-width: 0;
|
||||
max-height: max-content;
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
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;
|
||||
}
|
||||
|
||||
&.indent {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.show-diff {
|
||||
color: var(--theme-content-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--theme-content-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,46 +0,0 @@
|
||||
<script lang="ts">
|
||||
import core, { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { Component, IconAdd, IconDelete } from '@hcengineering/ui'
|
||||
import { getDTxProps, TxDisplayViewlet } from '../utils'
|
||||
import { DisplayTx } from '@hcengineering/activity'
|
||||
|
||||
export let tx: DisplayTx
|
||||
export let viewlet: TxDisplayViewlet
|
||||
export let edit: boolean
|
||||
export let presentersOnly: boolean = false
|
||||
export let onCancelEdit: () => void
|
||||
|
||||
function filterTx (dtx: DisplayTx[], _class: Ref<Class<Doc>>): DisplayTx[] {
|
||||
return dtx.filter((it) => it.tx._class === _class)
|
||||
}
|
||||
function getProps (ctx: DisplayTx, edit: boolean): any {
|
||||
if (viewlet?.pseudo) {
|
||||
return { value: ctx.doc }
|
||||
}
|
||||
const dprops = getDTxProps(ctx)
|
||||
return { ...dprops, edit }
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each filterTx([...tx.txes, tx], core.class.TxCreateDoc) as ctx, i}
|
||||
{#if i === 0 && !presentersOnly}
|
||||
<IconAdd size={'x-small'} fill={'var(--theme-trans-color)'} />
|
||||
{/if}
|
||||
{#if typeof viewlet?.component === 'string'}
|
||||
<Component is={viewlet?.component} props={getProps(ctx, edit)} disabled on:close={onCancelEdit} />
|
||||
{:else}
|
||||
<svelte:component this={viewlet?.component} {...getProps(ctx, edit)} disabled on:close={onCancelEdit} />
|
||||
{/if}
|
||||
<div class="antiHSpacer" />
|
||||
{/each}
|
||||
{#each filterTx([...tx.txes, tx], core.class.TxRemoveDoc) as ctx, i}
|
||||
{#if i === 0 && !presentersOnly}
|
||||
<IconDelete size={'x-small'} fill={'var(--theme-trans-color)'} />
|
||||
{/if}
|
||||
{#if typeof viewlet?.component === 'string'}
|
||||
<Component is={viewlet?.component} props={getProps(ctx, edit)} disabled on:close={onCancelEdit} />
|
||||
{:else}
|
||||
<svelte:component this={viewlet?.component} {...getProps(ctx, edit)} disabled on:close={onCancelEdit} />
|
||||
{/if}
|
||||
<div class="antiHSpacer" />
|
||||
{/each}
|
||||
Reference in New Issue
Block a user