From ff0258c7e18552e1044be0c7b24b619977f24af2 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Tue, 30 Aug 2022 12:54:03 +0700 Subject: [PATCH] Description field fixes (#2263) Signed-off-by: Andrey Sobolev --- dev/tool/package.json | 3 +- models/lead/src/index.ts | 2 +- models/view/src/index.ts | 12 +- models/view/src/plugin.ts | 3 +- .../src/components/SpaceSelector.svelte | 3 +- packages/presentation/src/utils.ts | 7 +- .../src/components/StyleButton.svelte | 88 ++++++++ .../src/components/StyledTextEditor.svelte | 206 +++++++++++++++--- .../src/components/TextEditor.svelte | 12 +- .../src/components/DepartmentEditor.svelte | 1 + plugins/view-resources/package.json | 3 +- .../src/components/EditDoc.svelte | 54 +++-- .../src/components/HTMLEditor.svelte | 41 ++++ plugins/view-resources/src/index.ts | 4 +- plugins/view-resources/src/utils.ts | 16 +- plugins/view/src/index.ts | 8 + 16 files changed, 394 insertions(+), 69 deletions(-) create mode 100644 packages/text-editor/src/components/StyleButton.svelte create mode 100644 plugins/view-resources/src/components/HTMLEditor.svelte diff --git a/dev/tool/package.json b/dev/tool/package.json index 81afbf1be3..4039672781 100644 --- a/dev/tool/package.json +++ b/dev/tool/package.json @@ -40,8 +40,7 @@ "@types/ws": "^8.2.1", "@types/xml2js": "~0.4.9", "@types/mime-types": "~2.1.1", - "@types/request": "~2.48.8", - "@types/email-addresses": "^3.0.0" + "@types/request": "~2.48.8" }, "dependencies": { "mongodb": "^4.9.0", diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index 8a8a426fc7..595b136aed 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -68,7 +68,7 @@ export class TCustomer extends TContact implements Customer { @Prop(Collection(lead.class.Lead), lead.string.Leads) leads?: number - @Prop(TypeString(), core.string.Description) + @Prop(TypeMarkup(), core.string.Description) @Index(IndexKind.FullText) description!: string diff --git a/models/view/src/index.ts b/models/view/src/index.ts index 5e25dc9e9f..86c22b049d 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -34,6 +34,7 @@ import type { FilterMode, HTMLPresenter, IgnoreActions, + InlineAttributEditor, KeyBinding, KeyFilter, LinkPresenter, @@ -118,6 +119,11 @@ export class TCollectionEditor extends TClass implements CollectionEditor { inlineEditor?: AnyComponent } +@Mixin(view.mixin.InlineAttributEditor, core.class.Class) +export class TInlineAttributEditor extends TClass implements InlineAttributEditor { + editor!: AnyComponent +} + @Mixin(view.mixin.ArrayEditor, core.class.Class) export class TArrayEditor extends TClass implements ArrayEditor { inlineEditor?: AnyComponent @@ -292,7 +298,8 @@ export function createModel (builder: Builder): void { TIgnoreActions, TPreviewPresenter, TLinkPresenter, - TArrayEditor + TArrayEditor, + TInlineAttributEditor ) classPresenter( @@ -311,6 +318,9 @@ export function createModel (builder: Builder): void { view.component.StringEditor, view.component.StringEditorPopup ) + builder.mixin(core.class.TypeMarkup, core.class.Class, view.mixin.InlineAttributEditor, { + editor: view.component.HTMLEditor + }) classPresenter(builder, core.class.TypeBoolean, view.component.BooleanPresenter, view.component.BooleanEditor) classPresenter(builder, core.class.TypeTimestamp, view.component.TimestampPresenter) classPresenter(builder, core.class.TypeDate, view.component.DatePresenter, view.component.DateEditor) diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index 43d99fadf3..f62584d26a 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -57,7 +57,8 @@ export default mergeIds(viewId, view, { YoutubePresenter: '' as AnyComponent, GithubPresenter: '' as AnyComponent, ClassPresenter: '' as AnyComponent, - EnumEditor: '' as AnyComponent + EnumEditor: '' as AnyComponent, + HTMLEditor: '' as AnyComponent }, string: { Table: '' as IntlString, diff --git a/packages/presentation/src/components/SpaceSelector.svelte b/packages/presentation/src/components/SpaceSelector.svelte index 8785ed7c67..9d6f9508cd 100644 --- a/packages/presentation/src/components/SpaceSelector.svelte +++ b/packages/presentation/src/components/SpaceSelector.svelte @@ -28,14 +28,15 @@ export let justify: 'left' | 'center' = 'center' export let width: string | undefined = undefined export let allowDeselect = false + export let focus = true export let create: ObjectCreate | undefined = undefined { }) } -export type AttributeCategory = 'attribute' | 'collection' | 'array' +export type AttributeCategory = 'attribute' | 'inplace' | 'collection' | 'array' + +export const AttributeCategoryOrder = { attribute: 0, inplace: 1, collection: 2, array: 2 } /** * @public */ @@ -162,6 +164,9 @@ export function getAttributePresenterClass ( attrClass = (attribute.type as RefTo).to category = 'attribute' } + if (hierarchy.isDerived(attrClass, core.class.TypeMarkup)) { + category = 'inplace' + } if (hierarchy.isDerived(attrClass, core.class.Collection)) { attrClass = (attribute.type as Collection).of category = 'collection' diff --git a/packages/text-editor/src/components/StyleButton.svelte b/packages/text-editor/src/components/StyleButton.svelte new file mode 100644 index 0000000000..bfc6d46f09 --- /dev/null +++ b/packages/text-editor/src/components/StyleButton.svelte @@ -0,0 +1,88 @@ + + + + + + diff --git a/packages/text-editor/src/components/StyledTextEditor.svelte b/packages/text-editor/src/components/StyledTextEditor.svelte index 39e5abe161..56386f546f 100644 --- a/packages/text-editor/src/components/StyledTextEditor.svelte +++ b/packages/text-editor/src/components/StyledTextEditor.svelte @@ -17,12 +17,26 @@ import { Scroller, showPopup } from '@anticrm/ui' import { createEventDispatcher } from 'svelte' - import Emoji from './icons/Emoji.svelte' - import GIF from './icons/GIF.svelte' - import TextStyle from './icons/TextStyle.svelte' - import EmojiPopup from './EmojiPopup.svelte' - import TextEditor from './TextEditor.svelte' import textEditorPlugin from '../plugin' + 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 '@anticrm/platform' + import { AnySvelteComponent } from '@anticrm/ui' + import { FormatMode, FORMAT_MODES, RefInputAction, TextEditorHandler } from '../types' + import Bold from './icons/Bold.svelte' + import Code from './icons/Code.svelte' + import CodeBlock from './icons/CodeBlock.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 LinkPopup from './LinkPopup.svelte' + import StyleButton from './StyleButton.svelte' const dispatch = createEventDispatcher() @@ -32,6 +46,7 @@ export let isScrollable: boolean = true export let focusable: boolean = false export let maxHeight: 'max' | 'card' | string = 'max' + export let withoutTopBorder = false let textEditor: TextEditor @@ -42,16 +57,160 @@ textEditor.focus() } - function openEmojiPopup (ev: MouseEvent & { currentTarget: EventTarget & HTMLDivElement }) { - showPopup(EmojiPopup, {}, ev.target as HTMLElement, (emoji) => { - if (!emoji) return - textEditor.insertText(emoji) + $: varsStyle = maxHeight === 'card' ? 'calc(70vh - 12.5rem)' : maxHeight === 'max' ? 'max-content' : maxHeight + + let isFormatting = false + let activeModes = new Set() + let isSelectionEmpty = true + + interface RefAction { + label: IntlString + icon: Asset | AnySvelteComponent + action: RefInputAction + order: number + } + const defActions: RefAction[] = [ + { + label: textEditorPlugin.string.TextStyle, + icon: TextStyle, + action: () => { + isFormatting = !isFormatting + textEditor.focus() + }, + order: 2000 + }, + { + label: textEditorPlugin.string.Emoji, + icon: Emoji, + action: (element) => { + showPopup( + EmojiPopup, + {}, + element, + (emoji) => { + if (!emoji) return + textEditor.insertText(emoji) + textEditor.focus() + }, + () => {} + ) + }, + order: 3000 + } + // { + // label: textEditorPlugin.string.GIF, + // icon: GIF, + // action: () => {}, + // order: 4000 + // } + ] + + function updateFormattingState () { + activeModes = new Set(FORMAT_MODES.filter(textEditor.checkIsActive)) + isSelectionEmpty = textEditor.checkIsSelectionEmpty() + } + + function getToggler (toggle: () => void) { + return () => { + toggle() + textEditor.focus() + updateFormattingState() + } + } + + async function formatLink (): Promise { + const link = textEditor.getLink() + + showPopup(LinkPopup, { link }, undefined, undefined, (newLink) => { + if (newLink === '') { + textEditor.unsetLink() + } else { + textEditor.setLink(newLink) + } }) } - $: varsStyle = maxHeight === 'card' ? 'calc(70vh - 12.5rem)' : maxHeight === 'max' ? 'max-content' : maxHeight + const editorHandler: TextEditorHandler = { + insertText: (text) => { + textEditor.insertText(text) + } + } + function handleAction (a: RefAction, evt?: Event): void { + a.action(evt?.target as HTMLElement, editorHandler) + }
+ {#if isFormatting} +
+ + + + +
+ + +
+ +
+ + +
+ {/if}
{#if isScrollable} @@ -69,6 +228,7 @@ on:blur on:focus supportSubmit={false} + on:selection-update={updateFormattingState} /> {:else} @@ -85,15 +245,18 @@ on:blur on:focus supportSubmit={false} + on:selection-update={updateFormattingState} /> {/if}
{#if showButtons}
-
-
-
+ {#each defActions as a} +
+ handleAction(a, evt)} /> +
+ {/each}
@@ -146,25 +309,8 @@ } } .buttons { - margin: 10px 0 0 8px; display: flex; align-items: center; - - .tool { - display: flex; - justify-content: center; - align-items: center; - width: 20px; - height: 20px; - opacity: 0.3; - cursor: pointer; - &:hover { - opacity: 1; - } - } - .tool + .tool { - margin-left: 16px; - } } } diff --git a/packages/text-editor/src/components/TextEditor.svelte b/packages/text-editor/src/components/TextEditor.svelte index 32cc7acfe0..a967bbc009 100644 --- a/packages/text-editor/src/components/TextEditor.svelte +++ b/packages/text-editor/src/components/TextEditor.svelte @@ -96,12 +96,16 @@ editor.commands.toggleCodeBlock() } let needFocus = false + + let focused = false export function focus (): void { needFocus = true } $: if (editor && needFocus) { - editor.commands.focus() + if (!focused) { + editor.commands.focus() + } needFocus = false } @@ -159,10 +163,12 @@ // force re-render so `editor.isActive` works as expected editor = editor }, - onBlur: () => { - dispatch('blur', editor.getHTML()) + onBlur: ({ event }) => { + focused = false + dispatch('blur', event) }, onFocus: () => { + focused = true dispatch('focus', editor.getHTML()) }, onUpdate: () => { diff --git a/plugins/hr-resources/src/components/DepartmentEditor.svelte b/plugins/hr-resources/src/components/DepartmentEditor.svelte index c14185aa5d..b8c2dfe6cc 100644 --- a/plugins/hr-resources/src/components/DepartmentEditor.svelte +++ b/plugins/hr-resources/src/components/DepartmentEditor.svelte @@ -37,6 +37,7 @@ {justify} allowDeselect {width} + focus={false} bind:space={value} on:change={(e) => onChange(e.detail)} /> diff --git a/plugins/view-resources/package.json b/plugins/view-resources/package.json index 932b1d328d..1b393296f6 100644 --- a/plugins/view-resources/package.json +++ b/plugins/view-resources/package.json @@ -43,6 +43,7 @@ "@anticrm/notification": "~0.6.0", "@anticrm/presentation": "~0.6.2", "@anticrm/setting": "~0.6.1", - "fast-equals": "^2.0.3" + "fast-equals": "^2.0.3", + "@anticrm/text-editor": "~0.6.0" } } diff --git a/plugins/view-resources/src/components/EditDoc.svelte b/plugins/view-resources/src/components/EditDoc.svelte index 6ef29a6a34..33f557a01d 100644 --- a/plugins/view-resources/src/components/EditDoc.svelte +++ b/plugins/view-resources/src/components/EditDoc.svelte @@ -20,6 +20,8 @@ import { Panel } from '@anticrm/panel' import { Asset, getResource, translate } from '@anticrm/platform' import { + AttributeCategory, + AttributeCategoryOrder, AttributesBar, createQuery, getAttributePresenterClass, @@ -29,7 +31,7 @@ import { AnyComponent, Button, Component } from '@anticrm/ui' import view from '@anticrm/view' import { createEventDispatcher, onDestroy } from 'svelte' - import { collectionsFilter, getCollectionCounter, getFiltredKeys } from '../utils' + import { fieldsFilter, getCollectionCounter, getFiltredKeys } from '../utils' import ActionContext from './ActionContext.svelte' import DocAttributeBar from './DocAttributeBar.svelte' import UpDownNavigator from './UpDownNavigator.svelte' @@ -73,7 +75,7 @@ } let keys: KeyedAttribute[] = [] - let collectionEditors: { key: KeyedAttribute; editor: AnyComponent }[] = [] + let fieldEditors: { key: KeyedAttribute; editor: AnyComponent; category: AttributeCategory }[] = [] let mixins: Mixin[] = [] @@ -97,9 +99,10 @@ let ignoreKeys: string[] = [] let allowedCollections: string[] = [] let collectionArrays: string[] = [] + let inplaceAttributes: string[] = [] let ignoreMixins: Set>> = new Set>>() - async function updateKeys (): Promise { + async function updateKeys (showAllMixins: boolean): Promise { const keysMap = new Map(getFiltredKeys(hierarchy, realObjectClass, ignoreKeys).map((p) => [p.attr._id, p])) for (const m of mixins) { const mkeys = getFiltredKeys(hierarchy, m._id, ignoreKeys) @@ -108,17 +111,22 @@ } } const filtredKeys = Array.from(keysMap.values()) - keys = collectionsFilter(hierarchy, filtredKeys, false, allowedCollections) + keys = fieldsFilter(hierarchy, filtredKeys, false, allowedCollections).map((it) => it.key) - const collectionKeys = collectionsFilter(hierarchy, filtredKeys, true, collectionArrays) - const editors: { key: KeyedAttribute; editor: AnyComponent }[] = [] - for (const k of collectionKeys) { - if (allowedCollections.includes(k.key)) continue - const editor = await getCollectionEditor(k) + const fieldKeys = fieldsFilter(hierarchy, filtredKeys, true, collectionArrays) + const editors: { key: KeyedAttribute; editor: AnyComponent; category: AttributeCategory }[] = [] + const newInplaceAttributes = [] + for (const k of fieldKeys) { + if (allowedCollections.includes(k.key.key)) continue + const editor = await getFieldEditor(k.key) if (editor === undefined) continue - editors.push({ key: k, editor }) + if (k.category === 'inplace') { + newInplaceAttributes.push(k.key.key) + } + editors.push({ key: k.key, editor, category: k.category }) } - collectionEditors = editors + inplaceAttributes = newInplaceAttributes + fieldEditors = editors.sort((a, b) => AttributeCategoryOrder[a.category] - AttributeCategoryOrder[b.category]) } async function getEditor (_class: Ref>): Promise { @@ -129,18 +137,24 @@ } let mainEditor: AnyComponent | undefined - $: getEditorOrDefault(realObjectClass) + $: getEditorOrDefault(realObjectClass, showAllMixins) - async function getEditorOrDefault (_class: Ref>): Promise { + async function getEditorOrDefault (_class: Ref>, showAllMixins: boolean): Promise { parentClass = getParentClass(_class) mainEditor = await getEditor(_class) - updateKeys() + updateKeys(showAllMixins) } - async function getCollectionEditor (key: KeyedAttribute): Promise { + async function getFieldEditor (key: KeyedAttribute): Promise { const attrClass = getAttributePresenterClass(hierarchy, key.attr) const clazz = hierarchy.getClass(attrClass.attrClass) - const mixinRef = attrClass.category === 'array' ? view.mixin.ArrayEditor : view.mixin.CollectionEditor + const mix = { + array: view.mixin.ArrayEditor, + collection: view.mixin.CollectionEditor, + inplace: view.mixin.InlineAttributEditor, + attribute: view.mixin.AttributeEditor + } + const mixinRef = mix[attrClass.category] const editorMixin = hierarchy.as(clazz, mixinRef) return editorMixin.editor } @@ -273,15 +287,15 @@ updateKeys(showAllMixins)} /> {:else if dir === 'column'} updateKeys(showAllMixins)} /> {:else} @@ -303,7 +317,7 @@ }} /> {/if} - {#each collectionEditors as collection} + {#each fieldEditors as collection} {#if collection.editor}
+ + +{#key description} + { + if (res.detail != null) { + updateAttribute(getClient(), object, object._class, key, res.detail) + } + }} + /> +{/key} diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index 05b87e611d..4fbf002974 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -52,6 +52,7 @@ import TimestampPresenter from './components/TimestampPresenter.svelte' import UpDownNavigator from './components/UpDownNavigator.svelte' import ViewletSettingButton from './components/ViewletSettingButton.svelte' import ValueSelector from './components/ValueSelector.svelte' +import HTMLEditor from './components/HTMLEditor.svelte' import { afterResult, beforeResult, @@ -139,7 +140,8 @@ export default async (): Promise => ({ BooleanTruePresenter, EnumEditor, FilterTypePopup, - ValueSelector + ValueSelector, + HTMLEditor }, popup: { PositionElementAlignment diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts index b3ce261f10..e407236832 100644 --- a/plugins/view-resources/src/utils.ts +++ b/plugins/view-resources/src/utils.ts @@ -29,7 +29,7 @@ import core, { } from '@anticrm/core' import type { IntlString } from '@anticrm/platform' import { getResource } from '@anticrm/platform' -import { getAttributePresenterClass, KeyedAttribute } from '@anticrm/presentation' +import { AttributeCategory, getAttributePresenterClass, KeyedAttribute } from '@anticrm/presentation' import { AnyComponent, ErrorPresenter, getCurrentLocation, getPlatformColorForText, locationToUrl } from '@anticrm/ui' import type { BuildModelOptions, Viewlet } from '@anticrm/view' import view, { AttributeModel, BuildModelKey } from '@anticrm/view' @@ -406,18 +406,20 @@ export function getFiltredKeys ( return filterKeys(hierarchy, keys, ignoreKeys) } -export function collectionsFilter ( +export function fieldsFilter ( hierarchy: Hierarchy, keys: KeyedAttribute[], get: boolean, include: string[] -): KeyedAttribute[] { - const result: KeyedAttribute[] = [] +): Array<{ key: KeyedAttribute, category: AttributeCategory }> { + const result: Array<{ key: KeyedAttribute, category: AttributeCategory }> = [] + for (const key of keys) { + const cl = getAttributePresenterClass(hierarchy, key.attr) if (include.includes(key.key)) { - result.push(key) - } else if (isCollectionAttr(hierarchy, key) === get) { - result.push(key) + result.push({ key: key, category: cl.category }) + } else if ((cl.category === 'collection') === get || (cl.category === 'inplace') === get) { + result.push({ key, category: cl.category }) } } return result diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 7f56127220..38a83b26de 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -98,6 +98,13 @@ export interface CollectionEditor extends Class { inlineEditor?: AnyComponent } +/** + * @public + */ +export interface InlineAttributEditor extends Class { + editor: AnyComponent +} + /** * @public */ @@ -400,6 +407,7 @@ const view = plugin(viewId, { AttributeEditor: '' as Ref>, CollectionPresenter: '' as Ref>, CollectionEditor: '' as Ref>, + InlineAttributEditor: '' as Ref>, ArrayEditor: '' as Ref>, AttributePresenter: '' as Ref>, ObjectEditor: '' as Ref>,