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:
Kristina
2023-12-11 18:54:36 +07:00
committed by GitHub
co-authored by Andrey Sobolev
parent 119d65653a
commit ea3eb4af83
212 changed files with 7611 additions and 3159 deletions
@@ -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}