Activity: filters (#2395)

* Activity: filters
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2022-11-27 19:05:26 +03:00 committed by GitHub
parent 46b70a2e9c
commit 90bc1da39f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 313 additions and 39 deletions

View File

@ -71,7 +71,7 @@
"@hcengineering/contact-assets": "~0.6.0",
"@hcengineering/activity": "~0.6.0",
"@hcengineering/activity-assets": "~0.6.0",
"@hcengineering/activity-resources": "~0.6.0",
"@hcengineering/activity-resources": "~0.6.1",
"@hcengineering/automation": "~0.6.0",
"@hcengineering/automation-assets": "~0.6.0",
"@hcengineering/automation-resources": "~0.6.0",

View File

@ -13,12 +13,12 @@
// limitations under the License.
//
import type { TxViewlet } from '@hcengineering/activity'
import type { ActivityFilter, DisplayTx, TxViewlet } from '@hcengineering/activity'
import activity from '@hcengineering/activity'
import core, { Class, Doc, DocumentQuery, DOMAIN_MODEL, Ref, Tx } from '@hcengineering/core'
import { Builder, Model } from '@hcengineering/model'
import { TDoc } from '@hcengineering/model-core'
import type { Asset, IntlString } from '@hcengineering/platform'
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui'
@Model(activity.class.TxViewlet, core.class.Doc, DOMAIN_MODEL)
@ -36,6 +36,12 @@ export class TTxViewlet extends TDoc implements TxViewlet {
hideOnRemove!: boolean
}
export function createModel (builder: Builder): void {
builder.createModel(TTxViewlet)
@Model(activity.class.ActivityFilter, core.class.Class, DOMAIN_MODEL)
export class TActivityFilter extends TDoc implements ActivityFilter {
label!: IntlString
filter!: Resource<(txes: DisplayTx[]) => DisplayTx[]>
}
export function createModel (builder: Builder): void {
builder.createModel(TTxViewlet, TActivityFilter)
}

View File

@ -96,6 +96,11 @@ export function createModel (builder: Builder): void {
attachment.ids.TxAttachmentCreate
)
builder.createDoc(activity.class.ActivityFilter, core.space.Model, {
label: attachment.string.FilterAttachments,
filter: attachment.filter.AttachmentsFilter
})
builder.createDoc(
view.class.ActionCategory,
core.space.Model,

View File

@ -13,11 +13,11 @@
// limitations under the License.
//
import type { TxViewlet } from '@hcengineering/activity'
import type { DisplayTx, TxViewlet } from '@hcengineering/activity'
import { attachmentId } from '@hcengineering/attachment'
import attachment from '@hcengineering/attachment-resources/src/plugin'
import type { Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import type { IntlString, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
import type { ActionCategory } from '@hcengineering/view'
@ -38,7 +38,8 @@ export default mergeIds(attachmentId, attachment, {
SavedAttachments: '' as IntlString,
Description: '' as IntlString,
PinAttachment: '' as IntlString,
UnPinAttachment: '' as IntlString
UnPinAttachment: '' as IntlString,
FilterAttachments: '' as IntlString
},
ids: {
TxAttachmentCreate: '' as Ref<TxViewlet>
@ -48,5 +49,8 @@ export default mergeIds(attachmentId, attachment, {
},
category: {
Attachments: '' as Ref<ActionCategory>
},
filter: {
AttachmentsFilter: '' as Resource<(txes: DisplayTx[]) => DisplayTx[]>
}
})

View File

@ -473,6 +473,16 @@ export function createModel (builder: Builder, options = { addApplication: true
chunter.ids.TxBacklinkRemove
)
builder.createDoc(activity.class.ActivityFilter, core.space.Model, {
label: chunter.string.FilterComments,
filter: chunter.filter.CommentsFilter
})
builder.createDoc(activity.class.ActivityFilter, core.space.Model, {
label: chunter.string.FilterBacklinks,
filter: chunter.filter.BacklinksFilter
})
builder.mixin(chunter.class.ChunterMessage, core.class.Class, view.mixin.ClassFilters, {
filters: ['space', 'modifiedOn', 'createBy', '_class']
})

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import type { TxViewlet } from '@hcengineering/activity'
import type { DisplayTx, TxViewlet } from '@hcengineering/activity'
import { Channel, chunterId } from '@hcengineering/chunter'
import chunter from '@hcengineering/chunter-resources/src/plugin'
import type { Ref, Space } from '@hcengineering/core'
@ -67,7 +67,9 @@ export default mergeIds(chunterId, chunter, {
SavedMessages: '' as IntlString,
ThreadMessage: '' as IntlString,
Reactions: '' as IntlString,
Emoji: '' as IntlString
Emoji: '' as IntlString,
FilterComments: '' as IntlString,
FilterBacklinks: '' as IntlString
},
viewlet: {
Chat: '' as Ref<ViewletDescriptor>
@ -89,5 +91,9 @@ export default mergeIds(chunterId, chunter, {
},
function: {
ChunterBrowserVisible: '' as Resource<(spaces: Space[]) => boolean>
},
filter: {
CommentsFilter: '' as Resource<(txes: DisplayTx[]) => DisplayTx[]>,
BacklinksFilter: '' as Resource<(txes: DisplayTx[]) => DisplayTx[]>
}
})

View File

@ -87,7 +87,7 @@
{#if $$slots.header}
<div class="header-row between">
{#if $$slots.header}<slot name="header" />{/if}
<div class="buttons-group xsmall-gap ml-4" style:align-self={'flex-start'}>
<div class="buttons-group xsmall-gap ml-4" style:align-self={'flex-end'}>
<slot name="tools" />
</div>
</div>

View File

@ -527,6 +527,7 @@ input.search {
.min-h-7 { min-height: 1.75rem; }
.min-h-30 { min-height: 7.5rem; }
.min-h-60 { min-height: 15rem; }
.max-w-2 { max-width: .5rem; }
.max-w-9 { max-width: 2.25rem; }
.max-w-30 { max-width: 7.5rem; }
.max-w-60 { max-width: 15rem; }

View File

@ -34,7 +34,7 @@
}
</script>
<label class="checkbox" class:circle class:primary class:checked>
<label class="checkbox" class:circle class:primary class:readonly class:checked>
<input class="chBox" disabled={readonly} type="checkbox" bind:checked on:change={handleValueChanged} />
<svg class="checkSVG" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
{#if checked}
@ -71,6 +71,10 @@
background-color: var(--primary-bg-color);
border-color: transparent;
}
&.readonly.checked {
background-color: var(--dark-color);
border-color: transparent;
}
.chBox {
position: absolute;
@ -94,8 +98,8 @@
&:not(:disabled) + .checkSVG {
cursor: pointer;
}
&:disabled + .checkSVG {
filter: grayscale(70%);
&:disabled + .checkSVG .check {
fill: var(--content-color);
}
}
.checkSVG {

View File

@ -45,7 +45,8 @@
maxWidth: '',
maxHeight: '',
minWidth: '',
minHeight: ''
minHeight: '',
transform: ''
},
showOverlay: false,
direction: 'bottom'

View File

@ -13,6 +13,7 @@
"CollectionUpdated": "Update {collection}",
"Added": "added",
"Removed": "removed",
"From": "from"
"From": "from",
"All": "All"
}
}

View File

@ -13,6 +13,7 @@
"CollectionUpdated": "Обновлена {collection}",
"Added": "добавила(а)",
"Removed": "удалил(а)",
"From": "из"
"From": "из",
"All": "Все"
}
}

View File

@ -13,17 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import activity, { TxViewlet } from '@hcengineering/activity'
import activity, { TxViewlet, ActivityFilter } from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import core, { Class, Doc, Ref, SortingOrder } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { getResource, IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import notification from '@hcengineering/notification'
import { Component, Grid, IconActivity, Label, Scroller } from '@hcengineering/ui'
import { Component, Grid, IconActivity, Label, Scroller, Button, showPopup } from '@hcengineering/ui'
import { ActivityKey, activityKey, DisplayTx, newActivity } from '../activity'
import TxView from './TxView.svelte'
import { filterCollectionTxes } from './utils'
import { Writable } from 'svelte/store'
import view from '@hcengineering/view'
import activityPlg from '../plugin'
import FilterPopup from './FilterPopup.svelte'
export let object: Doc
export let integrate: boolean = false
@ -31,10 +34,18 @@
export let transparent: boolean = false
let txes: DisplayTx[] = []
let txesF: DisplayTx[] = []
const client = getClient()
const attrs = client.getHierarchy().getAllAttributes(object._class)
let filterLabel: IntlString = activityPlg.string.All
const filters: ActivityFilter[] = []
const saved = localStorage.getItem('activity-filter')
let selectedFilter: Ref<Doc> | 'All' = saved !== null && saved !== undefined ? JSON.parse(saved) : 'All'
$: localStorage.setItem('activity-filter', JSON.stringify(selectedFilter))
client.findAll(activity.class.ActivityFilter, {}).then((res) => res.map((it) => filters.push(it)))
const activityQuery = newActivity(client, attrs)
getResource(notification.function.GetNotificationClient).then((res) => {
lastViews = res().getLastViews()
@ -82,6 +93,25 @@
}
return -1
}
let optionsBtn: HTMLButtonElement
const handleOptions = () => {
showPopup(FilterPopup, { selectedFilter, filters }, optionsBtn, (res) => {
if (res === undefined) return
if (res.action === 'select') selectedFilter = res.value as Ref<Doc> | 'All'
})
}
$: if (selectedFilter || txes) {
const filter = filters.find((it) => it._id === selectedFilter)
if (filter) {
filterLabel = filter.label
getResource(filter.filter).then((result) => (txesF = result(txes)))
} else {
filterLabel = activityPlg.string.All
txesF = txes
}
}
</script>
{#if !integrate || transparent}
@ -96,9 +126,9 @@
<div class="flex-col flex-grow min-h-0" class:background-accent-bg-color={!transparent}>
<Scroller>
<div class="p-10 select-text" id={activity.string.Activity}>
{#if txes}
{#if txesF}
<Grid column={1} rowGap={1.5}>
{#each txes as tx, i}
{#each txesF as tx, i}
<TxView {tx} {viewlets} isNew={newTxPos === i} />
{/each}
</Grid>
@ -117,6 +147,15 @@
<div class="antiSection-header mt-6">
<div class="antiSection-header__icon"><IconActivity size={'small'} /></div>
<span class="antiSection-header__title"><Label label={activity.string.Activity} /></span>
<span class="dark-color text-md"><Label label={filterLabel} /></span>
<div class="w-2 min-w-2 max-w-2" />
<Button
bind:input={optionsBtn}
icon={view.icon.ViewButton}
kind={'transparent'}
shape={'circle'}
on:click={handleOptions}
/>
</div>
{#if showCommenInput}
<div class="ref-input">
@ -124,9 +163,9 @@
</div>
{/if}
<div class="p-activity select-text" id={activity.string.Activity}>
{#if txes}
{#if txesF}
<Grid column={1} rowGap={1.5}>
{#each txes as tx, i}
{#each txesF as tx, i}
<TxView {tx} {viewlets} isNew={newTxPos === i} />
{/each}
</Grid>

View File

@ -0,0 +1,113 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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 { createEventDispatcher, onMount } from 'svelte'
import { IntlString } from '@hcengineering/platform'
import { Label, resizeObserver, CheckBox } from '@hcengineering/ui'
import { Doc, Ref } from '@hcengineering/core'
import { ActivityFilter } from '@hcengineering/activity'
import activity from '../plugin'
export let selectedFilter: Ref<Doc> | 'All' = 'All'
export let filters: ActivityFilter[] = []
const dispatch = createEventDispatcher()
interface ActionMenu {
label: IntlString
checked: boolean
value: Ref<Doc> | 'All'
}
const menu: ActionMenu[] = [
{
label: activity.string.All,
checked: selectedFilter === 'All',
value: 'All'
}
]
filters.map((fl) => menu.push({ label: fl.label, checked: selectedFilter === fl._id, value: fl._id }))
let popup: HTMLElement
$: popup?.focus()
const btns: HTMLElement[] = []
let activeElement: HTMLElement
const keyDown = (ev: KeyboardEvent): void => {
console.log('[KEY]', ev.key)
const n = btns.indexOf(activeElement) ?? 0
if (ev.key === ' ' || ev.key === 'Enter') {
ev.preventDefault()
ev.stopPropagation()
btns[n].focus()
btns[n].click()
}
if (ev.key === 'ArrowDown') {
if (n < btns.length - 1) {
activeElement = btns[n + 1]
}
ev.preventDefault()
ev.stopPropagation()
}
if (ev.key === 'ArrowUp') {
if (n > 0) {
activeElement = btns[n - 1]
}
ev.preventDefault()
ev.stopPropagation()
}
}
onMount(() => {
if (btns[0]) {
btns[0].focus()
}
})
</script>
<div
class="antiPopup"
use:resizeObserver={() => {
dispatch('changeContent')
}}
on:keydown={keyDown}
>
<div class="ap-space" />
<div class="ap-scroll">
<div class="ap-box" bind:this={popup}>
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
{#each menu as item, i}
<button
bind:this={btns[i]}
class="ap-menuItem flex-row-center withIcon"
class:hover={btns[i] === activeElement}
on:mousemove={() => {
if (btns[i] !== activeElement) activeElement = btns[i]
}}
on:click={() => {
dispatch('close', { action: 'select', value: item.value })
}}
>
<div class="flex-center justify-end mr-3 pointer-events-none">
<CheckBox checked={item.checked} />
</div>
<span class="overflow-label">
<Label label={item.label} />
</span>
</button>
{/each}
</div>
</div>
<div class="ap-space" />
</div>

View File

@ -28,6 +28,7 @@ export default mergeIds(activityId, activity, {
System: '' as IntlString,
Added: '' as IntlString,
Removed: '' as IntlString,
From: '' as IntlString
From: '' as IntlString,
All: '' as IntlString
}
})

View File

@ -13,8 +13,21 @@
// limitations under the License.
//
import type { Class, Doc, DocumentQuery, Ref, Tx } from '@hcengineering/core'
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import type {
AttachedDoc,
Attribute,
Class,
Collection,
Doc,
DocumentQuery,
Ref,
Tx,
TxCreateDoc,
TxCUD,
TxMixin,
TxUpdateDoc
} from '@hcengineering/core'
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
@ -47,23 +60,64 @@ export interface TxViewlet extends Doc {
// If defined and true, will hide all transactions from object in case it is deleted.
hideOnRemove?: boolean
}
/**
* Transaction being displayed.
* @public
*/
export interface DisplayTx {
// Source tx
tx: TxCUD<Doc>
// A set of collapsed transactions.
txes: DisplayTx[]
txDocIds?: Set<Ref<Doc>>
// type check for createTx
createTx?: TxCreateDoc<Doc>
// Type check for updateTx
updateTx?: TxUpdateDoc<Doc>
// Type check for updateTx
mixinTx?: TxMixin<Doc, Doc>
// Document in case it is required.
doc?: Doc
updated: boolean
mixin: boolean
removed: boolean
collectionAttribute?: Attribute<Collection<AttachedDoc>>
}
/**
* @public
*/
export interface ActivityFilter extends Doc {
label: IntlString
filter: Resource<(txes: DisplayTx[]) => DisplayTx[]>
}
/**
* @public
*/
export const activityId = 'activity' as Plugin
export default plugin(activityId, {
icon: {
Activity: '' as Asset
},
string: {
Delete: '' as IntlString,
Edit: '' as IntlString,
Edited: '' as IntlString,
Activity: '' as IntlString
},
icon: {
Activity: '' as Asset
},
class: {
TxViewlet: '' as Ref<Class<TxViewlet>>
TxViewlet: '' as Ref<Class<TxViewlet>>,
ActivityFilter: '' as Ref<Class<ActivityFilter>>
},
component: {
Activity: '' as AnyComponent

View File

@ -48,6 +48,7 @@
"Description": "Description",
"Pinned": "Important",
"PinAttachment": "Mark important",
"UnPinAttachment": "Mark less important"
"UnPinAttachment": "Mark less important",
"FilterAttachments": "Attachments"
}
}

View File

@ -48,6 +48,7 @@
"Description": "Описание",
"Pinned": "Важное",
"PinAttachment": "Пометить как важное",
"UnPinAttachment": "Убрать пометку важное"
"UnPinAttachment": "Убрать пометку важное",
"FilterAttachments": "Вложения"
}
}

View File

@ -44,6 +44,7 @@
"@hcengineering/text-editor": "~0.6.0",
"@hcengineering/login": "~0.6.1",
"filesize": "^8.0.3",
"@hcengineering/preference": "^0.6.1"
"@hcengineering/preference": "^0.6.1",
"@hcengineering/activity": "~0.6.0"
}
}

View File

@ -33,6 +33,7 @@ import FileDownload from './components/icons/FileDownload.svelte'
import Photos from './components/Photos.svelte'
import AttachmentStyledBox from './components/AttachmentStyledBox.svelte'
import { deleteFile, uploadFile } from './utils'
import { DisplayTx } from '@hcengineering/activity'
export {
AddAttachment,
@ -221,7 +222,14 @@ export async function DeleteAttachment (attach: Attachment): Promise<void> {
)
}
export function attachmentsFilter (txes: DisplayTx[]): DisplayTx[] {
return txes.filter((tx) => tx.tx.objectClass === attachment.class.Attachment)
}
export default async (): Promise<Resources> => ({
filter: {
AttachmentsFilter: attachmentsFilter
},
component: {
AttachmentsPresenter,
AttachmentPresenter,

View File

@ -66,6 +66,8 @@
"ChunterBrowser": "Search",
"Messages": "Messages",
"NoResults": "No results",
"CopyLink": "Copy link"
"CopyLink": "Copy link",
"FilterComments": "Comments",
"FilterBacklinks": "Backlinks"
}
}

View File

@ -66,6 +66,8 @@
"ChunterBrowser": "Поиск",
"Messages": "Сообщения",
"NoResults": "Нет результатов",
"CopyLink": "Копировать ссылку"
"CopyLink": "Копировать ссылку",
"FilterComments": "Коментарии",
"FilterBacklinks": "Упоминания"
}
}

View File

@ -52,6 +52,7 @@ import ConvertDmToPrivateChannelModal from './components/ConvertDmToPrivateChann
import { getDmName } from './utils'
import { writable } from 'svelte/store'
import { updateBacklinksList } from './backlinks'
import { DisplayTx } from '../../activity/lib'
export { default as Header } from './components/Header.svelte'
export { classIcon } from './utils'
@ -206,7 +207,19 @@ async function update (source: Doc, key: string, target: RelatedDocument[], msg:
await updateBacklinksList(getClient(), q, backlinks)
}
export function commentsFilter (txes: DisplayTx[]): DisplayTx[] {
return txes.filter((tx) => tx.tx.objectClass === chunter.class.Comment)
}
export function backlinksFilter (txes: DisplayTx[]): DisplayTx[] {
return txes.filter((tx) => tx.tx.objectClass === chunter.class.Backlink)
}
export default async (): Promise<Resources> => ({
filter: {
CommentsFilter: commentsFilter,
BacklinksFilter: backlinksFilter
},
component: {
CommentInput,
CreateChannel,

View File

@ -387,7 +387,7 @@
<UpDownNavigator element={documentObject} />
</svelte:fragment>
<svelte:fragment slot="header">
<span class="fs-title flex-row-center">
<span class="fs-title flex-row-center flex-shrink gap-1-5">
<EditBox
bind:value={name}
placeholder={document.string.DocumentNamePlaceholder}

View File

@ -36,7 +36,7 @@
"@hcengineering/notification": "~0.6.4",
"@hcengineering/ui": "^0.6.2",
"@hcengineering/presentation": "~0.6.2",
"@hcengineering/activity-resources": "~0.6.0",
"@hcengineering/activity-resources": "~0.6.1",
"@hcengineering/activity": "~0.6.0",
"@hcengineering/contact": "~0.6.8",
"@hcengineering/core": "^0.6.17",

View File

@ -73,7 +73,7 @@
"@hcengineering/contact-assets": "~0.6.0",
"@hcengineering/activity": "~0.6.0",
"@hcengineering/activity-assets": "~0.6.0",
"@hcengineering/activity-resources": "~0.6.0",
"@hcengineering/activity-resources": "~0.6.1",
"@hcengineering/telegram": "~0.6.2",
"@hcengineering/telegram-assets": "~0.6.0",
"@hcengineering/telegram-resources": "~0.6.0",