mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-23 18:15:03 +02:00
Init directs (#9715)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
+1
-1
Submodule communication updated: f7d1b98df4...407c52ca43
@@ -19,6 +19,8 @@ import {
|
||||
type CardSection,
|
||||
type CardSpace,
|
||||
type CardViewDefaults,
|
||||
type CreateCardExtension,
|
||||
type CanCreateCardResource,
|
||||
DOMAIN_CARD,
|
||||
type FavoriteCard,
|
||||
type MasterTag,
|
||||
@@ -168,6 +170,12 @@ export class TFavoriteCard extends TPreference implements FavoriteCard {
|
||||
application!: string
|
||||
}
|
||||
|
||||
@Mixin(card.mixin.CreateCardExtension, card.class.MasterTag)
|
||||
export class TCreateCardExtension extends TMasterTag implements CreateCardExtension {
|
||||
component?: AnyComponent
|
||||
canCreate?: CanCreateCardResource
|
||||
}
|
||||
|
||||
export * from './migration'
|
||||
|
||||
const listConfig: (BuildModelKey | string)[] = [
|
||||
@@ -337,7 +345,8 @@ export function createModel (builder: Builder): void {
|
||||
TRole,
|
||||
TCardSection,
|
||||
TCardViewDefaults,
|
||||
TFavoriteCard
|
||||
TFavoriteCard,
|
||||
TCreateCardExtension
|
||||
)
|
||||
|
||||
defineTabs(builder)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { type Builder, Model, TypeAny, TypeNumber } from '@hcengineering/model'
|
||||
import { ArrOf, type Builder, Model, TypeAny, TypeNumber, TypeRef } from '@hcengineering/model'
|
||||
import core, { TAttachedDoc, TConfiguration, TDoc } from '@hcengineering/model-core'
|
||||
import { type Class, type Domain, DOMAIN_MODEL, type Ref } from '@hcengineering/core'
|
||||
import { type Asset, type IntlString } from '@hcengineering/platform'
|
||||
@@ -25,17 +25,19 @@ import {
|
||||
type Poll,
|
||||
type CustomActivityPresenter,
|
||||
type GuestCommunicationSettings,
|
||||
type AppletGetTitleFnResource
|
||||
type AppletGetTitleFnResource,
|
||||
MessagesNavigationAnchors
|
||||
} from '@hcengineering/communication'
|
||||
import { PaletteColorIndexes } from '@hcengineering/ui/src/colors'
|
||||
import { type AppletType } from '@hcengineering/communication-types'
|
||||
import { createSystemType } from '@hcengineering/model-card'
|
||||
import card, { createSystemType } from '@hcengineering/model-card'
|
||||
import type { AnyComponent } from '@hcengineering/ui'
|
||||
import { type PersonSpace } from '@hcengineering/contact'
|
||||
|
||||
import communication from './plugin'
|
||||
import contact, { type PersonSpace } from '@hcengineering/contact'
|
||||
import { type Card, type MasterTag } from '@hcengineering/card'
|
||||
import { DOMAIN_SETTING } from '@hcengineering/setting'
|
||||
import view from '@hcengineering/model-view'
|
||||
|
||||
import communication from './plugin'
|
||||
|
||||
export const DOMAIN_POLL = 'poll' as Domain
|
||||
|
||||
@@ -86,6 +88,48 @@ export class TGuestCommunicationSettings extends TConfiguration implements Guest
|
||||
export function buildTypes (builder: Builder): void {
|
||||
builder.createModel(TMessageAction, TApplet, TPollAnswer, TCustomActivityPresenter, TGuestCommunicationSettings)
|
||||
|
||||
defineDirect(builder)
|
||||
definePoll(builder)
|
||||
}
|
||||
|
||||
function defineDirect (builder: Builder): void {
|
||||
createSystemType(
|
||||
builder,
|
||||
communication.type.Direct,
|
||||
contact.icon.Contacts,
|
||||
communication.string.Direct,
|
||||
communication.string.Directs,
|
||||
{
|
||||
defaultSection: communication.ids.CardMessagesSection,
|
||||
defaultNavigation: MessagesNavigationAnchors.LatestMessages
|
||||
},
|
||||
PaletteColorIndexes.Lavander
|
||||
)
|
||||
|
||||
builder.createDoc(core.class.Attribute, core.space.Model, {
|
||||
name: 'members',
|
||||
readonly: true, // TODO: remove
|
||||
attributeOf: communication.type.Direct,
|
||||
type: ArrOf(TypeRef(contact.class.Person)),
|
||||
label: communication.string.Members
|
||||
})
|
||||
|
||||
builder.mixin(communication.type.Direct, core.class.Class, view.mixin.ObjectIcon, {
|
||||
component: communication.component.DirectIcon
|
||||
})
|
||||
builder.mixin(communication.type.Direct, core.class.Class, card.mixin.CreateCardExtension, {
|
||||
component: communication.component.CreateDirect,
|
||||
canCreate: communication.function.CanCreateDirect,
|
||||
disableTitle: true,
|
||||
hideSpace: true
|
||||
})
|
||||
|
||||
builder.mixin(communication.type.Direct, core.class.Class, view.mixin.IgnoreActions, {
|
||||
actions: [view.action.Delete]
|
||||
})
|
||||
}
|
||||
|
||||
function definePoll (builder: Builder): void {
|
||||
createSystemType(
|
||||
builder,
|
||||
communication.type.Poll,
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@hcengineering/model": "^0.6.11",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/card": "^0.6.0",
|
||||
"@hcengineering/communication": "^0.6.0",
|
||||
"@hcengineering/server-card": "^0.6.0",
|
||||
"@hcengineering/server-core": "^0.6.1"
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import core from '@hcengineering/core'
|
||||
import serverCore from '@hcengineering/server-core'
|
||||
import serverCard from '@hcengineering/server-card'
|
||||
import card from '@hcengineering/card'
|
||||
import communication from '@hcengineering/communication'
|
||||
|
||||
export { serverCardId } from '@hcengineering/server-card'
|
||||
|
||||
@@ -85,6 +86,24 @@ export function createModel (builder: Builder): void {
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverCard.trigger.OnDirectCreate,
|
||||
isAsync: false,
|
||||
txMatch: {
|
||||
_class: core.class.TxCreateDoc,
|
||||
objectClass: communication.type.Direct
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverCard.trigger.OnThreadCreate,
|
||||
isAsync: false,
|
||||
txMatch: {
|
||||
_class: core.class.TxCreateDoc,
|
||||
objectClass: card.class.Card
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverCard.trigger.OnCardUpdate,
|
||||
isAsync: true,
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
/* Dark Theme */
|
||||
.theme-dark {
|
||||
--global-ui-BackgroundColor: #A5BDFF0D;
|
||||
--global-ui-BackgroundColor: 1530720D;
|
||||
--global-ui-BorderColor: #A5BDFF1A;
|
||||
--global-ui-hover-BackgroundColor: #A5BDFF1A;
|
||||
--global-ui-active-BackgroundColor: #A5BDFF26;
|
||||
@@ -134,6 +134,8 @@
|
||||
--love-active-call-filter: blur(17px);
|
||||
|
||||
--global-offline-color: #d1d5de;
|
||||
|
||||
--avatar-counter-BackgroundColor: #141523
|
||||
}
|
||||
|
||||
/* Light Theme */
|
||||
@@ -229,4 +231,6 @@
|
||||
--love-active-call-transform: scaleY(0.3) scaleX(0.42);
|
||||
|
||||
--global-offline-color: #5A667E;
|
||||
|
||||
--avatar-counter-BackgroundColor: #F1F1F5;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
//
|
||||
// Copyright © 2022 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.
|
||||
//
|
||||
@@ -115,7 +115,7 @@
|
||||
background-color: var(--theme-navpanel-selected);
|
||||
border: 1px solid var(--global-subtle-ui-BorderColor);
|
||||
border-radius: var(--extra-small-BorderRadius);
|
||||
|
||||
|
||||
&.folder {
|
||||
background-color: var(--theme-statusbar-color);
|
||||
border-color: var(--global-surface-01-BorderColor);
|
||||
@@ -131,7 +131,7 @@
|
||||
color: var(--global-tertiary-TextColor);
|
||||
border-radius: var(--extra-small-BorderRadius);
|
||||
}
|
||||
&__tools {
|
||||
&__tools, &__actions {
|
||||
display: none;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
@@ -153,6 +153,11 @@
|
||||
&:hover {
|
||||
.hulyNavGroup-header__tools { display: flex; }
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.hulyNavGroup-header__actions { display: flex; }
|
||||
}
|
||||
|
||||
&.showMenu,
|
||||
&.highlighted,
|
||||
&.selected {
|
||||
@@ -392,7 +397,7 @@
|
||||
&__mobile {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
&__main {
|
||||
flex-grow: 2;
|
||||
flex-basis: 760px;
|
||||
@@ -474,7 +479,7 @@
|
||||
&__aside {
|
||||
width: 25%;
|
||||
min-width: var(--panel-aside-width);
|
||||
|
||||
|
||||
&.float {
|
||||
position: absolute;
|
||||
flex-direction: row;
|
||||
@@ -576,7 +581,7 @@
|
||||
width: 100%;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
.popupPanel-pageFooter {
|
||||
@include watermark;
|
||||
|
||||
|
||||
@@ -155,6 +155,11 @@
|
||||
<slot name="tools" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $$slots.actions}
|
||||
<div class="hulyNavGroup-header__actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if selected && type === 'nested-selectable'}
|
||||
<div class="hulyNavGroup-header__arrow"><IconOpenedArrow size={'small'} /></div>
|
||||
{/if}
|
||||
|
||||
@@ -116,8 +116,10 @@
|
||||
</button>
|
||||
{/if}
|
||||
{#if visibleIcon || (type === 'type-tag' && color)}
|
||||
<div class="hulyNavItem-icon" class:withBackground class:w-auto={iconSize === 'x-small'}>
|
||||
{#if type !== 'type-tag' && visibleIcon}
|
||||
<div class="hulyNavItem-icon relative" class:withBackground class:w-auto={iconSize === 'x-small'}>
|
||||
{#if $$slots.icon}
|
||||
<slot name="icon" />
|
||||
{:else if type !== 'type-tag' && visibleIcon}
|
||||
<Icon icon={visibleIcon} size={iconSize} {iconProps} />
|
||||
{:else if type === 'type-tag'}
|
||||
<div style:background-color={color} class="hulyNavItem-icon__tag" />
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"Labels": "Štítky",
|
||||
"Properties": "Vlastnosti",
|
||||
"NoChildren": "Žádné děti",
|
||||
"ConfigDescription": "Rozšíření pro správu znalostí ve stylu databáze."
|
||||
"ConfigDescription": "Rozšíření pro správu znalostí ve stylu databáze.",
|
||||
"AddCollaborators": "Přidat spolupracovníky"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"Labels": "Labels",
|
||||
"Properties": "Eigenschaften",
|
||||
"NoChildren": "Keine Kinder",
|
||||
"ConfigDescription": "Erweiterung für Datenbank-basiertes Wissensmanagement."
|
||||
"ConfigDescription": "Erweiterung für Datenbank-basiertes Wissensmanagement.",
|
||||
"AddCollaborators": "Mitarbeiter hinzufügen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"Labels": "Labels",
|
||||
"Properties": "Properties",
|
||||
"NoChildren": "No children",
|
||||
"ConfigDescription": "Extension for database-style knowledge management."
|
||||
"ConfigDescription": "Extension for database-style knowledge management.",
|
||||
"AddCollaborators": "Add collaborators"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"Labels": "Etiquetas",
|
||||
"Properties": "Propiedades",
|
||||
"NoChildren": "No hay hijos",
|
||||
"ConfigDescription": "Extensión para la gestión del conocimiento al estilo de una base de datos."
|
||||
"ConfigDescription": "Extensión para la gestión del conocimiento al estilo de una base de datos.",
|
||||
"AddCollaborators": "Agregar colaboradores"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"Labels": "Étiquettes",
|
||||
"Properties": "Propriétés",
|
||||
"NoChildren": "Pas d'enfants",
|
||||
"ConfigDescription": "Extension pour la gestion des connaissances de style base de données."
|
||||
"ConfigDescription": "Extension pour la gestion des connaissances de style base de données.",
|
||||
"AddCollaborators": "Ajouter des collaborateurs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"Properties": "Proprietà",
|
||||
"NoChildren": "Nessun figlio",
|
||||
"NumberTypes": "{count, plural, one {# tipo} other {# tipi}}",
|
||||
"ConfigDescription": "Estensione per la gestione della conoscenza in stile database."
|
||||
"ConfigDescription": "Estensione per la gestione della conoscenza in stile database.",
|
||||
"AddCollaborators": "Aggiungi collaboratori"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"Labels": "ラベル",
|
||||
"Properties": "プロパティ",
|
||||
"NoChildren": "子がありません",
|
||||
"ConfigDescription": "データベーススタイルのナレッジマネジメント用の拡張機能。"
|
||||
"ConfigDescription": "データベーススタイルのナレッジマネジメント用の拡張機能。",
|
||||
"AddCollaborators": "コラボレーターを追加"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"Labels": "Etiquetas",
|
||||
"Properties": "Propriedades",
|
||||
"NoChildren": "Sem filhos",
|
||||
"ConfigDescription": "Extensão para gerenciamento de conhecimento no estilo de banco de dados."
|
||||
"ConfigDescription": "Extensão para gerenciamento de conhecimento no estilo de banco de dados.",
|
||||
"AddCollaborators": "Adicionar colaboradores"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"Labels": "Метки",
|
||||
"Properties": "Свойства",
|
||||
"NoChildren": "Нет потомков",
|
||||
"ConfigDescription": "Расширение для управления знаниями в стиле базы данных."
|
||||
"ConfigDescription": "Расширение для управления знаниями в стиле базы данных.",
|
||||
"AddCollaborators": "Добавить сотрудников"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"Labels": "标签",
|
||||
"Properties": "属性",
|
||||
"NoChildren": "没有子级",
|
||||
"ConfigDescription": "用于数据库风格知识管理的扩展。"
|
||||
"ConfigDescription": "用于数据库风格知识管理的扩展。",
|
||||
"AddCollaborators": "添加协作者"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<!--
|
||||
// Copyright © 2025 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 { Employee, Person } from '@hcengineering/contact'
|
||||
import { ButtonIcon, IconDelete, ModernButton, Scroller } from '@hcengineering/ui'
|
||||
import { employeeByIdStore, IconAddMember, UserDetails } from '@hcengineering/contact-resources'
|
||||
import { notEmpty, Ref } from '@hcengineering/core'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import card from '../plugin'
|
||||
|
||||
export let ids: Ref<Employee>[] = []
|
||||
export let disableRemoveFor: Ref<Person>[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: employees = ids.map((_id) => $employeeByIdStore.get(_id)).filter(notEmpty)
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="item" style:padding="var(--spacing-1_5)" class:withoutBorder={employees.length === 0}>
|
||||
<ModernButton
|
||||
label={card.string.AddCollaborators}
|
||||
icon={IconAddMember}
|
||||
iconSize="small"
|
||||
kind="secondary"
|
||||
size="small"
|
||||
on:click={() => dispatch('add')}
|
||||
/>
|
||||
</div>
|
||||
<Scroller>
|
||||
{#each employees as employee, index (employee._id)}
|
||||
<div class="item" class:withoutBorder={index === employees.length - 1}>
|
||||
<div class="item__content" class:disabled={disableRemoveFor.includes(employee._id)}>
|
||||
<UserDetails person={employee} showStatus />
|
||||
{#if !disableRemoveFor.includes(employee._id)}
|
||||
<div class="item__action">
|
||||
<ButtonIcon
|
||||
icon={IconDelete}
|
||||
size="small"
|
||||
on:click={() => {
|
||||
dispatch('remove', employee._id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Scroller>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1px;
|
||||
border-radius: 0.75rem;
|
||||
background: var(--global-ui-highlight-BackgroundColor);
|
||||
border: 1px solid var(--global-ui-BorderColor);
|
||||
max-height: 30rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: var(--spacing-0_75);
|
||||
border-bottom: 1px solid var(--global-ui-BorderColor);
|
||||
|
||||
&.withoutBorder {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.item__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-0_75);
|
||||
border-radius: var(--small-BorderRadius);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--global-ui-highlight-BackgroundColor);
|
||||
|
||||
.item__action {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item__action {
|
||||
visibility: hidden;
|
||||
|
||||
&:hover {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { Card } from '@hcengineering/card'
|
||||
import { getClient, createQuery } from '@hcengineering/presentation'
|
||||
import { Button, ButtonSize, Icon, IconSize, showPopup } from '@hcengineering/ui'
|
||||
import { Button, ButtonSize, Component, Icon, IconSize, showPopup } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { IconPicker } from '@hcengineering/view-resources'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
@@ -30,6 +30,7 @@
|
||||
export let editable: boolean = false
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const query = createQuery()
|
||||
|
||||
let doc: Card | undefined = value
|
||||
@@ -55,9 +56,12 @@
|
||||
}
|
||||
|
||||
$: iconData = getCardIconInfo(doc)
|
||||
$: iconMixin = doc ? hierarchy.classHierarchyMixin(doc._class, view.mixin.ObjectIcon) : undefined
|
||||
</script>
|
||||
|
||||
{#if editable}
|
||||
{#if iconMixin && iconMixin._id !== card.class.Card}
|
||||
<Component is={iconMixin.component} props={{ value: doc, size, editable }} />
|
||||
{:else if editable}
|
||||
<Button
|
||||
size={buttonSize}
|
||||
kind={'ghost'}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<!-- Copyright © 2025 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 card, { Card, CardSpace, MasterTag } from '@hcengineering/card'
|
||||
import presentation, { getClient, getCommunicationClient, SpaceSelector } from '@hcengineering/presentation'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import core, { Data, generateId, Ref, Markup, notEmpty } from '@hcengineering/core'
|
||||
import { getResource, translate, getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { Label, Modal, ModernEditbox, languageStore, showPopup, Component } from '@hcengineering/ui'
|
||||
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
|
||||
import { EmptyMarkup } from '@hcengineering/text'
|
||||
import { Employee, getCurrentEmployee } from '@hcengineering/contact'
|
||||
import { SelectUsersPopup, employeeByIdStore } from '@hcengineering/contact-resources'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
import { createCard } from '../utils'
|
||||
import CardCollaborators from './CardCollaborators.svelte'
|
||||
|
||||
export let type: Ref<MasterTag>
|
||||
export let space: CardSpace | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const communicationClient = getCommunicationClient()
|
||||
const me = getCurrentEmployee()
|
||||
const _id = generateId<Card>()
|
||||
|
||||
const extension = client
|
||||
.getModel()
|
||||
.findAllSync(card.mixin.CreateCardExtension, {})
|
||||
.find((it) => hierarchy.isDerived(type, it._id))
|
||||
|
||||
let data: Partial<Data<Card>> = {}
|
||||
let _space: Ref<CardSpace> | undefined = space?._id
|
||||
let description: Markup = EmptyMarkup
|
||||
let collaborators: Ref<Employee>[] = [me]
|
||||
|
||||
async function addCollaborators (): Promise<void> {
|
||||
const accounts = collaborators
|
||||
.filter((it) => it !== me)
|
||||
.map((it) => $employeeByIdStore.get(it)?.personUuid)
|
||||
.filter(notEmpty)
|
||||
|
||||
if (accounts.length > 0) {
|
||||
await communicationClient.addCollaborators(_id, type, accounts)
|
||||
}
|
||||
}
|
||||
|
||||
async function okAction (): Promise<void> {
|
||||
if (_space === undefined) return
|
||||
|
||||
if (extension?.canCreate) {
|
||||
const fn = await getResource(extension.canCreate)
|
||||
const res = await fn(_space, data)
|
||||
if (res === false) {
|
||||
dispatch('close')
|
||||
return
|
||||
} else if (typeof res === 'string') {
|
||||
dispatch('close', res)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await createCard(type, _space, data, description, _id)
|
||||
await addCollaborators()
|
||||
|
||||
dispatch('close', _id)
|
||||
}
|
||||
|
||||
function handleCancel (): void {
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
let label: string = ''
|
||||
|
||||
$: void updateLabel($languageStore)
|
||||
|
||||
async function updateLabel (lang: string): Promise<void> {
|
||||
const _clazz = hierarchy.getClass(type)
|
||||
const typeString = await translate(_clazz.label, {}, lang)
|
||||
const createString = await translate(presentation.string.Create, {}, lang)
|
||||
label = `${createString} ${typeString}`
|
||||
}
|
||||
|
||||
function openSelectUsersPopup (): void {
|
||||
showPopup(
|
||||
SelectUsersPopup,
|
||||
{
|
||||
okLabel: presentation.string.Ok,
|
||||
disableDeselectFor: [me],
|
||||
skipCurrentAccount: false,
|
||||
skipInactive: true,
|
||||
selected: collaborators,
|
||||
showStatus: true
|
||||
},
|
||||
'top',
|
||||
(result?: Ref<Employee>[]) => {
|
||||
if (result != null) {
|
||||
collaborators = result
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function handleChange (event: CustomEvent<{ data: Partial<Data<Card>>, space?: Ref<CardSpace> }>): void {
|
||||
data = {
|
||||
...data,
|
||||
...event.detail.data
|
||||
}
|
||||
|
||||
if (event.detail.space != null) {
|
||||
_space = event.detail.space
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
label={getEmbeddedLabel(label)}
|
||||
type="type-popup"
|
||||
width="large"
|
||||
okLabel={presentation.string.Create}
|
||||
{okAction}
|
||||
canSave={data.title != null && data.title.trim().length > 0 && _space != null}
|
||||
onCancel={handleCancel}
|
||||
on:close
|
||||
>
|
||||
<div class="hulyModal-content__titleGroup" style="padding: 0">
|
||||
<ModernEditbox
|
||||
bind:value={data.title}
|
||||
label={view.string.Title}
|
||||
size="large"
|
||||
kind="ghost"
|
||||
disabled={extension?.disableTitle ?? false}
|
||||
autoFocus={!(extension?.disableTitle ?? false)}
|
||||
/>
|
||||
|
||||
<AttachmentStyledBox
|
||||
objectId={_id}
|
||||
_class={type}
|
||||
space={_space ?? space?._id}
|
||||
alwaysEdit
|
||||
showButtons={false}
|
||||
bind:content={description}
|
||||
placeholder={core.string.Description}
|
||||
kind="indented"
|
||||
isScrollable={false}
|
||||
kitOptions={{ reference: true }}
|
||||
enableAttachments={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="hulyModal-content__settingsSet">
|
||||
{#if space == null && !(extension?.hideSpace ?? false)}
|
||||
<div class="hulyModal-content__settingsSet-line">
|
||||
<span class="label"><Label label={core.string.Space} /></span>
|
||||
<SpaceSelector
|
||||
_class={card.class.CardSpace}
|
||||
query={{ archived: false }}
|
||||
label={core.string.Space}
|
||||
bind:space={_space}
|
||||
focus={false}
|
||||
kind={'regular'}
|
||||
size={'large'}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<CardCollaborators
|
||||
ids={collaborators}
|
||||
disableRemoveFor={[me]}
|
||||
on:add={openSelectUsersPopup}
|
||||
on:remove={(ev) => {
|
||||
collaborators = collaborators.filter((id) => id !== ev.detail)
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if extension?.component}
|
||||
<Component is={extension.component} props={{ collaborators, data, space: _space }} on:change={handleChange} />
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
@@ -27,6 +27,7 @@
|
||||
import cardPlugin from '../../plugin'
|
||||
import { CardsNavigatorConfig } from '../../types'
|
||||
import { getCardIconInfo } from '../../utils'
|
||||
import { CardIcon } from '../../index'
|
||||
|
||||
export let type: Ref<MasterTag> | undefined = undefined
|
||||
export let card: Card
|
||||
@@ -110,6 +111,9 @@
|
||||
}}
|
||||
on:contextmenu
|
||||
>
|
||||
<svelte:fragment slot="icon">
|
||||
<CardIcon value={card} _id={card._id} size="small" editable={false} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
{#each actions as action}
|
||||
{#if action.icon}
|
||||
|
||||
@@ -15,15 +15,16 @@
|
||||
|
||||
<script lang="ts">
|
||||
import view from '@hcengineering/view'
|
||||
import { IconAdd, NavGroup, Action, NavItem } from '@hcengineering/ui'
|
||||
import { IconAdd, NavGroup, Action, NavItem, ButtonIcon, showPopup, languageStore } from '@hcengineering/ui'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { CardSpace, MasterTag } from '@hcengineering/card'
|
||||
import { IconWithEmoji, getClient } from '@hcengineering/presentation'
|
||||
import presentation, { IconWithEmoji, getClient } from '@hcengineering/presentation'
|
||||
import { translate, getEmbeddedLabel } from '@hcengineering/platform'
|
||||
|
||||
import type { NavigatorConfig } from '../../types'
|
||||
import cardPlugin from '../../plugin'
|
||||
import { createCard } from '../../utils'
|
||||
import CreateCardPopup from '../CreateCardPopup.svelte'
|
||||
|
||||
export let type: MasterTag
|
||||
export let level: number = -1
|
||||
@@ -35,24 +36,30 @@
|
||||
export let showIcon: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let activeAction: string | undefined = undefined
|
||||
|
||||
async function handleCreateCard (): Promise<void> {
|
||||
if (space === undefined) return
|
||||
const _id = await createCard(type._id, space._id)
|
||||
const card = await getClient().findOne(cardPlugin.class.Card, { _id })
|
||||
if (card === undefined) return
|
||||
dispatch('selectCard', card)
|
||||
showPopup(CreateCardPopup, { type: type._id, space }, 'center', async (result) => {
|
||||
if (result !== undefined) {
|
||||
const card = await getClient().findOne(cardPlugin.class.Card, { _id: result })
|
||||
if (card === undefined) return
|
||||
dispatch('selectCard', card)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getActions (): Action[] {
|
||||
async function getActions (lang: string): Promise<Action[]> {
|
||||
const result: Action[] = []
|
||||
const typeString = await translate(type.label, {}, lang)
|
||||
const createString = await translate(presentation.string.Create, {}, lang)
|
||||
|
||||
if (config.allowCreate === true && space !== undefined) {
|
||||
if (config.allowCreate === true) {
|
||||
result.push({
|
||||
id: 'create-card',
|
||||
label: cardPlugin.string.CreateCard,
|
||||
label: getEmbeddedLabel(`${createString} ${typeString}`),
|
||||
icon: IconAdd,
|
||||
action: async (): Promise<void> => {
|
||||
activeAction = 'create-card'
|
||||
await handleCreateCard()
|
||||
}
|
||||
})
|
||||
@@ -60,6 +67,11 @@
|
||||
|
||||
return result
|
||||
}
|
||||
let actions: Action[] = []
|
||||
|
||||
$: void getActions($languageStore).then((res) => {
|
||||
actions = res
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if level > -1}
|
||||
@@ -95,7 +107,7 @@
|
||||
isFold
|
||||
visible={active}
|
||||
type="selectable-header"
|
||||
actions={getActions()}
|
||||
{actions}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
@@ -104,6 +116,22 @@
|
||||
>
|
||||
<div class="mt-0-5" />
|
||||
<slot />
|
||||
<svelte:fragment slot="actions">
|
||||
{#each actions as action}
|
||||
<ButtonIcon
|
||||
icon={action.icon ?? view.icon.Edit}
|
||||
size="extra-small"
|
||||
kind="tertiary"
|
||||
pressed={activeAction === action.id}
|
||||
tooltip={{ label: action.label }}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
void action.action(action.props, e)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="visible" let:isOpen>
|
||||
<div class="mt-0-5" />
|
||||
<slot name="visible" {isOpen} />
|
||||
|
||||
@@ -99,6 +99,7 @@ export default mergeIds(cardId, card, {
|
||||
CreateSpace: '' as IntlString,
|
||||
NumberTypes: '' as IntlString,
|
||||
Properties: '' as IntlString,
|
||||
NoChildren: '' as IntlString
|
||||
NoChildren: '' as IntlString,
|
||||
AddCollaborators: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ interface BaseNavigatorConfig {
|
||||
types: Array<Ref<MasterTag>>
|
||||
groupBySpace?: boolean
|
||||
savedViews?: boolean
|
||||
allowCreate?: boolean // for now works only when groupBySpace is true
|
||||
allowCreate?: boolean
|
||||
preorder?: Array<{ type: Ref<MasterTag>, order: number }>
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ import core, {
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
fillDefaults,
|
||||
generateId,
|
||||
type Hierarchy,
|
||||
makeCollabId,
|
||||
type Markup,
|
||||
type MarkupBlobRef,
|
||||
type Ref,
|
||||
type RelatedDocument,
|
||||
@@ -28,7 +31,13 @@ import core, {
|
||||
type TxOperations,
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import { getClient, IconWithEmoji, MessageBox, type ObjectSearchResult } from '@hcengineering/presentation'
|
||||
import {
|
||||
createMarkup,
|
||||
getClient,
|
||||
IconWithEmoji,
|
||||
MessageBox,
|
||||
type ObjectSearchResult
|
||||
} from '@hcengineering/presentation'
|
||||
import {
|
||||
getCurrentResolvedLocation,
|
||||
getPanelURI,
|
||||
@@ -45,6 +54,7 @@ import { translate } from '@hcengineering/platform'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { createWidgetTab } from '@hcengineering/workbench-resources'
|
||||
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
|
||||
|
||||
import CardSearchItem from './components/CardSearchItem.svelte'
|
||||
import CreateSpace from './components/navigator/CreateSpace.svelte'
|
||||
@@ -189,23 +199,35 @@ const toCardObjectSearchResult = (e: WithLookup<Card>): ObjectSearchResult => ({
|
||||
component: CardSearchItem
|
||||
})
|
||||
|
||||
export async function createCard (type: Ref<MasterTag>, space: Ref<Space>): Promise<Ref<Card>> {
|
||||
export async function createCard (
|
||||
type: Ref<MasterTag>,
|
||||
space: Ref<Space>,
|
||||
data: Partial<Data<Card>> = {},
|
||||
contentMarkup: Markup = EmptyMarkup,
|
||||
id?: Ref<Card>
|
||||
): Promise<Ref<Card>> {
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const lastOne = await client.findOne(card.class.Card, {}, { sort: { rank: SortingOrder.Descending } })
|
||||
const title = await translate(card.string.Card, {})
|
||||
const title = data.title ?? (await translate(card.string.Card, {}))
|
||||
|
||||
const data: Data<Card> = {
|
||||
const _id = id ?? generateId()
|
||||
const content = isEmptyMarkup(contentMarkup)
|
||||
? ('' as MarkupBlobRef)
|
||||
: await createMarkup(makeCollabId(type, _id, 'content'), contentMarkup)
|
||||
|
||||
const _data: Data<Card> = {
|
||||
parentInfo: [],
|
||||
blobs: {},
|
||||
...data,
|
||||
title,
|
||||
rank: makeRank(lastOne?.rank, undefined),
|
||||
content: '' as MarkupBlobRef,
|
||||
parentInfo: [],
|
||||
blobs: {}
|
||||
content
|
||||
}
|
||||
|
||||
const filledData = fillDefaults(hierarchy, data, type)
|
||||
const filledData = fillDefaults(hierarchy, _data, type)
|
||||
|
||||
const _id = await client.createDoc(type, space, filledData)
|
||||
await client.createDoc(type, space, filledData, _id)
|
||||
|
||||
Analytics.handleEvent(CardEvents.CardCreated)
|
||||
return _id
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Blobs,
|
||||
Class,
|
||||
CollectionSize,
|
||||
Data,
|
||||
Doc,
|
||||
Domain,
|
||||
MarkupBlobRef,
|
||||
@@ -98,6 +99,16 @@ export interface FavoriteCard extends Preference {
|
||||
application: string
|
||||
}
|
||||
|
||||
export interface CreateCardExtension extends MasterTag {
|
||||
component?: AnyComponent
|
||||
canCreate?: CanCreateCardResource
|
||||
disableTitle?: boolean
|
||||
hideSpace?: boolean
|
||||
}
|
||||
|
||||
export type CanCreateCardFn = (space: Ref<Space>, data: Partial<Data<Card>>) => Promise<boolean | Ref<Card>>
|
||||
export type CanCreateCardResource = Resource<CanCreateCardFn>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -120,7 +131,8 @@ const cardPlugin = plugin(cardId, {
|
||||
FavoriteCard: '' as Ref<Class<FavoriteCard>>
|
||||
},
|
||||
mixin: {
|
||||
CardViewDefaults: '' as Ref<Mixin<CardViewDefaults>>
|
||||
CardViewDefaults: '' as Ref<Mixin<CardViewDefaults>>,
|
||||
CreateCardExtension: '' as Ref<Mixin<CreateCardExtension>>
|
||||
},
|
||||
space: {
|
||||
Default: '' as Ref<CardSpace>
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"@hcengineering/card-resources": "^0.6.0",
|
||||
"@hcengineering/chat": "^0.6.0",
|
||||
"@hcengineering/communication-types": "^0.1.0",
|
||||
"@hcengineering/communication": "^0.6.0",
|
||||
"@hcengineering/contact": "^0.6.24",
|
||||
"@hcengineering/contact-resources": "^0.6.0",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { SubscriptionLabelID } from '@hcengineering/communication-types'
|
||||
import chat, { chatId } from '@hcengineering/chat'
|
||||
import { Navigator } from '@hcengineering/card-resources'
|
||||
import communication from '@hcengineering/communication'
|
||||
|
||||
export let card: Card | undefined = undefined
|
||||
export let type: Ref<MasterTag> | undefined = undefined
|
||||
@@ -36,11 +37,16 @@
|
||||
labelFilter: [SubscriptionLabelID],
|
||||
preorder: [
|
||||
{ type: chat.masterTag.Thread, order: 1 },
|
||||
{ type: chat.masterTag.Channel, order: 2 }
|
||||
{ type: chat.masterTag.Channel, order: 2 },
|
||||
{ type: communication.type.Direct, order: 3 }
|
||||
],
|
||||
fixedTypes: [chat.masterTag.Thread, chat.masterTag.Channel],
|
||||
fixedTypes: [chat.masterTag.Thread, chat.masterTag.Channel, communication.type.Direct],
|
||||
specialSorting: {
|
||||
[communication.type.Direct]: 'alphabetical'
|
||||
},
|
||||
allowCreate: true,
|
||||
defaultSorting: 'recent',
|
||||
lookback: '1w',
|
||||
lookback: '2w',
|
||||
showTypeIcon: false,
|
||||
showCardIcon: true
|
||||
}}
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"Voted": "Hlasováno",
|
||||
"VotedFor": "Hlasováno pro",
|
||||
"RevokedVote": "Zrušeno hlasování",
|
||||
"AnonymousQuiz": "Anonymní test"
|
||||
"AnonymousQuiz": "Anonymní test",
|
||||
"Direct": "Přímý",
|
||||
"Directs": "Přímé",
|
||||
"Members": "Členové"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"Voted": "Abgestimmt",
|
||||
"VotedFor": "Für abgestimmt",
|
||||
"RevokedVote": "Stimme widerrufen",
|
||||
"AnonymousQuiz": "Anonyme quiz"
|
||||
"AnonymousQuiz": "Anonyme quiz",
|
||||
"Direct": "Direkt",
|
||||
"Directs": "Direkte",
|
||||
"Members": "Mitglieder"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"Voted": "Voted",
|
||||
"VotedFor": "Voted for",
|
||||
"RevokedVote": "Revoked vote",
|
||||
"AnonymousQuiz": "Anonymous quiz"
|
||||
"AnonymousQuiz": "Anonymous quiz",
|
||||
"Direct": "Direct",
|
||||
"Directs": "Directs",
|
||||
"Members": "Members"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"Voted": "Votado",
|
||||
"VotedFor": "Votado por",
|
||||
"RevokedVote": "Revocar voto",
|
||||
"AnonymousQuiz": "Quiz anónimo"
|
||||
"AnonymousQuiz": "Quiz anónimo",
|
||||
"Direct": "Directo",
|
||||
"Directs": "Directos",
|
||||
"Members": "Miembros"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"Voted": "A voté",
|
||||
"VotedFor": "A voté pour",
|
||||
"RevokedVote": "Révoquer le vote",
|
||||
"AnonymousQuiz": "Quiz anonyme"
|
||||
"AnonymousQuiz": "Quiz anonyme",
|
||||
"Direct": "Direct",
|
||||
"Directs": "Directs",
|
||||
"Members": "Membres"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"Voted": "Votato",
|
||||
"VotedFor": "Votato per",
|
||||
"RevokedVote": "Revoca il voto",
|
||||
"AnonymousQuiz": "Quiz anonimo"
|
||||
"AnonymousQuiz": "Quiz anonimo",
|
||||
"Direct": "Diretto",
|
||||
"Directs": "Diretti",
|
||||
"Members": "Membri"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"Voted": "投票済み",
|
||||
"VotedFor": "投票したユーザ",
|
||||
"RevokedVote": "投票を取り消し",
|
||||
"AnonymousQuiz": "匿名クイズ"
|
||||
"AnonymousQuiz": "匿名クイズ",
|
||||
"Direct": "ダイレクト",
|
||||
"Directs": "ダイレクト",
|
||||
"Members": "メンバー"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"Voted": "Votado",
|
||||
"VotedFor": "Votado para",
|
||||
"RevokedVote": "Revogar voto",
|
||||
"AnonymousQuiz": "Quiz anônimo"
|
||||
"AnonymousQuiz": "Quiz anônimo",
|
||||
"Direct": "Direto",
|
||||
"Directs": "Diretos",
|
||||
"Members": "Membros"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"Voted": "Проголосовавшие",
|
||||
"VotedFor": "Голосовал(а) за",
|
||||
"RevokedVote": "Отменил(а) голос",
|
||||
"AnonymousQuiz": "Анонимная викторина"
|
||||
"AnonymousQuiz": "Анонимная викторина",
|
||||
"Direct": "Личные сообщения",
|
||||
"Directs": "Личные сообщения",
|
||||
"Members": "Участники"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"Voted": "已投票",
|
||||
"VotedFor": "投票于",
|
||||
"RevokedVote": "撤销投票",
|
||||
"AnonymousQuiz": "匿名测验"
|
||||
"AnonymousQuiz": "匿名测验",
|
||||
"Direct": "私信",
|
||||
"Directs": "私信",
|
||||
"Members": "成员"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,9 +121,7 @@ export async function attachCardToMessage (
|
||||
},
|
||||
type
|
||||
)
|
||||
const apply = client.apply('create thread', undefined, true)
|
||||
await apply.createDoc(type, parentCard.space, data, threadCardID)
|
||||
await apply.commit()
|
||||
await client.createDoc(type, parentCard.space, data, threadCardID)
|
||||
|
||||
if (author?.active === true && author?.personUuid !== undefined) {
|
||||
await communicationClient.addCollaborators(threadCardID, type, [author.personUuid])
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<!-- Copyright © 2025 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 { createEventDispatcher, onMount } from 'svelte'
|
||||
import { Data, notEmpty, Ref } from '@hcengineering/core'
|
||||
import { Employee, formatName, getCurrentEmployee, getCurrentEmployeeSpace } from '@hcengineering/contact'
|
||||
import { Direct } from '@hcengineering/communication'
|
||||
import { employeeByIdStore } from '@hcengineering/contact-resources'
|
||||
|
||||
export let collaborators: Ref<Employee>[] = []
|
||||
export let data: Partial<Data<Direct>>
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: updateData(collaborators)
|
||||
|
||||
function updateData (collaborators: Ref<Employee>[]): void {
|
||||
const title = getTitle(collaborators)
|
||||
if (data.title === title) return
|
||||
|
||||
dispatch('change', { data: { title, members: collaborators } })
|
||||
}
|
||||
|
||||
function getTitle (ids: Ref<Employee>[]): string {
|
||||
const me = getCurrentEmployee()
|
||||
const employees = ids.map((id) => $employeeByIdStore.get(id)).filter(notEmpty)
|
||||
|
||||
if (employees.length === 1) {
|
||||
return employees.map((e) => formatName(e.name)).join(', ')
|
||||
} else {
|
||||
return employees
|
||||
.filter((it) => it._id !== me)
|
||||
.map((e) => formatName(e.name))
|
||||
.join(', ')
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const space = getCurrentEmployeeSpace()
|
||||
const members = collaborators
|
||||
const title = getTitle(collaborators)
|
||||
|
||||
dispatch('change', { data: { members, title }, space })
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,183 @@
|
||||
<!-- Copyright © 2025 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 { Direct } from '@hcengineering/communication'
|
||||
import { Icon, IconSize } from '@hcengineering/ui'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import contact, { Employee, getCurrentEmployee, Person } from '@hcengineering/contact'
|
||||
import { classIcon } from '@hcengineering/view-resources'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Avatar, employeeByIdStore, getPersonByPersonRef } from '@hcengineering/contact-resources'
|
||||
|
||||
import communication from '../plugin'
|
||||
|
||||
export let value: Direct | undefined = undefined
|
||||
export let size: IconSize = 'small'
|
||||
export let showStatus = true
|
||||
|
||||
const visiblePersons = 4
|
||||
const client = getClient()
|
||||
const me = getCurrentEmployee()
|
||||
|
||||
let persons: Person[] = []
|
||||
|
||||
$: void updatePersons(value?.members ?? [], $employeeByIdStore)
|
||||
|
||||
async function updatePersons (members: Ref<Person>[], employeeById: Map<Ref<Employee>, Employee>): Promise<void> {
|
||||
const res: Person[] = []
|
||||
|
||||
for (const member of members) {
|
||||
const p = employeeById.get(member as Ref<Employee>)
|
||||
if (p !== undefined) {
|
||||
res.push(p)
|
||||
} else {
|
||||
const person = await getPersonByPersonRef(member)
|
||||
if (person != null) {
|
||||
res.push(person)
|
||||
}
|
||||
}
|
||||
}
|
||||
persons = res
|
||||
}
|
||||
|
||||
let avatarSize = size
|
||||
|
||||
$: if (size === 'small') {
|
||||
avatarSize = 'x-small'
|
||||
} else if (size === 'medium') {
|
||||
avatarSize = 'small'
|
||||
} else if (size === 'large') {
|
||||
avatarSize = 'medium'
|
||||
}
|
||||
|
||||
$: withoutMe = persons.filter((it) => it._id !== me)
|
||||
</script>
|
||||
|
||||
{#if persons.length === 0}
|
||||
<Icon icon={classIcon(client, communication.type.Direct) ?? contact.icon.Contacts} {size} />
|
||||
{:else if withoutMe.length === 1 || persons.length === 1}
|
||||
{@const p = withoutMe[0] ?? persons[0]}
|
||||
<Avatar person={p} size={avatarSize} name={p.name} {showStatus} />
|
||||
{:else if persons.length > 1 && (size === 'medium' || avatarSize === 'medium')}
|
||||
<div class="group">
|
||||
{#each persons.slice(0, visiblePersons - 1) as person}
|
||||
<Avatar {person} size="tiny" name={person.name} />
|
||||
{/each}
|
||||
{#if persons.length === visiblePersons}
|
||||
{@const person = persons[persons.length - 1]}
|
||||
<Avatar {person} size="tiny" name={person.name} />
|
||||
{:else if persons.length > visiblePersons}
|
||||
<div class="rect">
|
||||
+{persons.length - visiblePersons + 1}
|
||||
</div>
|
||||
{:else if persons.length < visiblePersons}
|
||||
{#each Array(visiblePersons - persons.length) as _}
|
||||
<div class="rect" />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{:else if persons.length > 1}
|
||||
<span class="relative">
|
||||
<svg width="0" height="0" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<clipPath id="direct-count-marker" clipPathUnits="objectBoundingBox">
|
||||
<path d="M1,0 H0 V1 H0.48 V0.73 C0.48,0.58 0.58,0.48 0.73,0.48 H1 Z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<Avatar
|
||||
person={persons[0]}
|
||||
size={avatarSize}
|
||||
name={persons[0].name}
|
||||
showStatus={false}
|
||||
clipPath="url(#direct-count-marker)"
|
||||
/>
|
||||
<span class="persons-count {avatarSize}">
|
||||
{#if persons.length > 9}
|
||||
9+
|
||||
{:else}
|
||||
{persons.length}
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
min-width: 2.5rem;
|
||||
min-height: 2.5rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rect {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.13rem;
|
||||
height: 1.13rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
background-color: var(--theme-button-hovered);
|
||||
font-size: 0.688rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.group-ava {
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-bg-color);
|
||||
border: 1px solid var(--theme-divider-color);
|
||||
border-radius: 0.25rem;
|
||||
opacity: 0.9;
|
||||
|
||||
&.inline,
|
||||
&.tiny,
|
||||
&.card,
|
||||
&.x-small {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
.persons-count {
|
||||
position: absolute;
|
||||
right: -0.188rem;
|
||||
bottom: -0.188rem;
|
||||
width: 0.938rem;
|
||||
height: 0.938rem;
|
||||
background: var(--avatar-counter-BackgroundColor);
|
||||
border: 1px solid var(--global-ui-BorderColor);
|
||||
font-size: 0.625rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.313rem;
|
||||
color: var(--global-secondary-TextColor);
|
||||
font-weight: 500;
|
||||
|
||||
&.medium {
|
||||
right: -0.313rem;
|
||||
bottom: -0.313rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -21,8 +21,10 @@ import CreatePoll from './components/poll/CreatePoll.svelte'
|
||||
import PollPreview from './components/poll/PollPreview.svelte'
|
||||
import UserVoteActivityPresenter from './components/poll/UserVoteActivityPresenter.svelte'
|
||||
import UserVotesPresenter from './components/poll/UserVotesPresenter.svelte'
|
||||
import DirectIcon from './components/DirectIcon.svelte'
|
||||
import CreateDirect from './components/CreateDirect.svelte'
|
||||
|
||||
import { unsubscribe, subscribe, canSubscribe, canUnsubscribe } from './utils'
|
||||
import { unsubscribe, subscribe, canSubscribe, canUnsubscribe, canCreateDirect } from './utils'
|
||||
import {
|
||||
addReaction,
|
||||
canCreateCard,
|
||||
@@ -49,7 +51,9 @@ export { default as ActivityMessageViewer } from './components/message/ActivityM
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
CardMessagesSection
|
||||
CardMessagesSection,
|
||||
DirectIcon,
|
||||
CreateDirect
|
||||
},
|
||||
poll: {
|
||||
PollPresenter,
|
||||
@@ -81,6 +85,7 @@ export default async (): Promise<Resources> => ({
|
||||
CanShowOriginalMessage: canShowOriginalMessage,
|
||||
CanEditMessage: canEditMessage,
|
||||
CanRemoveMessage: canRemoveMessage,
|
||||
CanCreateCard: canCreateCard
|
||||
CanCreateCard: canCreateCard,
|
||||
CanCreateDirect: canCreateDirect
|
||||
}
|
||||
})
|
||||
|
||||
@@ -22,10 +22,13 @@ import communication, {
|
||||
} from '@hcengineering/communication'
|
||||
import { type AnyComponent } from '@hcengineering/ui'
|
||||
import { type Ref } from '@hcengineering/core'
|
||||
import { type CanCreateCardResource } from '@hcengineering/card'
|
||||
|
||||
export default mergeIds(communicationId, communication, {
|
||||
component: {
|
||||
CardMessagesSection: '' as AnyComponent
|
||||
CardMessagesSection: '' as AnyComponent,
|
||||
DirectIcon: '' as AnyComponent,
|
||||
CreateDirect: '' as AnyComponent
|
||||
},
|
||||
poll: {
|
||||
PollPresenter: '' as AnyComponent,
|
||||
@@ -81,7 +84,10 @@ export default mergeIds(communicationId, communication, {
|
||||
EditMessage: '' as IntlString,
|
||||
RemoveMessage: '' as IntlString,
|
||||
CreateCard: '' as IntlString,
|
||||
MessageAlreadyHasCardAttached: '' as IntlString
|
||||
MessageAlreadyHasCardAttached: '' as IntlString,
|
||||
Direct: '' as IntlString,
|
||||
Directs: '' as IntlString,
|
||||
Members: '' as IntlString
|
||||
},
|
||||
messageActionImpl: {
|
||||
AddReaction: '' as MessageActionFunctionResource,
|
||||
@@ -107,6 +113,7 @@ export default mergeIds(communicationId, communication, {
|
||||
CanShowOriginalMessage: '' as MessageActionVisibilityTesterResource,
|
||||
CanEditMessage: '' as MessageActionVisibilityTesterResource,
|
||||
CanRemoveMessage: '' as MessageActionVisibilityTesterResource,
|
||||
CanCreateCard: '' as MessageActionVisibilityTesterResource
|
||||
CanCreateCard: '' as MessageActionVisibilityTesterResource,
|
||||
CanCreateDirect: '' as CanCreateCardResource
|
||||
}
|
||||
})
|
||||
|
||||
@@ -11,9 +11,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { canDisplayLinkPreview, fetchLinkPreviewDetails, getCommunicationClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
canDisplayLinkPreview,
|
||||
fetchLinkPreviewDetails,
|
||||
getClient,
|
||||
getCommunicationClient
|
||||
} from '@hcengineering/presentation'
|
||||
import { type Card } from '@hcengineering/card'
|
||||
import { AccountRole, getCurrentAccount, type Markup } from '@hcengineering/core'
|
||||
import { AccountRole, type Data, getCurrentAccount, type Ref, type Space, type Markup } from '@hcengineering/core'
|
||||
import { getMetadata, translate } from '@hcengineering/platform'
|
||||
import { addNotification, languageStore, NotificationSeverity, showPopup } from '@hcengineering/ui'
|
||||
import { type LinkPreviewParams, type Message } from '@hcengineering/communication-types'
|
||||
@@ -28,6 +33,8 @@ import { type TextInputAction } from './types'
|
||||
import { guestCommunicationAllowedCards } from './stores'
|
||||
import { get } from 'svelte/store'
|
||||
import view from '@hcengineering/view'
|
||||
import { type Direct } from '@hcengineering/communication'
|
||||
import { type Employee } from '@hcengineering/contact'
|
||||
|
||||
export async function unsubscribe (card: Card): Promise<void> {
|
||||
const client = getCommunicationClient()
|
||||
@@ -160,3 +167,31 @@ export async function showForbidden (): Promise<void> {
|
||||
NotificationSeverity.Info
|
||||
)
|
||||
}
|
||||
|
||||
export async function canCreateDirect (space: Ref<Space>, data: Partial<Data<Direct>>): Promise<boolean | Ref<Card>> {
|
||||
const members = data.members ?? []
|
||||
if (members.length === 0) return false
|
||||
|
||||
const client = getClient()
|
||||
|
||||
if (members.length > 2) {
|
||||
return true
|
||||
}
|
||||
|
||||
const myDirects = await client.findAll<Direct>(communication.type.Direct, { space })
|
||||
const direct = myDirects.find((it) => {
|
||||
const directMembers = new Set(it.members)
|
||||
const createMembers = new Set(members)
|
||||
if (directMembers.size !== createMembers.size) return false
|
||||
for (const item of directMembers) {
|
||||
if (!createMembers.has(item as Ref<Employee>)) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if (direct != null) {
|
||||
return direct._id
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright © 2025 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.
|
||||
|
||||
import { Card } from '@hcengineering/card'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Person } from '@hcengineering/contact'
|
||||
|
||||
export interface Direct extends Card {
|
||||
// TODO: Do we neet it? Can we just reuse collaborators?
|
||||
members?: Ref<Person>[]
|
||||
}
|
||||
@@ -33,7 +33,8 @@ export default plugin(communicationId, {
|
||||
GuestCommunicationSettings: '' as Ref<Class<GuestCommunicationSettings>>
|
||||
},
|
||||
type: {
|
||||
Poll: '' as Ref<MasterTag>
|
||||
Poll: '' as Ref<MasterTag>,
|
||||
Direct: '' as Ref<MasterTag>
|
||||
},
|
||||
icon: {
|
||||
Bell: '' as Asset,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright © 2025 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.
|
||||
|
||||
import { AttachedDoc, Ref } from '@hcengineering/core'
|
||||
import { PersonSpace } from '@hcengineering/contact'
|
||||
import { AccountID, MessageID } from '@hcengineering/communication-types'
|
||||
import { Card } from '@hcengineering/card'
|
||||
|
||||
export interface PollAnswer extends AttachedDoc<Poll> {
|
||||
options: string[]
|
||||
space: Ref<PersonSpace>
|
||||
}
|
||||
|
||||
export interface UserVote {
|
||||
account: AccountID
|
||||
options: { id: string, label: string, votedAt: Date }[]
|
||||
}
|
||||
|
||||
export interface Poll extends Card {
|
||||
messageId: MessageID
|
||||
totalVotes: number
|
||||
|
||||
userVotes?: UserVote[]
|
||||
}
|
||||
@@ -25,6 +25,9 @@ import { Card, MasterTag } from '@hcengineering/card'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import { PersonSpace } from '@hcengineering/contact'
|
||||
|
||||
export * from './poll'
|
||||
export * from './direct'
|
||||
|
||||
export enum MessagesNavigationAnchors {
|
||||
ConversationStart = 'conversationStart',
|
||||
LatestMessages = 'latestMessages'
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
export let disabled: boolean = false
|
||||
export let style: 'modern' | undefined = undefined
|
||||
export let clickable: boolean = false
|
||||
export let clipPath: string | undefined = undefined
|
||||
|
||||
export function pulse (): void {
|
||||
avatarInst.pulse()
|
||||
@@ -141,6 +142,7 @@
|
||||
{disabled}
|
||||
{style}
|
||||
withStatus
|
||||
{clipPath}
|
||||
/>
|
||||
<div
|
||||
class="hulyAvatar-statusMarker {statusSize ?? size} {style}"
|
||||
@@ -163,6 +165,7 @@
|
||||
{adaptiveName}
|
||||
{disabled}
|
||||
{style}
|
||||
{clipPath}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
export let adaptiveName: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let style: 'modern' | undefined = undefined
|
||||
export let clipPath: string | undefined = undefined
|
||||
|
||||
function handleClick (): void {
|
||||
dispatch('click')
|
||||
@@ -79,6 +80,7 @@
|
||||
class:withStatus
|
||||
style:--border-color={bColor ?? 'var(--primary-button-default)'}
|
||||
style:background-color={background}
|
||||
style:clip-path={clipPath}
|
||||
use:resizeObserver={(element) => {
|
||||
fontSize = element.clientWidth * 0.6
|
||||
}}
|
||||
@@ -103,6 +105,7 @@
|
||||
class:withStatus
|
||||
style:--border-color={bColor ?? 'var(--primary-button-default)'}
|
||||
style:background-color={background}
|
||||
style:clip-path={clipPath}
|
||||
on:click={handleClick}
|
||||
>
|
||||
{#if url && !imgError}
|
||||
|
||||
@@ -119,6 +119,8 @@ export function start (
|
||||
findCollaborators: async () => [],
|
||||
findNotifications: async () => [],
|
||||
findLabels: async () => [],
|
||||
findThreads: async () => [],
|
||||
findPeers: async () => [],
|
||||
unsubscribeQuery: async () => {},
|
||||
event: async () => {
|
||||
return {}
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function generateActivity (
|
||||
): Promise<void> {
|
||||
const { hierarchy } = control
|
||||
|
||||
if (tx.space === core.space.DerivedTx) return
|
||||
// if (tx.space === core.space.DerivedTx) return
|
||||
|
||||
if (
|
||||
hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage) ||
|
||||
@@ -43,6 +43,9 @@ export async function generateActivity (
|
||||
switch (tx._class) {
|
||||
case core.class.TxCreateDoc: {
|
||||
const card = TxProcessor.createDoc2Doc(tx as TxCreateDoc<Card>)
|
||||
if (card._class === 'chat:masterTag:Thread') {
|
||||
break
|
||||
}
|
||||
await createMessages(tx, control, card)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/communication-types": "^0.1.0",
|
||||
"@hcengineering/communication-sdk-types": "^0.1.0",
|
||||
"@hcengineering/communication": "^0.6.0",
|
||||
"@hcengineering/server-contact": "^0.6.1",
|
||||
"@hcengineering/contact": "^0.6.24"
|
||||
}
|
||||
|
||||
@@ -20,10 +20,13 @@ import core, {
|
||||
Data,
|
||||
Doc,
|
||||
fillDefaults,
|
||||
generateId,
|
||||
getDiffUpdate,
|
||||
Mixin,
|
||||
notEmpty,
|
||||
OperationDomain,
|
||||
Ref,
|
||||
Space,
|
||||
splitMixinUpdate,
|
||||
Tx,
|
||||
TxCreateDoc,
|
||||
@@ -39,11 +42,17 @@ import {
|
||||
AddCollaboratorsEvent,
|
||||
CardEventType,
|
||||
NotificationEventType,
|
||||
PeerEventType,
|
||||
RemoveCardEvent,
|
||||
UpdateCardTypeEvent
|
||||
UpdateCardTypeEvent,
|
||||
CreatePeerEvent,
|
||||
ThreadPatchEvent,
|
||||
MessageEventType
|
||||
} from '@hcengineering/communication-sdk-types'
|
||||
import { getEmployee, getPersonSpaces } from '@hcengineering/server-contact'
|
||||
import contact from '@hcengineering/contact'
|
||||
import contact, { Employee, Person } from '@hcengineering/contact'
|
||||
import communication, { Direct } from '@hcengineering/communication'
|
||||
import { CardPeer } from '@hcengineering/communication-types'
|
||||
|
||||
async function OnAttribute (ctx: TxCreateDoc<AnyAttribute>[], control: TriggerControl): Promise<Tx[]> {
|
||||
const attr = TxProcessor.createDoc2Doc(ctx[0])
|
||||
@@ -400,6 +409,192 @@ async function updateParentInfoName (
|
||||
return res
|
||||
}
|
||||
|
||||
async function OnThreadCreate (ctx: TxCreateDoc<Card>[], control: TriggerControl): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
for (const tx of ctx) {
|
||||
if (tx.space === core.space.DerivedTx) continue
|
||||
const doc = TxProcessor.createDoc2Doc(tx)
|
||||
const parent = doc.parentInfo?.[0]
|
||||
if (parent == null) continue
|
||||
if (!control.hierarchy.isDerived(parent._class, communication.type.Direct)) continue
|
||||
const direct = (await control.findAll(control.ctx, parent._class, { _id: parent._id }, { limit: 1 }))[0] as Direct
|
||||
if (direct == null) continue
|
||||
|
||||
res.push(...(await createThreadCardPeers(direct, doc, control)))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function createThreadCardPeers (direct: Direct, doc: Card, control: TriggerControl): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
const cardIds = new Map<Ref<Card>, Ref<Space>>([[doc._id, doc.space]])
|
||||
const members = direct.members ?? []
|
||||
if (members.length === 0) return []
|
||||
|
||||
const thread = (
|
||||
await control.domainRequest(control.ctx, 'communication' as OperationDomain, {
|
||||
findThreads: { params: { threadId: doc._id } }
|
||||
})
|
||||
).value[0]
|
||||
if (thread === undefined) return []
|
||||
|
||||
const messageId = thread.messageId
|
||||
const directPeer = (
|
||||
(
|
||||
await control.domainRequest(control.ctx, 'communication' as OperationDomain, {
|
||||
findPeers: { params: { kind: 'card', cardId: direct._id } }
|
||||
})
|
||||
).value as CardPeer[]
|
||||
)[0]
|
||||
|
||||
const personSpaces = (await getPersonSpaces(control)).filter(
|
||||
(it) => it._id !== doc.space && members.includes(it.person)
|
||||
)
|
||||
if (personSpaces.length === 0) return []
|
||||
const accounts = (
|
||||
await control.findAll(control.ctx, contact.mixin.Employee, {
|
||||
_id: { $in: personSpaces.map((it) => it.person) as Ref<Employee>[] }
|
||||
})
|
||||
)
|
||||
.map((it) => it.personUuid)
|
||||
.filter(notEmpty)
|
||||
if (accounts.length === 0) return []
|
||||
|
||||
// TODO: create directs in person_workspace
|
||||
for (const personSpace of personSpaces) {
|
||||
const _id = generateId<Card>()
|
||||
const _class = doc._class
|
||||
cardIds.set(_id, personSpace._id)
|
||||
res.push(
|
||||
control.txFactory.createTxCreateDoc(
|
||||
_class,
|
||||
personSpace._id,
|
||||
{
|
||||
...doc
|
||||
},
|
||||
_id
|
||||
)
|
||||
)
|
||||
|
||||
const parentDirect = directPeer?.members?.find((m) => m.extra?.space === personSpace._id)
|
||||
|
||||
if (parentDirect !== undefined) {
|
||||
const threadPatchEvent: ThreadPatchEvent = {
|
||||
type: MessageEventType.ThreadPatch,
|
||||
cardId: parentDirect.cardId,
|
||||
messageId,
|
||||
operation: {
|
||||
opcode: 'attach',
|
||||
threadId: _id,
|
||||
threadType: _class
|
||||
},
|
||||
socialId: doc.modifiedBy
|
||||
}
|
||||
await control.domainRequest(control.ctx, 'communication' as OperationDomain, {
|
||||
event: threadPatchEvent
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (cardIds.size > 1) {
|
||||
const group = generateId()
|
||||
for (const [cardId, spaceId] of cardIds.entries()) {
|
||||
const event: CreatePeerEvent = {
|
||||
type: PeerEventType.CreatePeer,
|
||||
workspaceId: control.workspace.uuid, // TODO: person_workspace
|
||||
cardId,
|
||||
kind: 'card',
|
||||
value: group,
|
||||
extra: { space: spaceId },
|
||||
date: new Date(doc.modifiedOn)
|
||||
}
|
||||
await control.domainRequest(control.ctx, 'communication' as OperationDomain, { event })
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function createDirectCardPeers (doc: Card, members: Ref<Person>[], control: TriggerControl): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
const cardIds = new Map<Ref<Card>, Ref<Space>>([[doc._id, doc.space]])
|
||||
if (members.length === 0) return []
|
||||
|
||||
const personSpaces = (await getPersonSpaces(control)).filter(
|
||||
(it) => it._id !== doc.space && members.includes(it.person)
|
||||
)
|
||||
if (personSpaces.length === 0) return []
|
||||
const accounts = (
|
||||
await control.findAll(control.ctx, contact.mixin.Employee, {
|
||||
_id: { $in: personSpaces.map((it) => it.person) as Ref<Employee>[] }
|
||||
})
|
||||
)
|
||||
.map((it) => it.personUuid)
|
||||
.filter(notEmpty)
|
||||
if (accounts.length === 0) return []
|
||||
|
||||
// TODO: create directs in person_workspace
|
||||
for (const personSpace of personSpaces) {
|
||||
const _id = generateId<Card>()
|
||||
const _class = doc._class
|
||||
cardIds.set(_id, personSpace._id)
|
||||
res.push(
|
||||
control.txFactory.createTxCreateDoc(
|
||||
_class,
|
||||
personSpace._id,
|
||||
{
|
||||
...doc
|
||||
},
|
||||
_id
|
||||
)
|
||||
)
|
||||
|
||||
const event: AddCollaboratorsEvent = {
|
||||
type: NotificationEventType.AddCollaborators,
|
||||
cardId: _id,
|
||||
cardType: _class,
|
||||
collaborators: accounts,
|
||||
socialId: doc.modifiedBy,
|
||||
date: new Date(doc.modifiedOn + 1)
|
||||
}
|
||||
|
||||
await control.domainRequest(control.ctx, 'communication' as OperationDomain, { event })
|
||||
}
|
||||
|
||||
if (cardIds.size > 1) {
|
||||
const group = generateId()
|
||||
for (const [cardId, spaceId] of cardIds.entries()) {
|
||||
const event: CreatePeerEvent = {
|
||||
type: PeerEventType.CreatePeer,
|
||||
workspaceId: control.workspace.uuid, // TODO: person_workspace
|
||||
cardId,
|
||||
kind: 'card',
|
||||
value: group,
|
||||
extra: { space: spaceId },
|
||||
date: new Date(doc.modifiedOn)
|
||||
}
|
||||
await control.domainRequest(control.ctx, 'communication' as OperationDomain, { event })
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function OnDirectCreate (ctx: TxCreateDoc<Direct>[], control: TriggerControl): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
|
||||
for (const tx of ctx) {
|
||||
if (tx.space === core.space.DerivedTx) continue
|
||||
const doc = TxProcessor.createDoc2Doc(tx)
|
||||
const members = doc.members ?? []
|
||||
|
||||
res.push(...(await createDirectCardPeers(doc, members, control)))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function OnCardCreate (ctx: TxCreateDoc<Card>[], control: TriggerControl): Promise<Tx[]> {
|
||||
const createTx = ctx[0]
|
||||
const doc = TxProcessor.createDoc2Doc(createTx)
|
||||
@@ -515,6 +710,8 @@ export default async () => ({
|
||||
OnCardRemove,
|
||||
OnCardCreate,
|
||||
OnCardUpdate,
|
||||
OnCardTag
|
||||
OnCardTag,
|
||||
OnDirectCreate,
|
||||
OnThreadCreate
|
||||
}
|
||||
})
|
||||
|
||||
@@ -33,6 +33,8 @@ export default plugin(serverCardId, {
|
||||
OnTagRemove: '' as Resource<TriggerFunc>,
|
||||
OnMasterTagRemove: '' as Resource<TriggerFunc>,
|
||||
OnCardCreate: '' as Resource<TriggerFunc>,
|
||||
OnDirectCreate: '' as Resource<TriggerFunc>,
|
||||
OnThreadCreate: '' as Resource<TriggerFunc>,
|
||||
OnCardUpdate: '' as Resource<TriggerFunc>,
|
||||
OnCardTag: '' as Resource<TriggerFunc>,
|
||||
OnCardRemove: '' as Resource<TriggerFunc>
|
||||
|
||||
@@ -129,10 +129,18 @@ export class CommunicationMiddleware extends BaseMiddleware implements Middlewar
|
||||
const { params } = args.findLabels
|
||||
return await this.communicationApi.findLabels(ctx, params)
|
||||
}
|
||||
if (args.findThreads !== undefined) {
|
||||
const { params } = args.findThreads
|
||||
return await this.communicationApi.findThreads(ctx, params)
|
||||
}
|
||||
if (args.findCollaborators !== undefined) {
|
||||
const { params } = args.findCollaborators
|
||||
return await this.communicationApi.findCollaborators(ctx, params)
|
||||
}
|
||||
if (args.findPeers !== undefined) {
|
||||
const { params } = args.findPeers
|
||||
return await this.communicationApi.findPeers(ctx, params)
|
||||
}
|
||||
if (args.unsubscribeQuery !== undefined) {
|
||||
const { id } = args.unsubscribeQuery
|
||||
await this.communicationApi.unsubscribeQuery(ctx, id)
|
||||
|
||||
Reference in New Issue
Block a user