Fillable templates (#2667)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-02-21 12:40:03 +06:00 committed by GitHub
parent a088b51e24
commit af09bb3ead
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 626 additions and 122 deletions

View File

@ -39,6 +39,7 @@
"@hcengineering/contact": "^0.6.11",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/view": "^0.6.2",
"cross-fetch": "^3.1.5"
"cross-fetch": "^3.1.5",
"@hcengineering/templates": "^0.6.0"
}
}

View File

@ -55,6 +55,7 @@ import workbench from '@hcengineering/model-workbench'
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import { AnyComponent } from '@hcengineering/ui'
import templates from '@hcengineering/templates'
import contact from './plugin'
export const DOMAIN_CONTACT = 'contact' as Domain
@ -576,6 +577,57 @@ export function createModel (builder: Builder): void {
},
contact.filter.FilterChannelNin
)
builder.createDoc(
templates.class.TemplateFieldCategory,
core.space.Model,
{
label: contact.string.CurrentEmployee
},
contact.templateFieldCategory.CurrentEmployee
)
builder.createDoc(
templates.class.TemplateField,
core.space.Model,
{
label: contact.string.Name,
category: contact.templateFieldCategory.CurrentEmployee,
func: contact.function.GetCurrentEmployeeName
},
contact.templateField.CurrentEmployeeName
)
builder.createDoc(
templates.class.TemplateField,
core.space.Model,
{
label: contact.string.Email,
category: contact.templateFieldCategory.CurrentEmployee,
func: contact.function.GetCurrentEmployeeEmail
},
contact.templateField.CurrentEmployeeEmail
)
builder.createDoc(
templates.class.TemplateFieldCategory,
core.space.Model,
{
label: contact.string.Contact
},
contact.templateFieldCategory.Contact
)
builder.createDoc(
templates.class.TemplateField,
core.space.Model,
{
label: contact.string.Name,
category: contact.templateFieldCategory.Contact,
func: contact.function.GetContactName
},
contact.templateField.ContactName
)
}
export { contactOperation } from './migration'

View File

@ -19,6 +19,7 @@ import type { Ref } from '@hcengineering/core'
import {} from '@hcengineering/core'
import { ObjectSearchCategory, ObjectSearchFactory } from '@hcengineering/model-presentation'
import { IntlString, mergeIds, Resource } from '@hcengineering/platform'
import { TemplateFieldFunc } from '@hcengineering/templates'
import type { AnyComponent } from '@hcengineering/ui'
import { Action, ActionCategory, ViewAction } from '@hcengineering/view'
@ -75,7 +76,8 @@ export default mergeIds(contactId, contact, {
Birthday: '' as IntlString,
CreatedOn: '' as IntlString,
Whatsapp: '' as IntlString,
WhatsappPlaceholder: '' as IntlString
WhatsappPlaceholder: '' as IntlString,
CurrentEmployee: '' as IntlString
},
completion: {
PersonQuery: '' as Resource<ObjectSearchFactory>,
@ -94,5 +96,10 @@ export default mergeIds(contactId, contact, {
actionImpl: {
KickEmployee: '' as ViewAction,
OpenChannel: '' as ViewAction
},
function: {
GetCurrentEmployeeName: '' as Resource<TemplateFieldFunc>,
GetCurrentEmployeeEmail: '' as Resource<TemplateFieldFunc>,
GetContactName: '' as Resource<TemplateFieldFunc>
}
})

View File

@ -36,6 +36,7 @@
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/task": "^0.6.1",
"@hcengineering/templates": "^0.6.0",
"@hcengineering/activity": "^0.6.0"
}
}

View File

@ -31,6 +31,7 @@ import {
} from '@hcengineering/setting'
import task from '@hcengineering/task'
import setting from './plugin'
import templates from '@hcengineering/templates'
import workbench from '@hcengineering/model-workbench'
import { AnyComponent } from '@hcengineering/ui'
@ -411,6 +412,37 @@ export function createModel (builder: Builder): void {
group: 'edit'
}
})
builder.createDoc(
templates.class.TemplateFieldCategory,
core.space.Model,
{
label: setting.string.Integrations
},
setting.templateFieldCategory.Integration
)
builder.createDoc(
templates.class.TemplateField,
core.space.Model,
{
label: setting.string.IntegrationWith,
category: setting.templateFieldCategory.Integration,
func: setting.function.GetValue
},
setting.templateField.Value
)
builder.createDoc(
templates.class.TemplateField,
core.space.Model,
{
label: setting.string.Owner,
category: setting.templateFieldCategory.Integration,
func: setting.function.GetOwnerName
},
setting.templateField.OwnerName
)
}
export { settingOperation } from './migration'

View File

@ -15,11 +15,12 @@
import type { TxViewlet } from '@hcengineering/activity'
import { Doc, Ref } from '@hcengineering/core'
import { mergeIds } from '@hcengineering/platform'
import { IntlString, mergeIds, Resource } from '@hcengineering/platform'
import { settingId } from '@hcengineering/setting'
import setting from '@hcengineering/setting-resources/src/plugin'
import { AnyComponent } from '@hcengineering/ui'
import { Action, ActionCategory, ViewAction } from '@hcengineering/view'
import { TemplateFieldFunc } from '@hcengineering/templates'
export default mergeIds(settingId, setting, {
activity: {
@ -52,5 +53,12 @@ export default mergeIds(settingId, setting, {
},
actionImpl: {
DeleteMixin: '' as ViewAction<Record<string, any>>
},
string: {
Value: '' as IntlString
},
function: {
GetValue: '' as Resource<TemplateFieldFunc>,
GetOwnerName: '' as Resource<TemplateFieldFunc>
}
})

View File

@ -14,12 +14,13 @@
// limitations under the License.
//
import { Domain, IndexKind } from '@hcengineering/core'
import { Domain, DOMAIN_MODEL, IndexKind, Ref } from '@hcengineering/core'
import { Builder, Index, Model, Prop, TypeString } from '@hcengineering/model'
import core, { TDoc } from '@hcengineering/model-core'
import textEditor from '@hcengineering/model-text-editor'
import { IntlString, Resource } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import type { MessageTemplate } from '@hcengineering/templates'
import type { MessageTemplate, TemplateField, TemplateFieldCategory, TemplateFieldFunc } from '@hcengineering/templates'
import templates from './plugin'
export const DOMAIN_TEMPLATES = 'templates' as Domain
@ -35,8 +36,20 @@ export class TMessageTemplate extends TDoc implements MessageTemplate {
message!: string
}
@Model(templates.class.TemplateFieldCategory, core.class.Doc, DOMAIN_MODEL)
export class TTemplateFieldCategory extends TDoc implements TemplateFieldCategory {
label!: IntlString
}
@Model(templates.class.TemplateField, core.class.Doc, DOMAIN_MODEL)
export class TTemplateField extends TDoc implements TemplateField {
category!: Ref<TemplateFieldCategory>
label!: IntlString
func!: Resource<TemplateFieldFunc>
}
export function createModel (builder: Builder): void {
builder.createModel(TMessageTemplate)
builder.createModel(TMessageTemplate, TTemplateFieldCategory, TTemplateField)
builder.createDoc(
setting.class.WorkspaceSettingCategory,

View File

@ -13,47 +13,46 @@
// limitations under the License.
-->
<script lang="ts">
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
import { Asset, getEmbeddedLabel, getResource, IntlString } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation'
import {
AnySvelteComponent,
getEventPositionElement,
IconSize,
Scroller,
showPopup,
SelectPopup,
AnySvelteComponent,
getEventPositionElement
showPopup
} from '@hcengineering/ui'
import { Level } from '@tiptap/extension-heading'
import { createEventDispatcher } from 'svelte'
import textEditorPlugin from '../plugin'
import { FormatMode, FORMAT_MODES, RefInputAction, RefInputActionItem, TextEditorHandler } from '../types'
import EmojiPopup from './EmojiPopup.svelte'
import Emoji from './icons/Emoji.svelte'
import TextStyle from './icons/TextStyle.svelte'
import TextEditor from './TextEditor.svelte'
import { Asset } from '@hcengineering/platform'
import { FormatMode, FORMAT_MODES, RefInputAction, TextEditorHandler } from '../types'
import { headingLevels, mInsertTable } from './extensions'
import { Level } from '@tiptap/extension-heading'
import presentation from '@hcengineering/presentation'
import Header from './icons/Header.svelte'
import IconTable from './icons/IconTable.svelte'
import Attach from './icons/Attach.svelte'
import Bold from './icons/Bold.svelte'
import Code from './icons/Code.svelte'
import CodeBlock from './icons/CodeBlock.svelte'
import Emoji from './icons/Emoji.svelte'
import Header from './icons/Header.svelte'
import IconTable from './icons/IconTable.svelte'
import Italic from './icons/Italic.svelte'
import Link from './icons/Link.svelte'
import ListBullet from './icons/ListBullet.svelte'
import ListNumber from './icons/ListNumber.svelte'
import Quote from './icons/Quote.svelte'
import Strikethrough from './icons/Strikethrough.svelte'
import AddColAfter from './icons/table/AddColAfter.svelte'
import AddColBefore from './icons/table/AddColBefore.svelte'
import AddRowAfter from './icons/table/AddRowAfter.svelte'
import AddRowBefore from './icons/table/AddRowBefore.svelte'
import DeleteCol from './icons/table/DeleteCol.svelte'
import DeleteRow from './icons/table/DeleteRow.svelte'
import DeleteTable from './icons/table/DeleteTable.svelte'
import TextStyle from './icons/TextStyle.svelte'
import LinkPopup from './LinkPopup.svelte'
import StyleButton from './StyleButton.svelte'
import AddRowBefore from './icons/table/AddRowBefore.svelte'
import AddRowAfter from './icons/table/AddRowAfter.svelte'
import AddColBefore from './icons/table/AddColBefore.svelte'
import AddColAfter from './icons/table/AddColAfter.svelte'
import DeleteRow from './icons/table/DeleteRow.svelte'
import DeleteCol from './icons/table/DeleteCol.svelte'
import DeleteTable from './icons/table/DeleteTable.svelte'
import TextEditor from './TextEditor.svelte'
const dispatch = createEventDispatcher()
@ -68,6 +67,7 @@
export let withoutTopBorder = false
export let enableFormatting = false
export let autofocus = false
export let full = false
let textEditor: TextEditor
@ -92,6 +92,9 @@
export function isEmptyContent (): boolean {
return textEditor.isEmptyContent()
}
export function insertText (text: string): void {
textEditor.insertText(text)
}
$: varsStyle =
maxHeight === 'card'
@ -113,8 +116,7 @@
order: number
hidden?: boolean
}
let defActions: RefAction[]
$: defActions = [
const defActions: RefAction[] = [
{
label: textEditorPlugin.string.Attach,
icon: Attach,
@ -159,6 +161,21 @@
// }
]
const client = getClient()
let actions: RefAction[] = []
client.findAll<RefInputActionItem>(textEditorPlugin.class.RefInputActionItem, {}).then(async (res) => {
const cont: RefAction[] = []
for (const r of res) {
cont.push({
label: r.label,
icon: r.icon,
order: r.order ?? 10000,
action: await getResource(r.action)
})
}
actions = defActions.concat(...cont).sort((a, b) => a.order - b.order)
})
function updateFormattingState () {
activeModes = new Set(FORMAT_MODES.filter(textEditor.checkIsActive))
for (const l of headingLevels) {
@ -361,7 +378,12 @@
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="ref-container clear-mins" tabindex="-1" on:click|preventDefault|stopPropagation={() => (needFocus = true)}>
<div
class="ref-container clear-mins"
class:h-full={full}
tabindex="-1"
on:click|preventDefault|stopPropagation={() => (needFocus = true)}
>
{#if isFormatting}
<div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder>
<StyleButton
@ -497,26 +519,19 @@
</div>
</div>
{#if showButtons}
{#if $$slots.right}
<div class="flex-between">
<div class="buttons-group xsmall-gap mt-4">
{#each defActions.filter((it) => it.hidden === undefined || it.hidden === false) as a}
<StyleButton icon={a.icon} size={buttonSize} on:click={(evt) => handleAction(a, evt)} />
{/each}
<slot />
</div>
<div class="buttons-group xsmall-gap mt-4">
<slot name="right" />
</div>
</div>
{:else}
<div class="flex-between">
<div class="buttons-group xsmall-gap mt-4">
{#each defActions.filter((it) => it.hidden === undefined || it.hidden === false) as a}
{#each actions.filter((it) => it.hidden !== true) as a}
<StyleButton icon={a.icon} size={buttonSize} on:click={(evt) => handleAction(a, evt)} />
{/each}
<slot />
</div>
{/if}
{#if $$slots.right}
<div class="buttons-group xsmall-gap mt-4">
<slot name="right" />
</div>
{/if}
</div>
{/if}
</div>

View File

@ -76,6 +76,7 @@
"NotSpecified": "Not specified",
"CreatedOn": "Created",
"Whatsapp": "Whatsapp",
"WhatsappPlaceholder": "Whatsapp"
"WhatsappPlaceholder": "Whatsapp",
"CurrentEmployee": "Current employee"
}
}

View File

@ -76,6 +76,7 @@
"NotSpecified": "Не указан",
"CreatedOn": "Создан",
"Whatsapp": "Whatsapp",
"WhatsappPlaceholder": "Whatsapp"
"WhatsappPlaceholder": "Whatsapp",
"CurrentEmployee": "Текущий сотрудник"
}
}

View File

@ -45,6 +45,7 @@
"@hcengineering/panel": "^0.6.0",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/attachment": "^0.6.1",
"@hcengineering/login-resources": "^0.6.2"
"@hcengineering/login-resources": "^0.6.2",
"@hcengineering/templates": "^0.6.0"
}
}

View File

@ -56,7 +56,14 @@ import PersonRefPresenter from './components/PersonRefPresenter.svelte'
import EmployeeRefPresenter from './components/EmployeeRefPresenter.svelte'
import ChannelFilter from './components/ChannelFilter.svelte'
import contact from './plugin'
import { employeeSort, filterChannelInResult, filterChannelNinResult } from './utils'
import {
employeeSort,
filterChannelInResult,
filterChannelNinResult,
getContactName,
getCurrentEmployeeEmail,
getCurrentEmployeeName
} from './utils'
export {
Channels,
@ -192,6 +199,9 @@ export default async (): Promise<Resources> => ({
GetColorUrl: (uri: string) => uri,
EmployeeSort: employeeSort,
FilterChannelInResult: filterChannelInResult,
FilterChannelNinResult: filterChannelNinResult
FilterChannelNinResult: filterChannelNinResult,
GetCurrentEmployeeName: getCurrentEmployeeName,
GetCurrentEmployeeEmail: getCurrentEmployeeEmail,
GetContactName: getContactName
}
})

View File

@ -14,11 +14,13 @@
// limitations under the License.
//
import contact, { ChannelProvider, Employee, formatName } from '@hcengineering/contact'
import { Doc, ObjQueryType, Ref, Timestamp, toIdMap } from '@hcengineering/core'
import { ChannelProvider, Contact, Employee, EmployeeAccount, formatName } from '@hcengineering/contact'
import { Doc, getCurrentAccount, ObjQueryType, Ref, Timestamp, toIdMap } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { TemplateDataProvider } from '@hcengineering/templates'
import view, { Filter } from '@hcengineering/view'
import { FilterQuery } from '@hcengineering/view-resources'
import contact from './plugin'
const client = getClient()
const channelProviders = client.findAll(contact.class.ChannelProvider, {})
@ -107,3 +109,25 @@ export async function getRefs (filter: Filter, onUpdate: () => void): Promise<Ar
})
return await promise
}
export async function getCurrentEmployeeName (): Promise<string> {
const me = getCurrentAccount() as EmployeeAccount
return formatName(me.name)
}
export async function getCurrentEmployeeEmail (): Promise<string> {
const me = getCurrentAccount() as EmployeeAccount
return me.email
}
export async function getContactName (provider: TemplateDataProvider): Promise<string | undefined> {
const value = provider.get(contact.templateFieldCategory.Contact) as Contact
if (value === undefined) return
const client = getClient()
const hierarchy = client.getHierarchy()
if (hierarchy.isDerived(value._class, contact.class.Person)) {
return formatName(value.name)
} else {
return value.name
}
}

View File

@ -30,6 +30,7 @@
"@hcengineering/platform": "^0.6.8",
"@hcengineering/ui": "^0.6.3",
"@hcengineering/core": "^0.6.21",
"@hcengineering/templates": "^0.6.0",
"@hcengineering/view": "^0.6.2",
"crypto-js": "^4.1.1"
},

View File

@ -33,6 +33,7 @@ import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform'
import type { AnyComponent, IconSize } from '@hcengineering/ui'
import { FilterMode, ViewAction, Viewlet } from '@hcengineering/view'
import { TemplateFieldCategory, TemplateField } from '@hcengineering/templates'
/**
* @public
@ -287,6 +288,15 @@ const contactPlugin = plugin(contactId, {
filter: {
FilterChannelIn: '' as Ref<FilterMode>,
FilterChannelNin: '' as Ref<FilterMode>
},
templateFieldCategory: {
CurrentEmployee: '' as Ref<TemplateFieldCategory>,
Contact: '' as Ref<TemplateFieldCategory>
},
templateField: {
CurrentEmployeeName: '' as Ref<TemplateField>,
CurrentEmployeeEmail: '' as Ref<TemplateField>,
ContactName: '' as Ref<TemplateField>
}
})

View File

@ -47,6 +47,7 @@
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/login": "^0.6.1",
"@hcengineering/core": "^0.6.21",
"@hcengineering/panel": "^0.6.0"
"@hcengineering/panel": "^0.6.0",
"@hcengineering/templates": "^0.6.0"
}
}

View File

@ -14,17 +14,15 @@
// limitations under the License.
-->
<script lang="ts">
import { createQuery, getClient } from '@hcengineering/presentation'
import contact, { Channel, Contact, EmployeeAccount, formatName } from '@hcengineering/contact'
import { Ref, SortingOrder } from '@hcengineering/core'
import { Message, SharedMessage } from '@hcengineering/gmail'
import gmail from '../plugin'
import { Channel, Contact, EmployeeAccount, formatName } from '@hcengineering/contact'
import contact from '@hcengineering/contact'
import plugin, { IconShare, Button, Icon, Label, Scroller } from '@hcengineering/ui'
import { getCurrentAccount, Ref, SortingOrder, Space } from '@hcengineering/core'
import setting from '@hcengineering/setting'
import Messages from './Messages.svelte'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
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 Messages from './Messages.svelte'
export let object: Contact
export let channel: Channel
@ -41,8 +39,7 @@
const messagesQuery = createQuery()
const accauntsQuery = createQuery()
const settingsQuery = createQuery()
const accountId = getCurrentAccount()._id
const notificationClient = NotificationClientImpl.getClient()
function updateMessagesQuery (channelId: Ref<Channel>): void {
@ -67,13 +64,6 @@
})
}
settingsQuery.query(
setting.class.Integration,
{ type: gmail.integrationType.Gmail, space: accountId as string as Ref<Space> },
(res) => {
enabled = res.length > 0
}
)
const client = getClient()
async function share (): Promise<void> {

View File

@ -15,12 +15,15 @@
-->
<script lang="ts">
import contact, { Channel, formatName } from '@hcengineering/contact'
import { Class, Doc, Ref } from '@hcengineering/core'
import { Class, Doc, getCurrentAccount, Ref, Space } from '@hcengineering/core'
import { SharedMessage } from '@hcengineering/gmail'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { getResource } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import setting, { Integration } from '@hcengineering/setting'
import templates, { TemplateDataProvider } from '@hcengineering/templates'
import { Button, eventToHTMLElement, Icon, Label, Panel, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, onDestroy } from 'svelte'
import gmail from '../plugin'
import Chats from './Chats.svelte'
import Connect from './Connect.svelte'
@ -35,7 +38,7 @@
let currentMessage: SharedMessage | undefined = undefined
let channel: Channel | undefined = undefined
const notificationClient = NotificationClientImpl.getClient()
let enabled: boolean
let integration: Integration | undefined
const channelQuery = createQuery()
const dispatch = createEventDispatcher()
@ -71,6 +74,29 @@
await notificationClient.updateLastView(channel._id, channel._class, undefined, true)
}
}
const settingsQuery = createQuery()
const accountId = getCurrentAccount()._id
let templateProvider: TemplateDataProvider | undefined
getResource(templates.function.GetTemplateDataProvider).then((p) => {
templateProvider = p()
})
onDestroy(() => {
templateProvider?.destroy()
})
$: templateProvider && integration && templateProvider.set(setting.templateFieldCategory.Integration, integration)
settingsQuery.query(
setting.class.Integration,
{ type: gmail.integrationType.Gmail, space: accountId as string as Ref<Space> },
(res) => {
integration = res[0]
}
)
</script>
{#if channel && object}
@ -97,7 +123,7 @@
</svelte:fragment>
<svelte:fragment slot="utils">
{#if !enabled}
{#if !integration}
<Button
label={gmail.string.Connect}
kind={'primary'}
@ -113,7 +139,7 @@
{:else if currentMessage}
<FullMessage {currentMessage} bind:newMessage on:close={back} />
{:else}
<Chats {object} {channel} bind:newMessage bind:enabled on:select={selectHandler} />
<Chats {object} {channel} bind:newMessage enabled={integration !== undefined} on:select={selectHandler} />
{/if}
</Panel>
{/if}

View File

@ -15,16 +15,16 @@
<script lang="ts">
import attachmentP, { Attachment } from '@hcengineering/attachment'
import { AttachmentPresenter } from '@hcengineering/attachment-resources'
import { Channel, Contact, formatName } from '@hcengineering/contact'
import contact, { Channel, Contact, formatName } from '@hcengineering/contact'
import { Data, generateId } from '@hcengineering/core'
import { NewMessage, SharedMessage } from '@hcengineering/gmail'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { TextEditor } from '@hcengineering/text-editor'
import { Scroller, IconArrowLeft, IconAttachment, Label } from '@hcengineering/ui'
import Button from '@hcengineering/ui/src/components/Button.svelte'
import EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
import templates, { TemplateDataProvider } from '@hcengineering/templates'
import { StyledTextEditor } from '@hcengineering/text-editor'
import { Button, EditBox, IconArrowLeft, IconAttachment, Label, Scroller } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { createEventDispatcher } from 'svelte'
import plugin from '../plugin'
@ -35,7 +35,7 @@
const notificationClient = NotificationClientImpl.getClient()
let objectId = generateId()
let editor: TextEditor
let editor: StyledTextEditor
let copy: string = ''
const obj: Data<NewMessage> = {
@ -46,6 +46,18 @@
status: 'new'
}
let templateProvider: TemplateDataProvider | undefined
getResource(templates.function.GetTemplateDataProvider).then((p) => {
templateProvider = p()
})
onDestroy(() => {
templateProvider?.destroy()
})
$: templateProvider && templateProvider.set(contact.templateFieldCategory.Contact, object)
async function sendMsg () {
await client.createDoc(
plugin.class.NewMessage,
@ -202,7 +214,7 @@
</div>
{/if}
<div class="input mt-4 clear-mins">
<TextEditor bind:this={editor} bind:content={obj.content} on:blur={editor.submit} />
<StyledTextEditor bind:this={editor} full bind:content={obj.content} maxHeight="panel" on:blur={editor.submit} />
</div>
</div>
</Scroller>
@ -230,7 +242,8 @@
color: #d6d6d6;
caret-color: var(--caret-color);
min-height: 0;
height: calc(100% - 12rem);
margin-bottom: 2rem;
height: 100%;
border-radius: 0.25rem;
:global(.ProseMirror) {

View File

@ -16,21 +16,31 @@
import attachmentP, { Attachment } from '@hcengineering/attachment'
import { AttachmentPresenter } from '@hcengineering/attachment-resources'
import contact, { Channel, Contact, formatName } from '@hcengineering/contact'
import { generateId, Ref, toIdMap } from '@hcengineering/core'
import { generateId, getCurrentAccount, Ref, Space, toIdMap } from '@hcengineering/core'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import setting, { Integration } from '@hcengineering/setting'
import { createQuery, getClient } from '@hcengineering/presentation'
import { TextEditor } from '@hcengineering/text-editor'
import { Icon, IconAttachment, Label, Panel, Scroller } from '@hcengineering/ui'
import Button from '@hcengineering/ui/src/components/Button.svelte'
import EditBox from '@hcengineering/ui/src/components/EditBox.svelte'
import { createEventDispatcher } from 'svelte'
import {
Button,
EditBox,
eventToHTMLElement,
Icon,
IconAttachment,
Label,
Panel,
Scroller,
showPopup
} from '@hcengineering/ui'
import { createEventDispatcher, onDestroy } from 'svelte'
import plugin from '../plugin'
import templates, { TemplateDataProvider } from '@hcengineering/templates'
import Connect from './Connect.svelte'
import { StyledTextEditor } from '@hcengineering/text-editor'
export let value: Contact[] | Contact
const contacts = Array.isArray(value) ? value : [value]
console.log(contacts)
const contactMap = toIdMap(contacts)
const query = createQuery()
@ -51,17 +61,23 @@
const attachmentParentId = generateId()
let editor: TextEditor
let editor: StyledTextEditor
let subject: string = ''
let content: string = ''
let copy: string = ''
let saved = false
async function sendMsg () {
const templateProvider = (await getResource(templates.function.GetTemplateDataProvider))()
if (templateProvider === undefined) return
for (const channel of channels) {
const target = contacts.find((p) => p._id === channel.attachedTo)
if (target === undefined) continue
templateProvider.set(contact.templateFieldCategory.Contact, target)
const message = await templateProvider.fillTemplate(content)
const id = await client.createDoc(plugin.class.NewMessage, plugin.space.Gmail, {
subject,
content,
content: message,
to: channel.value,
status: 'new',
copy: copy
@ -86,6 +102,7 @@
}
)
}
templateProvider.destroy()
}
saved = true
dispatch('close')
@ -166,6 +183,30 @@
if (contact === undefined) return channel.value
return `${formatName(contact.name)} (${channel.value})`
}
const settingsQuery = createQuery()
const accountId = getCurrentAccount()._id
let templateProvider: TemplateDataProvider | undefined
let integration: Integration | undefined
getResource(templates.function.GetTemplateDataProvider).then((p) => {
templateProvider = p()
})
onDestroy(() => {
templateProvider?.destroy()
})
$: templateProvider && integration && templateProvider.set(setting.templateFieldCategory.Integration, integration)
settingsQuery.query(
setting.class.Integration,
{ type: plugin.integrationType.Gmail, space: accountId as string as Ref<Space> },
(res) => {
integration = res[0]
}
)
</script>
<Panel
@ -189,6 +230,18 @@
</div>
</svelte:fragment>
<svelte:fragment slot="utils">
{#if !integration}
<Button
label={plugin.string.Connect}
kind={'primary'}
on:click={(e) => {
showPopup(Connect, {}, eventToHTMLElement(e))
}}
/>
{/if}
</svelte:fragment>
<input
bind:this={inputFile}
multiple
@ -223,13 +276,15 @@
inputFile.click()
}}
/>
<Button
label={plugin.string.Send}
size={'small'}
kind={'primary'}
disabled={channels.length === 0}
on:click={sendMsg}
/>
{#if integration}
<Button
label={plugin.string.Send}
size={'small'}
kind={'primary'}
disabled={channels.length === 0}
on:click={sendMsg}
/>
{/if}
</div>
</div>
</div>
@ -262,7 +317,7 @@
</div>
{/if}
<div class="input mt-4 clear-mins">
<TextEditor bind:this={editor} bind:content on:blur={editor.submit} />
<StyledTextEditor bind:this={editor} full bind:content on:blur={editor.submit} />
</div>
</div>
</Scroller>
@ -291,7 +346,8 @@
color: #d6d6d6;
caret-color: var(--caret-color);
min-height: 0;
height: calc(100% - 12rem);
height: 100%;
margin-bottom: 2rem;
border-radius: 0.25rem;
:global(.ProseMirror) {

View File

@ -17,7 +17,7 @@
"Saving": "Saving...",
"Saved": "Saved",
"Add": "Add",
"LearnMore": "Learn more",
"Value": "Value",
"EnterCurrentPassword": "Enter current password",
"EnterNewPassword": "Enter new password",
"RepeatNewPassword": "Repeat new password",

View File

@ -17,7 +17,7 @@
"Saving": "Сохранение...",
"Saved": "Сохранено",
"Add": "Добавить",
"LearnMore": "Узнать больше",
"Value": "Значение",
"EnterCurrentPassword": "Введите текущий пароль",
"EnterNewPassword": "Введите новый пароль",
"RepeatNewPassword": "Повторите новый пароль",

View File

@ -47,6 +47,7 @@
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/login": "^0.6.1",
"@hcengineering/model": "^0.6.0",
"@hcengineering/templates": "^0.6.0",
"@hcengineering/workbench": "^0.6.2",
"@hcengineering/workbench-resources": "^0.6.1"
}

View File

@ -44,6 +44,7 @@ import StringTypeEditor from './components/typeEditors/StringTypeEditor.svelte'
import WorkspaceSettings from './components/WorkspaceSettings.svelte'
import InviteSetting from './components/InviteSetting.svelte'
import setting from './plugin'
import { getOwnerName, getValue } from './utils'
export { ClassSetting }
@ -101,5 +102,9 @@ export default async (): Promise<Resources> => ({
},
actionImpl: {
DeleteMixin
},
function: {
GetOwnerName: getOwnerName,
GetValue: getValue
}
})

View File

@ -1,5 +1,8 @@
import contact, { EmployeeAccount, formatName } from '@hcengineering/contact'
import { Class, Doc, Hierarchy, Ref } from '@hcengineering/core'
import setting from '@hcengineering/setting'
import { getClient } from '@hcengineering/presentation'
import setting, { Integration } from '@hcengineering/setting'
import { TemplateDataProvider } from '@hcengineering/templates'
function isEditable (hierarchy: Hierarchy, p: Class<Doc>): boolean {
let ancestors = [p._id]
@ -45,3 +48,19 @@ export function filterDescendants (
return _classes.map((p) => p._id)
}
export async function getValue (provider: TemplateDataProvider): Promise<string | undefined> {
const value = provider.get(setting.templateFieldCategory.Integration) as Integration
if (value === undefined) return
return value.value
}
export async function getOwnerName (provider: TemplateDataProvider): Promise<string | undefined> {
const value = provider.get(setting.templateFieldCategory.Integration) as Integration
if (value === undefined) return
const client = getClient()
const employeeAccount = await client.findOne(contact.class.EmployeeAccount, {
_id: value.modifiedBy as Ref<EmployeeAccount>
})
return employeeAccount != null ? formatName(employeeAccount.name) : undefined
}

View File

@ -28,6 +28,7 @@
"dependencies": {
"@hcengineering/platform": "^0.6.8",
"@hcengineering/core": "^0.6.21",
"@hcengineering/templates": "^0.6.0",
"@hcengineering/ui": "^0.6.3"
},
"repository": "https://github.com/hcengineering/anticrm",

View File

@ -17,6 +17,7 @@ import type { Class, Configuration, Doc, Mixin, Ref, Space } from '@hcengineerin
import type { Plugin } from '@hcengineering/platform'
import { Asset, IntlString, plugin, Resource } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui'
import { TemplateFieldCategory, TemplateField } from '@hcengineering/templates'
/**
* @public
@ -145,7 +146,6 @@ export default plugin(settingId, {
Delete: '' as IntlString,
Disconnect: '' as IntlString,
Add: '' as IntlString,
LearnMore: '' as IntlString,
EditProfile: '' as IntlString,
ChangePassword: '' as IntlString,
CurrentPassword: '' as IntlString,
@ -175,5 +175,12 @@ export default plugin(settingId, {
SelectWorkspace: '' as Asset,
Clazz: '' as Asset,
Enums: '' as Asset
},
templateFieldCategory: {
Integration: '' as Ref<TemplateFieldCategory>
},
templateField: {
OwnerName: '' as Ref<TemplateField>,
Value: '' as Ref<TemplateField>
}
})

View File

@ -45,6 +45,7 @@
"@hcengineering/notification-resources": "^0.6.0",
"@hcengineering/attachment": "^0.6.1",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/panel": "^0.6.0"
"@hcengineering/panel": "^0.6.0",
"@hcengineering/templates": "^0.6.0"
}
}

View File

@ -29,6 +29,9 @@
import TelegramIcon from './icons/Telegram.svelte'
import Messages from './Messages.svelte'
import Reconnect from './Reconnect.svelte'
import templates, { TemplateDataProvider } from '@hcengineering/templates'
import { getResource } from '@hcengineering/platform'
import { onDestroy } from 'svelte'
export let _id: Ref<Contact>
export let _class: Ref<Class<Contact>>
@ -54,6 +57,19 @@
}
)
let templateProvider: TemplateDataProvider | undefined
getResource(templates.function.GetTemplateDataProvider).then((p) => {
templateProvider = p()
})
onDestroy(() => {
templateProvider?.destroy()
})
$: templateProvider && object && templateProvider.set(contact.templateFieldCategory.Contact, object)
$: templateProvider && integration && templateProvider.set(setting.templateFieldCategory.Integration, integration)
const query = createQuery()
$: _id &&
_class &&

View File

@ -11,6 +11,7 @@
"SearchTemplate": "search for template",
"TemplatePlaceholder": "New template",
"Title": "Title",
"Message": "Message"
"Message": "Message",
"Field": "Field"
}
}

View File

@ -11,6 +11,7 @@
"SearchTemplate": "Поиск шаблона",
"TemplatePlaceholder": "Новый шаблон",
"Title": "Заголовок",
"Message": "Сообщение"
"Message": "Сообщение",
"Field": "Поле"
}
}

View File

@ -0,0 +1,65 @@
<!--
// 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 { DocumentQuery, FindOptions, IdMap, toIdMap } from '@hcengineering/core'
import type { Asset, IntlString } from '@hcengineering/platform'
import presentation, { createQuery, ObjectPopup } from '@hcengineering/presentation'
import { TemplateField, TemplateFieldCategory } from '@hcengineering/templates'
import { AnySvelteComponent, Label } from '@hcengineering/ui'
import templates from '../plugin'
export let options: FindOptions<TemplateField> | undefined = undefined
export let docQuery: DocumentQuery<TemplateField> | undefined = undefined
export let placeholder: IntlString = presentation.string.Search
export let shadows: boolean = true
export let icon: Asset | AnySvelteComponent | undefined = undefined
const query = createQuery()
let categories: IdMap<TemplateFieldCategory> = new Map()
query.query(templates.class.TemplateFieldCategory, {}, (res) => {
categories = toIdMap(res)
})
</script>
<ObjectPopup
_class={templates.class.TemplateField}
{options}
closeAfterSelect
{placeholder}
searchField={'label'}
{docQuery}
groupBy={'category'}
{shadows}
on:update
on:close
on:changeContent
>
<svelte:fragment slot="item" let:item={field}>
<div class="flex flex-grow overflow-label">
<Label label={field.label} />
</div>
</svelte:fragment>
<svelte:fragment slot="category" let:item={field}>
<div class="flex flex-grow overflow-label">
<span class="fs-medium flex-center gap-2 mt-2 mb-2 ml-2">
<Label label={categories.get(field.category)?.label ?? field.label} />
</span>
</div>
</svelte:fragment>
</ObjectPopup>

View File

@ -19,6 +19,7 @@
import { TextEditorHandler } from '@hcengineering/text-editor'
import { closePopup, EditWithIcon, IconSearch, Label, deviceOptionsStore } from '@hcengineering/ui'
import templates from '../plugin'
import { getTemplateDataProvider } from '../utils'
export let editor: TextEditorHandler
let items: MessageTemplate[] = []
@ -33,8 +34,10 @@
let selected = 0
function dispatchItem (item: MessageTemplate): void {
editor.insertText(item.message)
const provider = getTemplateDataProvider()
async function dispatchItem (item: MessageTemplate): Promise<void> {
const message = await provider.fillTemplate(item.message)
editor.insertText(message)
closePopup()
}

View File

@ -3,8 +3,9 @@
import { getClient, LiveQuery, MessageViewer } from '@hcengineering/presentation'
import { MessageTemplate } from '@hcengineering/templates'
import { StyledTextEditor } from '@hcengineering/text-editor'
import { Button, CircleButton, EditBox, Icon, IconAdd, Label } from '@hcengineering/ui'
import { Button, CircleButton, EditBox, eventToHTMLElement, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
import templatesPlugin from '../plugin'
import FieldPopup from './FieldPopup.svelte'
import TemplateElement from './TemplateElement.svelte'
const client = getClient()
@ -69,6 +70,14 @@
const updateTemplate = (evt: any) => {
newTemplate = { title: newTemplate?.title ?? '', message: evt.detail }
}
function addField (ev: MouseEvent) {
showPopup(FieldPopup, {}, eventToHTMLElement(ev), (res) => {
if (res !== undefined) {
textEditor.insertText(`\${${res._id}}`)
}
})
}
</script>
<div class="antiComponent">
@ -132,15 +141,18 @@
on:click={saveNewTemplate}
/>
</div>
<Button
label={templatesPlugin.string.Cancel}
on:click={() => {
if (mode === Mode.Create) {
newTemplate = undefined
}
mode = Mode.View
}}
/>
<div class="ml-2">
<Button
label={templatesPlugin.string.Cancel}
on:click={() => {
if (mode === Mode.Create) {
newTemplate = undefined
}
mode = Mode.View
}}
/>
</div>
<Button label={templatesPlugin.string.Field} on:click={addField} />
</div>
</StyledTextEditor>
{:else}

View File

@ -19,6 +19,7 @@ import Templates from './components/Templates.svelte'
import { TextEditorHandler } from '@hcengineering/text-editor'
import { showPopup } from '@hcengineering/ui'
import TemplatePopup from './components/TemplatePopup.svelte'
import { getTemplateDataProvider } from './utils'
function ShowTemplates (element: HTMLElement, editor: TextEditorHandler): void {
showPopup(TemplatePopup, { editor }, element)
@ -30,5 +31,8 @@ export default async (): Promise<Resources> => ({
},
action: {
ShowTemplates
},
function: {
GetTemplateDataProvider: getTemplateDataProvider
}
})

View File

@ -29,7 +29,8 @@ export default mergeIds(templatesId, templates, {
SaveTemplate: '' as IntlString,
Suggested: '' as IntlString,
SearchTemplate: '' as IntlString,
TemplatePlaceholder: '' as IntlString
TemplatePlaceholder: '' as IntlString,
Field: '' as IntlString
},
component: {
Templates: '' as AnyComponent

View File

@ -0,0 +1,60 @@
import { generateId, Ref } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import templates, {
TemplateData,
TemplateDataProvider,
TemplateField,
TemplateFieldCategory
} from '@hcengineering/templates'
const fieldRegexp = /\$\{(\S+?)}/gi
const templateData: Map<Ref<TemplateFieldCategory>, TemplateData[]> = new Map()
class TemplateDataProviderImpl implements TemplateDataProvider {
private readonly id = generateId()
set (key: Ref<TemplateFieldCategory>, value: any): void {
const data = templateData.get(key) ?? []
data.unshift({
owner: this.id,
data: value
})
templateData.set(key, data)
}
get (key: Ref<TemplateFieldCategory>): any | undefined {
const data = templateData.get(key) ?? []
const current = data.find((p) => p.owner === this.id)
return current?.data ?? data[0]?.data
}
async fillTemplate (message: string): Promise<string> {
while (true) {
const matched = fieldRegexp.exec(message)
if (matched === null) return message
const client = getClient()
const field = await client.findOne(templates.class.TemplateField, { _id: matched[1] as Ref<TemplateField> })
if (field === undefined) continue
const f = await getResource(field.func)
const result = await f(this)
if (result !== undefined) {
message = message.replaceAll(matched[0], result)
fieldRegexp.lastIndex = 0
}
}
}
destroy (): void {
for (const key of templateData.keys()) {
const data = templateData.get(key) ?? []
const res = data.filter((p) => p.owner === this.id)
templateData.set(key, res)
}
}
}
export function getTemplateDataProvider (): TemplateDataProviderImpl {
return new TemplateDataProviderImpl()
}

View File

@ -14,7 +14,7 @@
//
import type { Class, Doc, Ref, Space } from '@hcengineering/core'
import type { Plugin } from '@hcengineering/platform'
import type { IntlString, Plugin, Resource } from '@hcengineering/platform'
import { Asset, plugin } from '@hcengineering/platform'
/**
@ -25,6 +25,45 @@ export interface MessageTemplate extends Doc {
message: string
}
/**
* @public
*/
export interface TemplateData {
owner: string
data: any
}
/**
* @public
*/
export interface TemplateDataProvider {
set: (key: Ref<TemplateFieldCategory>, value: any) => void
get: (key: Ref<TemplateFieldCategory>) => any | undefined
fillTemplate: (message: string) => Promise<string>
destroy: () => void
}
/**
* @public
*/
export interface TemplateFieldCategory extends Doc {
label: IntlString
}
/**
* @public
*/
export declare type TemplateFieldFunc = (provider: TemplateDataProvider) => Promise<string | undefined>
/**
* @public
*/
export interface TemplateField extends Doc {
category: Ref<TemplateFieldCategory>
label: IntlString
func: Resource<TemplateFieldFunc>
}
/**
* @public
*/
@ -32,7 +71,9 @@ export const templatesId = 'templates' as Plugin
export default plugin(templatesId, {
class: {
MessageTemplate: '' as Ref<Class<MessageTemplate>>
MessageTemplate: '' as Ref<Class<MessageTemplate>>,
TemplateField: '' as Ref<Class<TemplateField>>,
TemplateFieldCategory: '' as Ref<Class<TemplateFieldCategory>>
},
space: {
Templates: '' as Ref<Space>
@ -40,5 +81,8 @@ export default plugin(templatesId, {
icon: {
Templates: '' as Asset,
Template: '' as Asset
},
function: {
GetTemplateDataProvider: '' as Resource<() => TemplateDataProvider>
}
})