TSK-997 В Activity Feed таланта писать событие об отправленном письме (#2917)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-04-07 12:09:16 +06:00 committed by GitHub
parent b699096e22
commit cab96e795d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 441 additions and 309 deletions

View File

@ -13,11 +13,11 @@
// limitations under the License.
//
import type { ActivityFilter, DisplayTx, TxViewlet } from '@hcengineering/activity'
import type { ActivityFilter, DisplayTx, ExtraActivityComponent, TxViewlet } from '@hcengineering/activity'
import activity from './plugin'
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 { Builder, Mixin, Model } from '@hcengineering/model'
import { TClass, TDoc } from '@hcengineering/model-core'
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui'
@ -42,8 +42,13 @@ export class TActivityFilter extends TDoc implements ActivityFilter {
filter!: Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>
}
@Mixin(activity.mixin.ExtraActivityComponent, core.class.Class)
export class TExtraActivityComponent extends TClass implements ExtraActivityComponent {
component!: AnyComponent
}
export function createModel (builder: Builder): void {
builder.createModel(TTxViewlet, TActivityFilter)
builder.createModel(TTxViewlet, TActivityFilter, TExtraActivityComponent)
builder.createDoc(activity.class.ActivityFilter, core.space.Model, {
label: activity.string.Attributes,

View File

@ -36,6 +36,7 @@
"@hcengineering/core": "^0.6.21",
"@hcengineering/ui": "^0.6.3",
"@hcengineering/platform": "^0.6.8",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/contact": "^0.6.11",
"@hcengineering/notification": "^0.6.8",
"@hcengineering/contact-resources": "^0.6.0",

View File

@ -31,6 +31,7 @@ import {
Status,
ContactsTab
} from '@hcengineering/contact'
import activity from '@hcengineering/activity'
import { Class, DateRangeMode, Domain, DOMAIN_MODEL, IndexKind, Ref, Timestamp } from '@hcengineering/core'
import {
Builder,
@ -722,6 +723,10 @@ export function createModel (builder: Builder): void {
},
contact.templateField.ContactLastName
)
builder.mixin(contact.class.Contact, core.class.Class, activity.mixin.ExtraActivityComponent, {
component: contact.component.ActivityChannelMessage
})
}
export { contactOperation } from './migration'

View File

@ -49,7 +49,8 @@ export default mergeIds(contactId, contact, {
CreateEmployee: '' as AnyComponent,
AccountArrayEditor: '' as AnyComponent,
ChannelFilter: '' as AnyComponent,
MergeEmployee: '' as AnyComponent
MergeEmployee: '' as AnyComponent,
ActivityChannelMessage: '' as AnyComponent
},
string: {
Persons: '' as IntlString,

View File

@ -150,12 +150,26 @@ export function createModel (builder: Builder): void {
gmail.integrationType.Gmail
)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
{
objectClass: gmail.class.Message,
icon: contact.icon.Email,
txClass: core.class.TxCreateDoc,
label: gmail.string.HaveWrittenEmail,
labelComponent: gmail.activity.TxWriteMessage,
display: 'inline'
},
gmail.ids.TxSharedCreate
)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
{
objectClass: gmail.class.SharedMessages,
icon: contact.icon.Telegram,
icon: contact.icon.Email,
txClass: core.class.TxCreateDoc,
component: gmail.activity.TxSharedCreate,
label: gmail.string.SharedMessages,

View File

@ -36,7 +36,6 @@ export default mergeIds(gmailId, gmail, {
Message: '' as IntlString,
Messages: '' as IntlString,
Incoming: '' as IntlString,
Email: '' as IntlString,
Status: '' as IntlString,
EmailPlaceholder: '' as IntlString
},
@ -44,7 +43,8 @@ export default mergeIds(gmailId, gmail, {
TxSharedCreate: '' as Ref<TxViewlet>
},
activity: {
TxSharedCreate: '' as AnyComponent
TxSharedCreate: '' as AnyComponent,
TxWriteMessage: '' as AnyComponent
},
function: {
HasEmail: '' as Resource<(doc?: Doc | Doc[] | undefined) => Promise<boolean>>

View File

@ -139,28 +139,20 @@
<div class="h-2 min-h-2 max-h-2" />
</svelte:fragment>
{#if withoutActivity}
{#if $deviceInfo.isMobile}
<div class="popupPanel-body__mobile-content clear-mins">
<slot />
</div>
{:else}
<div class="popupPanel-body__main-content py-8 clear-mins" class:max={useMaxWidth}>
<slot />
</div>
{/if}
{:else if $deviceInfo.isMobile}
{#if $deviceInfo.isMobile}
<div class="popupPanel-body__mobile-content clear-mins" class:max={useMaxWidth}>
<Component is={activity.component.Activity} props={{ object, integrate: true, showCommenInput: !withoutInput }}>
<slot />
</Component>
<slot />
{#if !withoutActivity}
<Component is={activity.component.Activity} props={{ object, showCommenInput: !withoutInput }} />
{/if}
</div>
{:else}
<Scroller>
<div class="popupPanel-body__main-content py-8 clear-mins" class:max={useMaxWidth}>
<Component is={activity.component.Activity} props={{ object, integrate: true, showCommenInput: !withoutInput }}>
<slot />
</Component>
<slot />
{#if !withoutActivity}
<Component is={activity.component.Activity} props={{ object, showCommenInput: !withoutInput }} />
{/if}
</div>
</Scroller>
{/if}

View File

@ -1,3 +1,4 @@
import { DisplayTx } from '@hcengineering/activity'
import core, {
AnyAttribute,
AttachedDoc,
@ -43,37 +44,6 @@ function isEqualOps (op1: any, op2: any): boolean {
return o1 === o2
}
/**
* 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
*/

View File

@ -13,35 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import activity, { ActivityFilter, TxViewlet } from '@hcengineering/activity'
import activity, { DisplayTx, TxViewlet } from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import core, { Class, Doc, Ref, SortingOrder } from '@hcengineering/core'
import notification, { LastView } from '@hcengineering/notification'
import { getResource, IntlString } from '@hcengineering/platform'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
ActionIcon,
Component,
eventToHTMLElement,
Grid,
Icon,
IconActivity,
Label,
Scroller,
showPopup,
Spinner
} from '@hcengineering/ui'
import { Component, Grid, Label, Spinner } from '@hcengineering/ui'
import { Writable } from 'svelte/store'
import { ActivityKey, activityKey, DisplayTx, newActivity } from '../activity'
import activityPlg from '../plugin'
import { ActivityKey, activityKey, newActivity } from '../activity'
import { filterCollectionTxes } from '../utils'
import FilterPopup from './FilterPopup.svelte'
import IconClose from './icons/Close.svelte'
import IconFilter from './icons/Filter.svelte'
import ActivityFilter from './ActivityFilter.svelte'
import TxView from './TxView.svelte'
export let object: Doc
export let integrate: boolean = false
export let showCommenInput: boolean = true
export let transparent: boolean = false
@ -50,23 +35,6 @@
const client = getClient()
const attrs = client.getHierarchy().getAllAttributes(object._class)
let labels: IntlString[] = []
const filters: ActivityFilter[] = []
const saved = localStorage.getItem('activity-filter')
let selectedFilter: Ref<Doc>[] | 'All' =
saved !== null && saved !== undefined ? (JSON.parse(saved) as Ref<Doc>[] | 'All') : 'All'
$: localStorage.setItem('activity-filter', JSON.stringify(selectedFilter))
client.findAll(activity.class.ActivityFilter, {}).then((res) => {
res.map((it) => filters.push(it))
if (saved !== null && saved !== undefined) {
const temp: Ref<Doc>[] | 'All' = JSON.parse(saved)
if (temp !== 'All' && Array.isArray(temp)) {
selectedFilter = temp.filter((it) => filters.findIndex((f) => it === f._id) > -1)
if ((selectedFilter as Ref<Doc>[]).length === 0) selectedFilter = 'All'
} else selectedFilter = 'All'
}
})
const activityQuery = newActivity(client, attrs)
getResource(notification.function.GetNotificationClient).then((res) => {
lastViews = res().getLastViews()
@ -97,6 +65,7 @@
activityQuery.update(
object,
(result) => {
console.log('query txes update')
txes = filterCollectionTxes(result)
if (txes.length > 0) {
@ -110,6 +79,8 @@
$: if (editableMap) updateTxes(object)
let filtered: DisplayTx[] = []
$: newTxPos = newTx(filtered, $lastViews)
function newTx (txes: DisplayTx[], lastViews: LastView | undefined): number {
@ -121,136 +92,35 @@
}
return -1
}
const handleOptions = (ev: MouseEvent) => {
showPopup(
FilterPopup,
{ selectedFilter, filters },
eventToHTMLElement(ev),
() => {},
(res) => {
if (res === undefined) return
if (res.action === 'select') selectedFilter = res.value as Ref<Doc>[] | 'All'
}
)
}
let filterActions: ((tx: DisplayTx, _class?: Ref<Doc>) => boolean)[] = [] // Enabled filters
const updateFiltered = () => (filtered = txes.filter((it) => filterActions.some((f) => f(it, object._class))))
async function updateFilterActions (fls: ActivityFilter[], selected: Ref<Doc>[] | 'All'): Promise<void> {
if (selected === 'All' || !Array.isArray(selected)) filterActions = [() => true]
else {
const tf = fls.filter((filter) => (selected as Ref<Doc>[]).includes(filter._id))
filterActions = []
labels = []
tf.forEach((filter) => {
labels.push(filter.label)
getResource(filter.filter).then((res) => filterActions.push(res))
})
}
setTimeout(() => updateFiltered(), 0)
}
$: updateFilterActions(filters, selectedFilter)
$: filtered = txes.filter((it) => filterActions.some((f) => f(it, object._class)))
</script>
{#if !integrate || transparent}
<!-- OLD TRANSPARENT -->
{#if transparent !== undefined && !transparent}
<div class="ac-header short mirror-tool highlight">
<div class="ac-header__wrap-title">
<div class="flex-center icon"><IconActivity size={'small'} /></div>
<span class="ac-header__title flex-row-center">
<Label label={activity.string.Activity} />
{#if loading}
<div class="ml-1">
<Spinner size={'small'} />
</div>
{/if}
</span>
</div>
</div>
{/if}
<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 filtered}
<Grid column={1} rowGap={1.5}>
{#each filtered as tx, i}
<TxView
{tx}
{viewlets}
isNew={newTxPos >= i && newTxPos !== -1}
isNextNew={newTxPos > i && newTxPos !== -1}
/>
{/each}
</Grid>
{/if}
</div>
</Scroller>
{#if showCommenInput}
<div class="ref-input">
<Component is={chunter.component.CommentInput} props={{ object }} />
<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}
<div class="ml-1">
<Spinner size={'small'} />
</div>
{/if}
</div>
{:else}
<!-- MODERN -->
<slot />
<!-- <div class="antiDivider" style:margin={'1rem -1.5rem'} /> -->
<div class="antiSection-header high mt-9">
<span class="antiSection-header__title flex-row-center">
<Label label={activity.string.Activity} />
{#if loading}
<div class="ml-1">
<Spinner size={'small'} />
</div>
{/if}
</span>
{#if selectedFilter === 'All'}
<div class="antiSection-header__tag highlight">
<Label label={activityPlg.string.All} />
</div>
{:else}
{#each labels as label}
<div class="antiSection-header__tag overflow-label">
<Label {label} />
<div class="tag-icon">
<Icon icon={IconClose} size={'small'} />
</div>
</div>
</span>
<ActivityFilter {txes} {object} on:update={(e) => (filtered = e.detail)} />
</div>
<div class="p-activity select-text" id={activity.string.Activity}>
{#if filtered}
<Grid column={1} rowGap={0.75}>
{#each filtered as tx, i}
<TxView {tx} {viewlets} isNew={newTxPos >= i && newTxPos !== -1} isNextNew={newTxPos > i && newTxPos !== -1} />
{/each}
{/if}
<div class="w-4 min-w-4 max-w-4" />
<ActionIcon icon={IconFilter} size={'medium'} action={handleOptions} />
</div>
<div class="p-activity select-text" id={activity.string.Activity}>
{#if filtered}
<Grid column={1} rowGap={0.75}>
{#each filtered as tx, i}
<TxView
{tx}
{viewlets}
isNew={newTxPos >= i && newTxPos !== -1}
isNextNew={newTxPos > i && newTxPos !== -1}
/>
{/each}
</Grid>
{/if}
</div>
{#if showCommenInput}
<div class="ref-input">
<Component is={chunter.component.CommentInput} props={{ object }} />
</div>
</Grid>
{/if}
</div>
{#if showCommenInput}
<div class="ref-input">
<Component is={chunter.component.CommentInput} props={{ object }} />
</div>
{/if}
<style lang="scss">
.icon {
margin-left: 1rem;
height: 2rem;
color: var(--caption-color);
}
.ref-input {
flex-shrink: 0;
margin-top: 1.75rem;
@ -259,6 +129,9 @@
.p-activity {
margin-top: 1.75rem;
}
.invisible {
display: none;
}
:global(.grid .msgactivity-container.showIcon:last-child::after) {
content: none;

View File

@ -0,0 +1,130 @@
<!--
// 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 activity, { ActivityFilter, DisplayTx } from '@hcengineering/activity'
import { Class, Doc, Ref } from '@hcengineering/core'
import { IntlString, getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { ActionIcon, AnyComponent, Icon, Label, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import activityPlg 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 object: Doc
let filtered: DisplayTx[]
const client = getClient()
let filters: ActivityFilter[] = []
const saved = localStorage.getItem('activity-filter')
let selectedFilter: Ref<Doc>[] | 'All' =
saved !== null && saved !== undefined ? (JSON.parse(saved) as Ref<Doc>[] | 'All') : 'All'
$: localStorage.setItem('activity-filter', JSON.stringify(selectedFilter))
client.findAll(activity.class.ActivityFilter, {}).then((res) => {
filters = res
if (saved !== null && saved !== undefined) {
const temp: Ref<Doc>[] | 'All' = JSON.parse(saved)
if (temp !== 'All' && Array.isArray(temp)) {
selectedFilter = temp.filter((it) => filters.findIndex((f) => it === f._id) > -1)
if ((selectedFilter as Ref<Doc>[]).length === 0) {
selectedFilter = 'All'
}
} else {
selectedFilter = 'All'
}
}
})
function getAdditionalComponent (_class: Ref<Class<Doc>>): AnyComponent | undefined {
const hierarchy = client.getHierarchy()
const mixin = hierarchy.classHierarchyMixin(_class, activity.mixin.ExtraActivityComponent)
if (mixin !== undefined) {
return mixin.component
}
}
let extraComponent = getAdditionalComponent(object._class)
$: extraComponent = getAdditionalComponent(object._class)
const handleOptions = (ev: MouseEvent) => {
showPopup(
FilterPopup,
{ selectedFilter, filters },
eventToHTMLElement(ev),
() => {},
(res) => {
if (res === undefined) return
if (res.action === 'select') selectedFilter = res.value as Ref<Doc>[] | 'All'
}
)
}
let labels: IntlString[] = []
const dispatch = createEventDispatcher()
async function updateFilterActions (
txes: DisplayTx[],
filters: ActivityFilter[],
selected: Ref<Doc>[] | 'All'
): Promise<void> {
if (selected === 'All') {
filtered = txes
if (extraComponent === undefined) {
dispatch('update', filtered)
}
} else {
const selectedFilters = filters.filter((filter) => selected.includes(filter._id))
const filterActions: ((tx: DisplayTx, _class?: Ref<Doc>) => boolean)[] = []
labels = []
for (const filter of selectedFilters) {
labels.push(filter.label)
const fltr = await getResource(filter.filter)
filterActions.push(fltr)
}
filtered = txes.filter((it) => filterActions.some((f) => f(it, object._class)))
if (extraComponent === undefined) {
dispatch('update', filtered)
}
}
}
$: updateFilterActions(txes, filters, selectedFilter)
</script>
{#if selectedFilter === 'All'}
<div class="antiSection-header__tag highlight">
<Label label={activityPlg.string.All} />
</div>
{:else}
{#each labels as label}
<div class="antiSection-header__tag overflow-label">
<Label {label} />
<div class="tag-icon">
<Icon icon={IconClose} size={'small'} />
</div>
</div>
{/each}
{/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}

View File

@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import type { TxViewlet } from '@hcengineering/activity'
import type { DisplayTx, TxViewlet } from '@hcengineering/activity'
import contact, { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import core, { AnyAttribute, Doc, getCurrentAccount, Ref, Class } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
@ -34,7 +34,7 @@
import attachment from '@hcengineering/attachment'
import chunter from '@hcengineering/chunter'
import { Menu, ObjectPresenter } from '@hcengineering/view-resources'
import { ActivityKey, DisplayTx } from '../activity'
import { ActivityKey } from '../activity'
import activity from '../plugin'
import { getValue, TxDisplayViewlet, updateViewlet } from '../utils'
import TxViewTx from './TxViewTx.svelte'
@ -206,9 +206,9 @@
<span class="lower">
<Label label={viewlet.label} params={viewlet.labelParams ?? {}} />
</span>
{#if viewlet.labelComponent}
<Component is={viewlet.labelComponent} {props} />
{/if}
{/if}
{#if viewlet && viewlet.labelComponent}
<Component is={viewlet.labelComponent} {props} />
{/if}
{#if viewlet === undefined && model.length > 0 && tx.updateTx}

View File

@ -1,8 +1,8 @@
<script lang="ts">
import core, { Class, Doc, Ref } from '@hcengineering/core'
import { Component, IconAdd, IconDelete } from '@hcengineering/ui'
import { DisplayTx } from '../activity'
import { getDTxProps, TxDisplayViewlet } from '../utils'
import { DisplayTx } from '@hcengineering/activity'
export let tx: DisplayTx
export let viewlet: TxDisplayViewlet

View File

@ -1,4 +1,4 @@
import type { TxViewlet } from '@hcengineering/activity'
import type { DisplayTx, TxViewlet } from '@hcengineering/activity'
import core, {
AttachedDoc,
Class,
@ -17,7 +17,7 @@ import { Asset, IntlString, translate } from '@hcengineering/platform'
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view'
import { buildModel, getObjectPresenter } from '@hcengineering/view-resources'
import { ActivityKey, activityKey, DisplayTx } from './activity'
import { ActivityKey, activityKey } from './activity'
import activity from './plugin'
const valueTypes: ReadonlyArray<Ref<Class<Doc>>> = [

View File

@ -100,6 +100,13 @@ export interface ActivityFilter extends Doc {
filter: Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>
}
/**
* @public
*/
export interface ExtraActivityComponent extends Class<Doc> {
component: AnyComponent
}
/**
* @public
*/
@ -121,6 +128,9 @@ export default plugin(activityId, {
From: '' as IntlString,
Removed: '' as IntlString
},
mixin: {
ExtraActivityComponent: '' as Ref<Class<ExtraActivityComponent>>
},
class: {
TxViewlet: '' as Ref<Class<TxViewlet>>,
ActivityFilter: '' as Ref<Class<ActivityFilter>>

View File

@ -45,6 +45,7 @@
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/notification": "^0.6.8",
"@hcengineering/notification-resources": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/attachment": "^0.6.1",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/view-resources": "^0.6.0",

View File

@ -13,6 +13,7 @@
// limitations under the License.
//
import { DisplayTx } from '@hcengineering/activity'
import chunter, {
Backlink,
Channel,
@ -22,16 +23,12 @@ import chunter, {
Message,
ThreadMessage
} from '@hcengineering/chunter'
import core, { Data, Doc, DocumentQuery, getCurrentAccount, Ref, RelatedDocument, Space } from '@hcengineering/core'
import core, { Data, Doc, DocumentQuery, Ref, RelatedDocument, Space, getCurrentAccount } from '@hcengineering/core'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { IntlString, Resources, translate } from '@hcengineering/platform'
import preference from '@hcengineering/preference'
import { getClient, MessageBox } from '@hcengineering/presentation'
import { MessageBox, getClient } from '@hcengineering/presentation'
import { location, navigate, showPopup } from '@hcengineering/ui'
import TxBacklinkCreate from './components/activity/TxBacklinkCreate.svelte'
import TxBacklinkReference from './components/activity/TxBacklinkReference.svelte'
import TxCommentCreate from './components/activity/TxCommentCreate.svelte'
import TxMessageCreate from './components/activity/TxMessageCreate.svelte'
import ChannelHeader from './components/ChannelHeader.svelte'
import ChannelPresenter from './components/ChannelPresenter.svelte'
import ChannelView from './components/ChannelView.svelte'
@ -41,25 +38,28 @@ import CommentInput from './components/CommentInput.svelte'
import CommentPopup from './components/CommentPopup.svelte'
import CommentPresenter from './components/CommentPresenter.svelte'
import CommentsPresenter from './components/CommentsPresenter.svelte'
import MessagePresenter from './components/MessagePresenter.svelte'
import ThreadParentPresenter from './components/ThreadParentPresenter.svelte'
import ThreadViewPanel from './components/ThreadViewPanel.svelte'
import ConvertDmToPrivateChannelModal from './components/ConvertDmToPrivateChannel.svelte'
import CreateChannel from './components/CreateChannel.svelte'
import CreateDirectMessage from './components/CreateDirectMessage.svelte'
import DirectMessagePresenter from './components/DirectMessagePresenter.svelte'
import DmHeader from './components/DmHeader.svelte'
import DmPresenter from './components/DmPresenter.svelte'
import EditChannel from './components/EditChannel.svelte'
import DirectMessagePresenter from './components/DirectMessagePresenter.svelte'
import MessagePresenter from './components/MessagePresenter.svelte'
import SavedMessages from './components/SavedMessages.svelte'
import Threads from './components/Threads.svelte'
import ThreadParentPresenter from './components/ThreadParentPresenter.svelte'
import ThreadView from './components/ThreadView.svelte'
import ThreadViewPanel from './components/ThreadViewPanel.svelte'
import Threads from './components/Threads.svelte'
import TxBacklinkCreate from './components/activity/TxBacklinkCreate.svelte'
import TxBacklinkReference from './components/activity/TxBacklinkReference.svelte'
import TxCommentCreate from './components/activity/TxCommentCreate.svelte'
import TxMessageCreate from './components/activity/TxMessageCreate.svelte'
import { get, writable } from 'svelte/store'
import { DisplayTx } from '../../activity/lib'
import { updateBacklinksList } from './backlinks'
import { getDmName, getTitle, getLink, resolveLocation } from './utils'
import notification from '@hcengineering/notification'
import { get, writable } from 'svelte/store'
import { updateBacklinksList } from './backlinks'
import { getDmName, getLink, getTitle, resolveLocation } from './utils'
export { default as Header } from './components/Header.svelte'
export { classIcon } from './utils'

View File

@ -37,6 +37,8 @@
"@hcengineering/contact": "^0.6.11",
"@hcengineering/ui": "^0.6.3",
"@hcengineering/setting": "^0.6.3",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/activity-resources": "^0.6.1",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/notification": "^0.6.8",
"@hcengineering/core": "^0.6.21",

View File

@ -0,0 +1,64 @@
<!--
// 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 { DisplayTx } from '@hcengineering/activity'
import core, { AttachedDoc, Doc, TxCUD, TxCollectionCUD, TxProcessor } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import contact from '../../plugin'
import { Channel, Contact } from '@hcengineering/contact'
import { newDisplayTx } from '@hcengineering/activity-resources'
import { createEventDispatcher } from 'svelte'
export let object: Contact
export let filtered: DisplayTx[]
let channels: Channel[] = []
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
const query = createQuery()
$: query.query(
contact.class.Channel,
{
attachedTo: object._id,
provider: contact.channelProvider.Email
},
(res) => {
channels = res
}
)
const txQuery = createQuery()
$: txQuery.query(
core.class.TxCollectionCUD,
{
objectId: { $in: channels.map((p) => p._id) },
'tx._class': core.class.TxCreateDoc,
'tx.attributes.incoming': false
},
(res) => {
const newTxes = createDisplayTxes(res)
const result = filtered.concat(newTxes).sort((a, b) => a.tx.modifiedOn - b.tx.modifiedOn)
dispatch('update', result)
}
)
function createDisplayTxes (txes: TxCollectionCUD<Doc, AttachedDoc>[]): DisplayTx[] {
return txes.map((p) => newDisplayTx(TxProcessor.extractTx(p) as TxCUD<Doc>, hierarchy))
}
</script>

View File

@ -17,11 +17,12 @@
import { Channel, Contact, Employee, getGravatarUrl, getName } from '@hcengineering/contact'
import { Class, Client, DocumentQuery, Ref, RelatedDocument, WithLookup } from '@hcengineering/core'
import login from '@hcengineering/login'
import { getResource, IntlString, Resources } from '@hcengineering/platform'
import { getClient, getFileUrl, MessageBox, ObjectSearchResult } from '@hcengineering/presentation'
import { IntlString, Resources, getResource } from '@hcengineering/platform'
import { MessageBox, ObjectSearchResult, getClient, getFileUrl } from '@hcengineering/presentation'
import { AnyComponent, AnySvelteComponent, showPopup } from '@hcengineering/ui'
import AccountArrayEditor from './components/AccountArrayEditor.svelte'
import AccountBox from './components/AccountBox.svelte'
import AssigneeBox from './components/AssigneeBox.svelte'
import Avatar from './components/Avatar.svelte'
import ChannelFilter from './components/ChannelFilter.svelte'
import Channels from './components/Channels.svelte'
@ -29,6 +30,7 @@ import ChannelsDropdown from './components/ChannelsDropdown.svelte'
import ChannelsEditor from './components/ChannelsEditor.svelte'
import ChannelsPresenter from './components/ChannelsPresenter.svelte'
import ChannelsView from './components/ChannelsView.svelte'
import CombineAvatars from './components/CombineAvatars.svelte'
import ContactArrayEditor from './components/ContactArrayEditor.svelte'
import ContactPresenter from './components/ContactPresenter.svelte'
import ContactRefPresenter from './components/ContactRefPresenter.svelte'
@ -37,19 +39,19 @@ import ContactsTabs from './components/ContactsTabs.svelte'
import CreateEmployee from './components/CreateEmployee.svelte'
import CreateOrganization from './components/CreateOrganization.svelte'
import CreatePerson from './components/CreatePerson.svelte'
import EditableAvatar from './components/EditableAvatar.svelte'
import EditEmployee from './components/EditEmployee.svelte'
import EditMember from './components/EditMember.svelte'
import EditOrganization from './components/EditOrganization.svelte'
import EditPerson from './components/EditPerson.svelte'
import EditableAvatar from './components/EditableAvatar.svelte'
import EmployeeAccountPresenter from './components/EmployeeAccountPresenter.svelte'
import EmployeeAccountRefPresenter from './components/EmployeeAccountRefPresenter.svelte'
import EmployeeArrayEditor from './components/EmployeeArrayEditor.svelte'
import EmployeeBox from './components/EmployeeBox.svelte'
import EmployeeBrowser from './components/EmployeeBrowser.svelte'
import EmployeeEditor from './components/EmployeeEditor.svelte'
import EmployeePresenter from './components/EmployeePresenter.svelte'
import EmployeeRefPresenter from './components/EmployeeRefPresenter.svelte'
import ExpandRightDouble from './components/icons/ExpandRightDouble.svelte'
import MemberPresenter from './components/MemberPresenter.svelte'
import Members from './components/Members.svelte'
import MembersPresenter from './components/MembersPresenter.svelte'
@ -60,14 +62,13 @@ import PersonEditor from './components/PersonEditor.svelte'
import PersonPresenter from './components/PersonPresenter.svelte'
import PersonRefPresenter from './components/PersonRefPresenter.svelte'
import SocialEditor from './components/SocialEditor.svelte'
import UserInfo from './components/UserInfo.svelte'
import UserBox from './components/UserBox.svelte'
import AssigneeBox from './components/AssigneeBox.svelte'
import UsersPopup from './components/UsersPopup.svelte'
import EmployeeBox from './components/EmployeeBox.svelte'
import UserBoxList from './components/UserBoxList.svelte'
import SpaceMembers from './components/SpaceMembers.svelte'
import CombineAvatars from './components/CombineAvatars.svelte'
import UserBox from './components/UserBox.svelte'
import UserBoxList from './components/UserBoxList.svelte'
import UserInfo from './components/UserInfo.svelte'
import UsersPopup from './components/UsersPopup.svelte'
import ActivityChannelMessage from './components/activity/ActivityChannelMessage.svelte'
import ExpandRightDouble from './components/icons/ExpandRightDouble.svelte'
import IconMembers from './components/icons/Members.svelte'
import contact from './plugin'
@ -258,7 +259,8 @@ export default async (): Promise<Resources> => ({
ChannelFilter,
MergeEmployee,
Avatar,
UserBoxList
UserBoxList,
ActivityChannelMessage
},
completion: {
EmployeeQuery: async (

View File

@ -32,6 +32,7 @@
"EmailPlaceholder": "john.appleseed@apple.com",
"WriteEmail": "Write Email",
"Shared": "Shared",
"AvailableTo": "Available to"
"AvailableTo": "Available to",
"HaveWrittenEmail": "have written an"
}
}

View File

@ -32,6 +32,7 @@
"EmailPlaceholder": "john.appleseed@apple.com",
"WriteEmail": "Написать Email",
"Shared": "Публичный",
"AvailableTo": "Доступен для"
"AvailableTo": "Доступен для",
"HaveWrittenEmail": "написал(а)"
}
}

View File

@ -14,13 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import contact, {
Channel,
Contact,
Employee,
EmployeeAccount,
getName as getContactName
} from '@hcengineering/contact'
import contact, { Channel, Contact, EmployeeAccount } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { IdMap, Ref, SortingOrder, toIdMap } from '@hcengineering/core'
import { Message, SharedMessage } from '@hcengineering/gmail'
@ -28,17 +22,15 @@
import { createQuery, getClient } from '@hcengineering/presentation'
import plugin, { Button, Icon, IconShare, Label, Scroller } from '@hcengineering/ui'
import gmail from '../plugin'
import IconInbox from './icons/Inbox.svelte'
import { convertMessages } from '../utils'
import Messages from './Messages.svelte'
import IconInbox from './icons/Inbox.svelte'
export let object: Contact
export let channel: Channel
export let newMessage: boolean
export let enabled: boolean
const EMAIL_REGEX =
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/
let messages: Message[] = []
let accounts: IdMap<EmployeeAccount> = new Map()
let selected: Set<Ref<SharedMessage>> = new Set<Ref<SharedMessage>>()
@ -76,7 +68,7 @@
object._class,
'gmailSharedMessages',
{
messages: convertMessages(selectedMessages, accounts, $employeeByIdStore)
messages: convertMessages(object, channel, selectedMessages, accounts, $employeeByIdStore)
}
)
await notificationClient.updateLastView(channel._id, channel._class, undefined, true)
@ -88,39 +80,6 @@
selected.clear()
selected = selected
}
function convertMessages (
messages: Message[],
accounts: IdMap<EmployeeAccount>,
employees: IdMap<Employee>
): SharedMessage[] {
return messages.map((m) => {
return {
...m,
_id: m._id as string as Ref<SharedMessage>,
sender: getName(m, accounts, employees, true),
receiver: getName(m, accounts, employees, false)
}
})
}
function getName (
message: Message,
accounts: IdMap<EmployeeAccount>,
employees: IdMap<Employee>,
sender: boolean
): string {
if (message.incoming === sender) {
return `${getContactName(object)} (${channel.value})`
} else {
const account = accounts.get(message.modifiedBy as Ref<EmployeeAccount>)
const emp = account ? employees.get(account?.employee) : undefined
const value = message.incoming ? message.to : message.from
const email = value.match(EMAIL_REGEX)
const emailVal = email?.[0] ?? value
return emp ? `${getContactName(emp)} (${emailVal})` : emailVal
}
}
</script>
<div class="popupPanel-body__main-header bottom-divider p-2">
@ -167,7 +126,7 @@
<div class="popupPanel-body__main-content py-4 clear-mins flex-no-shrink">
{#if messages && messages.length > 0}
<Messages
messages={convertMessages(messages, accounts, $employeeByIdStore)}
messages={convertMessages(object, channel, messages, accounts, $employeeByIdStore)}
{selectable}
bind:selected
on:select

View File

@ -16,7 +16,7 @@
<script lang="ts">
import contact, { Channel, Contact, getName } from '@hcengineering/contact'
import { Class, getCurrentAccount, Ref } from '@hcengineering/core'
import { SharedMessage } from '@hcengineering/gmail'
import { Message, SharedMessage } from '@hcengineering/gmail'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { getResource } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
@ -30,14 +30,17 @@
import FullMessage from './FullMessage.svelte'
import IntegrationSelector from './IntegrationSelector.svelte'
import NewMessage from './NewMessage.svelte'
import { convertMessage } from '../utils'
import { employeeByIdStore } from '@hcengineering/contact-resources'
export let _id: Ref<Contact>
export let _class: Ref<Class<Contact>>
export let message: Message | undefined = undefined
let object: Contact
let currentMessage: SharedMessage | undefined = undefined
let newMessage: boolean = false
let currentMessage: SharedMessage | undefined = undefined
let channel: Channel | undefined = undefined
const notificationClient = NotificationClientImpl.getClient()
let integrations: Integration[] = []
@ -97,6 +100,11 @@
integrations = res.filter((p) => p.createdBy === me || p.shared?.includes(me))
selectedIntegration = integrations.find((p) => p.createdBy === me) ?? integrations[0]
})
$: message &&
channel &&
object &&
convertMessage(object, channel, message, $employeeByIdStore).then((p) => (currentMessage = p))
</script>
{#if channel && object}

View File

@ -0,0 +1,33 @@
<!--
// 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 { Message } from '@hcengineering/gmail'
import { getClient } from '@hcengineering/presentation'
import { Label, showPopup } from '@hcengineering/ui'
import gmail from '../../plugin'
import Main from '../Main.svelte'
export let value: Message
async function click () {
const client = getClient()
const channel = await client.findOne(value.attachedToClass, { _id: value.attachedTo })
if (channel !== undefined) {
showPopup(Main, { _id: channel.attachedTo, _class: channel.attachedToClass, message: value }, 'float')
}
}
</script>
<span class="over-underline" on:click={click}><Label label={gmail.string.Email} /></span>

View File

@ -18,6 +18,7 @@ import { concatLink } from '@hcengineering/core'
import { getMetadata, Resources } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import TxSharedCreate from './components/activity/TxSharedCreate.svelte'
import TxWriteMessage from './components/activity/TxWriteMessage.svelte'
import Configure from './components/Configure.svelte'
import Connect from './components/Connect.svelte'
import IconGmail from './components/icons/GmailColor.svelte'
@ -35,7 +36,8 @@ export default async (): Promise<Resources> => ({
Configure
},
activity: {
TxSharedCreate
TxSharedCreate,
TxWriteMessage
},
function: {
HasEmail: checkHasEmail

View File

@ -41,6 +41,8 @@ export default mergeIds(gmailId, gmail, {
CopyPlaceholder: '' as IntlString,
WriteEmail: '' as IntlString,
Shared: '' as IntlString,
AvailableTo: '' as IntlString
AvailableTo: '' as IntlString,
Email: '' as IntlString,
HaveWrittenEmail: '' as IntlString
}
})

View File

@ -1,5 +1,6 @@
import contact from '@hcengineering/contact'
import { Doc } from '@hcengineering/core'
import contact, { Channel, Contact, Employee, EmployeeAccount, getName as getContactName } from '@hcengineering/contact'
import { Doc, IdMap, Ref, toIdMap } from '@hcengineering/core'
import { Message, SharedMessage } from '@hcengineering/gmail'
import { getClient } from '@hcengineering/presentation'
export function getTime (time: number): string {
@ -56,3 +57,59 @@ export async function checkHasEmail (doc: Doc | Doc[] | undefined): Promise<bool
}
return true
}
const EMAIL_REGEX =
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/
export function convertMessages (
object: Contact,
channel: Channel,
messages: Message[],
accounts: IdMap<EmployeeAccount>,
employees: IdMap<Employee>
): SharedMessage[] {
return messages.map((m) => {
return {
...m,
_id: m._id as string as Ref<SharedMessage>,
sender: getName(object, channel, m, accounts, employees, true),
receiver: getName(object, channel, m, accounts, employees, false)
}
})
}
export async function convertMessage (
object: Contact,
channel: Channel,
message: Message,
employees: IdMap<Employee>
): Promise<SharedMessage> {
const client = getClient()
const accounts = toIdMap(await client.findAll(contact.class.EmployeeAccount, {}))
return {
...message,
_id: message._id as string as Ref<SharedMessage>,
sender: getName(object, channel, message, accounts, employees, true),
receiver: getName(object, channel, message, accounts, employees, false)
}
}
export function getName (
object: Contact,
channel: Channel,
message: Message,
accounts: IdMap<EmployeeAccount>,
employees: IdMap<Employee>,
sender: boolean
): string {
if (message.incoming === sender) {
return `${getContactName(object)} (${channel.value})`
} else {
const account = accounts.get(message.modifiedBy as Ref<EmployeeAccount>)
const emp = account != null ? employees.get(account?.employee) : undefined
const value = message.incoming ? message.to : message.from
const email = value.match(EMAIL_REGEX)
const emailVal = email?.[0] ?? value
return emp != null ? `${getContactName(emp)} (${emailVal})` : emailVal
}
}

View File

@ -13,10 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import { TxViewlet } from '@hcengineering/activity'
import { DisplayTx, TxViewlet } from '@hcengineering/activity'
import {
ActivityKey,
DisplayTx,
getValue,
newDisplayTx,
TxDisplayViewlet,

View File

@ -13,11 +13,11 @@
// limitations under the License.
-->
<script lang="ts">
import { TxViewlet } from '@hcengineering/activity'
import { ActivityKey, DisplayTx, getValue, newDisplayTx, updateViewlet } from '@hcengineering/activity-resources'
import { DisplayTx, TxViewlet } from '@hcengineering/activity'
import { ActivityKey, getValue, newDisplayTx, updateViewlet } from '@hcengineering/activity-resources'
import activity from '@hcengineering/activity-resources/src/plugin'
import contact, { EmployeeAccount } from '@hcengineering/contact'
import core, { AnyAttribute, Ref, Tx } from '@hcengineering/core'
import core, { AnyAttribute, Doc, Ref, Tx, TxCUD } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
@ -37,7 +37,7 @@
let modelIcon: Asset | undefined = undefined
$: if (tx._id !== ptx?.tx._id) {
ptx = newDisplayTx(tx, client.getHierarchy())
ptx = newDisplayTx(tx as TxCUD<Doc>, client.getHierarchy())
if (tx.modifiedBy !== employee?._id) {
employee = undefined
}