Description field fixes (#2263)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-08-30 12:54:03 +07:00 committed by GitHub
parent 251c67f9fe
commit ff0258c7e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 394 additions and 69 deletions

View File

@ -40,8 +40,7 @@
"@types/ws": "^8.2.1", "@types/ws": "^8.2.1",
"@types/xml2js": "~0.4.9", "@types/xml2js": "~0.4.9",
"@types/mime-types": "~2.1.1", "@types/mime-types": "~2.1.1",
"@types/request": "~2.48.8", "@types/request": "~2.48.8"
"@types/email-addresses": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"mongodb": "^4.9.0", "mongodb": "^4.9.0",

View File

@ -68,7 +68,7 @@ export class TCustomer extends TContact implements Customer {
@Prop(Collection(lead.class.Lead), lead.string.Leads) @Prop(Collection(lead.class.Lead), lead.string.Leads)
leads?: number leads?: number
@Prop(TypeString(), core.string.Description) @Prop(TypeMarkup(), core.string.Description)
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
description!: string description!: string

View File

@ -34,6 +34,7 @@ import type {
FilterMode, FilterMode,
HTMLPresenter, HTMLPresenter,
IgnoreActions, IgnoreActions,
InlineAttributEditor,
KeyBinding, KeyBinding,
KeyFilter, KeyFilter,
LinkPresenter, LinkPresenter,
@ -118,6 +119,11 @@ export class TCollectionEditor extends TClass implements CollectionEditor {
inlineEditor?: AnyComponent 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) @Mixin(view.mixin.ArrayEditor, core.class.Class)
export class TArrayEditor extends TClass implements ArrayEditor { export class TArrayEditor extends TClass implements ArrayEditor {
inlineEditor?: AnyComponent inlineEditor?: AnyComponent
@ -292,7 +298,8 @@ export function createModel (builder: Builder): void {
TIgnoreActions, TIgnoreActions,
TPreviewPresenter, TPreviewPresenter,
TLinkPresenter, TLinkPresenter,
TArrayEditor TArrayEditor,
TInlineAttributEditor
) )
classPresenter( classPresenter(
@ -311,6 +318,9 @@ export function createModel (builder: Builder): void {
view.component.StringEditor, view.component.StringEditor,
view.component.StringEditorPopup 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.TypeBoolean, view.component.BooleanPresenter, view.component.BooleanEditor)
classPresenter(builder, core.class.TypeTimestamp, view.component.TimestampPresenter) classPresenter(builder, core.class.TypeTimestamp, view.component.TimestampPresenter)
classPresenter(builder, core.class.TypeDate, view.component.DatePresenter, view.component.DateEditor) classPresenter(builder, core.class.TypeDate, view.component.DatePresenter, view.component.DateEditor)

View File

@ -57,7 +57,8 @@ export default mergeIds(viewId, view, {
YoutubePresenter: '' as AnyComponent, YoutubePresenter: '' as AnyComponent,
GithubPresenter: '' as AnyComponent, GithubPresenter: '' as AnyComponent,
ClassPresenter: '' as AnyComponent, ClassPresenter: '' as AnyComponent,
EnumEditor: '' as AnyComponent EnumEditor: '' as AnyComponent,
HTMLEditor: '' as AnyComponent
}, },
string: { string: {
Table: '' as IntlString, Table: '' as IntlString,

View File

@ -28,14 +28,15 @@
export let justify: 'left' | 'center' = 'center' export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = undefined export let width: string | undefined = undefined
export let allowDeselect = false export let allowDeselect = false
export let focus = true
export let create: ObjectCreate | undefined = undefined export let create: ObjectCreate | undefined = undefined
</script> </script>
<SpaceSelect <SpaceSelect
{create} {create}
focus
focusIndex={-10} focusIndex={-10}
{focus}
{_class} {_class}
spaceQuery={query} spaceQuery={query}
{allowDeselect} {allowDeselect}

View File

@ -148,7 +148,9 @@ export async function getBlobURL (blob: Blob): Promise<string> {
}) })
} }
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 * @public
*/ */
@ -162,6 +164,9 @@ export function getAttributePresenterClass (
attrClass = (attribute.type as RefTo<Doc>).to attrClass = (attribute.type as RefTo<Doc>).to
category = 'attribute' category = 'attribute'
} }
if (hierarchy.isDerived(attrClass, core.class.TypeMarkup)) {
category = 'inplace'
}
if (hierarchy.isDerived(attrClass, core.class.Collection)) { if (hierarchy.isDerived(attrClass, core.class.Collection)) {
attrClass = (attribute.type as Collection<AttachedDoc>).of attrClass = (attribute.type as Collection<AttachedDoc>).of
category = 'collection' category = 'collection'

View File

@ -0,0 +1,88 @@
<!--
// 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.
-->
<script lang="ts">
import type { Asset } from '@anticrm/platform'
import { AnySvelteComponent, Icon, LabelAndProps, tooltip } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
export let icon: Asset | AnySvelteComponent
export let size: 'small' | 'medium' | 'large'
export let selected: boolean = false
export let showTooltip: LabelAndProps | undefined = undefined
export let disabled: boolean = false
const dispatch = createEventDispatcher()
</script>
<button
class="button {size}"
class:selected
{disabled}
use:tooltip={showTooltip}
tabindex="0"
on:mousedown|preventDefault|stopPropagation={() => {
dispatch('click')
}}
>
<div class="icon {size}">
<Icon {icon} {size} />
</div>
</button>
<style lang="scss">
.button {
color: inherit;
border-radius: 0.125rem;
cursor: pointer;
padding: 0.75rem;
.icon {
color: var(--dark-color);
}
&:hover .icon {
color: var(--accent-color);
opacity: 1;
}
&:focus {
border: 1px solid var(--primary-button-focused-border);
box-shadow: 0 0 0 3px var(--primary-button-outline);
.icon {
color: var(--theme-caption-color);
opacity: 1;
}
}
&.selected {
background-color: var(--button-bg-hover);
border-color: var(--button-border-hover);
color: var(--caption-color);
.btn-icon {
color: var(--accent-color);
}
}
}
.small {
width: 1.143em;
height: 1.143em;
}
.medium {
width: 1.429em;
height: 1.429em;
}
.large {
width: 1.715em;
height: 1.715em;
}
</style>

View File

@ -17,12 +17,26 @@
import { Scroller, showPopup } from '@anticrm/ui' import { Scroller, showPopup } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte' 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 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() const dispatch = createEventDispatcher()
@ -32,6 +46,7 @@
export let isScrollable: boolean = true export let isScrollable: boolean = true
export let focusable: boolean = false export let focusable: boolean = false
export let maxHeight: 'max' | 'card' | string = 'max' export let maxHeight: 'max' | 'card' | string = 'max'
export let withoutTopBorder = false
let textEditor: TextEditor let textEditor: TextEditor
@ -42,16 +57,160 @@
textEditor.focus() textEditor.focus()
} }
function openEmojiPopup (ev: MouseEvent & { currentTarget: EventTarget & HTMLDivElement }) { $: varsStyle = maxHeight === 'card' ? 'calc(70vh - 12.5rem)' : maxHeight === 'max' ? 'max-content' : maxHeight
showPopup(EmojiPopup, {}, ev.target as HTMLElement, (emoji) => {
if (!emoji) return let isFormatting = false
textEditor.insertText(emoji) let activeModes = new Set<FormatMode>()
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<void> {
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)
}
</script> </script>
<div class="ref-container"> <div class="ref-container">
{#if isFormatting}
<div class="formatPanel buttons-group xsmall-gap mb-4" class:withoutTopBorder>
<StyleButton
icon={Bold}
size={'small'}
selected={activeModes.has('bold')}
showTooltip={{ label: textEditorPlugin.string.Bold }}
on:click={getToggler(textEditor.toggleBold)}
/>
<StyleButton
icon={Italic}
size={'small'}
selected={activeModes.has('italic')}
showTooltip={{ label: textEditorPlugin.string.Italic }}
on:click={getToggler(textEditor.toggleItalic)}
/>
<StyleButton
icon={Strikethrough}
size={'small'}
selected={activeModes.has('strike')}
showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
on:click={getToggler(textEditor.toggleStrike)}
/>
<StyleButton
icon={Link}
size={'small'}
selected={activeModes.has('link')}
disabled={isSelectionEmpty && !activeModes.has('link')}
showTooltip={{ label: textEditorPlugin.string.Link }}
on:click={formatLink}
/>
<div class="buttons-divider" />
<StyleButton
icon={ListNumber}
size={'small'}
selected={activeModes.has('orderedList')}
showTooltip={{ label: textEditorPlugin.string.OrderedList }}
on:click={getToggler(textEditor.toggleOrderedList)}
/>
<StyleButton
icon={ListBullet}
size={'small'}
selected={activeModes.has('bulletList')}
showTooltip={{ label: textEditorPlugin.string.BulletedList }}
on:click={getToggler(textEditor.toggleBulletList)}
/>
<div class="buttons-divider" />
<StyleButton
icon={Quote}
size={'small'}
selected={activeModes.has('blockquote')}
showTooltip={{ label: textEditorPlugin.string.Blockquote }}
on:click={getToggler(textEditor.toggleBlockquote)}
/>
<div class="buttons-divider" />
<StyleButton
icon={Code}
size={'small'}
selected={activeModes.has('code')}
showTooltip={{ label: textEditorPlugin.string.Code }}
on:click={getToggler(textEditor.toggleCode)}
/>
<StyleButton
icon={CodeBlock}
size={'small'}
selected={activeModes.has('codeBlock')}
showTooltip={{ label: textEditorPlugin.string.CodeBlock }}
on:click={getToggler(textEditor.toggleCodeBlock)}
/>
</div>
{/if}
<div class="textInput" class:focusable> <div class="textInput" class:focusable>
<div class="inputMsg" class:scrollable={isScrollable} style="--texteditor-maxheight: {varsStyle};"> <div class="inputMsg" class:scrollable={isScrollable} style="--texteditor-maxheight: {varsStyle};">
{#if isScrollable} {#if isScrollable}
@ -69,6 +228,7 @@
on:blur on:blur
on:focus on:focus
supportSubmit={false} supportSubmit={false}
on:selection-update={updateFormattingState}
/> />
</Scroller> </Scroller>
{:else} {:else}
@ -85,15 +245,18 @@
on:blur on:blur
on:focus on:focus
supportSubmit={false} supportSubmit={false}
on:selection-update={updateFormattingState}
/> />
{/if} {/if}
</div> </div>
</div> </div>
{#if showButtons} {#if showButtons}
<div class="buttons"> <div class="buttons">
<div class="tool"><TextStyle size={'large'} /></div> {#each defActions as a}
<div class="tool" on:click={openEmojiPopup}><Emoji size={'large'} /></div> <div class="p-1">
<div class="tool"><GIF size={'large'} /></div> <StyleButton icon={a.icon} size={'large'} on:click={(evt) => handleAction(a, evt)} />
</div>
{/each}
<div class="flex-grow"> <div class="flex-grow">
<slot /> <slot />
</div> </div>
@ -146,25 +309,8 @@
} }
} }
.buttons { .buttons {
margin: 10px 0 0 8px;
display: flex; display: flex;
align-items: center; 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;
}
} }
} }
</style> </style>

View File

@ -96,12 +96,16 @@
editor.commands.toggleCodeBlock() editor.commands.toggleCodeBlock()
} }
let needFocus = false let needFocus = false
let focused = false
export function focus (): void { export function focus (): void {
needFocus = true needFocus = true
} }
$: if (editor && needFocus) { $: if (editor && needFocus) {
editor.commands.focus() if (!focused) {
editor.commands.focus()
}
needFocus = false needFocus = false
} }
@ -159,10 +163,12 @@
// force re-render so `editor.isActive` works as expected // force re-render so `editor.isActive` works as expected
editor = editor editor = editor
}, },
onBlur: () => { onBlur: ({ event }) => {
dispatch('blur', editor.getHTML()) focused = false
dispatch('blur', event)
}, },
onFocus: () => { onFocus: () => {
focused = true
dispatch('focus', editor.getHTML()) dispatch('focus', editor.getHTML())
}, },
onUpdate: () => { onUpdate: () => {

View File

@ -37,6 +37,7 @@
{justify} {justify}
allowDeselect allowDeselect
{width} {width}
focus={false}
bind:space={value} bind:space={value}
on:change={(e) => onChange(e.detail)} on:change={(e) => onChange(e.detail)}
/> />

View File

@ -43,6 +43,7 @@
"@anticrm/notification": "~0.6.0", "@anticrm/notification": "~0.6.0",
"@anticrm/presentation": "~0.6.2", "@anticrm/presentation": "~0.6.2",
"@anticrm/setting": "~0.6.1", "@anticrm/setting": "~0.6.1",
"fast-equals": "^2.0.3" "fast-equals": "^2.0.3",
"@anticrm/text-editor": "~0.6.0"
} }
} }

View File

@ -20,6 +20,8 @@
import { Panel } from '@anticrm/panel' import { Panel } from '@anticrm/panel'
import { Asset, getResource, translate } from '@anticrm/platform' import { Asset, getResource, translate } from '@anticrm/platform'
import { import {
AttributeCategory,
AttributeCategoryOrder,
AttributesBar, AttributesBar,
createQuery, createQuery,
getAttributePresenterClass, getAttributePresenterClass,
@ -29,7 +31,7 @@
import { AnyComponent, Button, Component } from '@anticrm/ui' import { AnyComponent, Button, Component } from '@anticrm/ui'
import view from '@anticrm/view' import view from '@anticrm/view'
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import { collectionsFilter, getCollectionCounter, getFiltredKeys } from '../utils' import { fieldsFilter, getCollectionCounter, getFiltredKeys } from '../utils'
import ActionContext from './ActionContext.svelte' import ActionContext from './ActionContext.svelte'
import DocAttributeBar from './DocAttributeBar.svelte' import DocAttributeBar from './DocAttributeBar.svelte'
import UpDownNavigator from './UpDownNavigator.svelte' import UpDownNavigator from './UpDownNavigator.svelte'
@ -73,7 +75,7 @@
} }
let keys: KeyedAttribute[] = [] let keys: KeyedAttribute[] = []
let collectionEditors: { key: KeyedAttribute; editor: AnyComponent }[] = [] let fieldEditors: { key: KeyedAttribute; editor: AnyComponent; category: AttributeCategory }[] = []
let mixins: Mixin<Doc>[] = [] let mixins: Mixin<Doc>[] = []
@ -97,9 +99,10 @@
let ignoreKeys: string[] = [] let ignoreKeys: string[] = []
let allowedCollections: string[] = [] let allowedCollections: string[] = []
let collectionArrays: string[] = [] let collectionArrays: string[] = []
let inplaceAttributes: string[] = []
let ignoreMixins: Set<Ref<Mixin<Doc>>> = new Set<Ref<Mixin<Doc>>>() let ignoreMixins: Set<Ref<Mixin<Doc>>> = new Set<Ref<Mixin<Doc>>>()
async function updateKeys (): Promise<void> { async function updateKeys (showAllMixins: boolean): Promise<void> {
const keysMap = new Map(getFiltredKeys(hierarchy, realObjectClass, ignoreKeys).map((p) => [p.attr._id, p])) const keysMap = new Map(getFiltredKeys(hierarchy, realObjectClass, ignoreKeys).map((p) => [p.attr._id, p]))
for (const m of mixins) { for (const m of mixins) {
const mkeys = getFiltredKeys(hierarchy, m._id, ignoreKeys) const mkeys = getFiltredKeys(hierarchy, m._id, ignoreKeys)
@ -108,17 +111,22 @@
} }
} }
const filtredKeys = Array.from(keysMap.values()) 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 fieldKeys = fieldsFilter(hierarchy, filtredKeys, true, collectionArrays)
const editors: { key: KeyedAttribute; editor: AnyComponent }[] = [] const editors: { key: KeyedAttribute; editor: AnyComponent; category: AttributeCategory }[] = []
for (const k of collectionKeys) { const newInplaceAttributes = []
if (allowedCollections.includes(k.key)) continue for (const k of fieldKeys) {
const editor = await getCollectionEditor(k) if (allowedCollections.includes(k.key.key)) continue
const editor = await getFieldEditor(k.key)
if (editor === undefined) continue 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<Class<Doc>>): Promise<AnyComponent> { async function getEditor (_class: Ref<Class<Doc>>): Promise<AnyComponent> {
@ -129,18 +137,24 @@
} }
let mainEditor: AnyComponent | undefined let mainEditor: AnyComponent | undefined
$: getEditorOrDefault(realObjectClass) $: getEditorOrDefault(realObjectClass, showAllMixins)
async function getEditorOrDefault (_class: Ref<Class<Doc>>): Promise<void> { async function getEditorOrDefault (_class: Ref<Class<Doc>>, showAllMixins: boolean): Promise<void> {
parentClass = getParentClass(_class) parentClass = getParentClass(_class)
mainEditor = await getEditor(_class) mainEditor = await getEditor(_class)
updateKeys() updateKeys(showAllMixins)
} }
async function getCollectionEditor (key: KeyedAttribute): Promise<AnyComponent | undefined> { async function getFieldEditor (key: KeyedAttribute): Promise<AnyComponent | undefined> {
const attrClass = getAttributePresenterClass(hierarchy, key.attr) const attrClass = getAttributePresenterClass(hierarchy, key.attr)
const clazz = hierarchy.getClass(attrClass.attrClass) 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) const editorMixin = hierarchy.as(clazz, mixinRef)
return editorMixin.editor return editorMixin.editor
} }
@ -273,15 +287,15 @@
<Component <Component
is={headerEditor} is={headerEditor}
props={{ object, keys, mixins, ignoreKeys, vertical: dir === 'column', allowedCollections }} props={{ object, keys, mixins, ignoreKeys, vertical: dir === 'column', allowedCollections }}
on:update={updateKeys} on:update={() => updateKeys(showAllMixins)}
/> />
{:else if dir === 'column'} {:else if dir === 'column'}
<DocAttributeBar <DocAttributeBar
{object} {object}
{mixins} {mixins}
ignoreKeys={[...ignoreKeys, ...collectionArrays]} ignoreKeys={[...ignoreKeys, ...collectionArrays, ...inplaceAttributes]}
{allowedCollections} {allowedCollections}
on:update={updateKeys} on:update={() => updateKeys(showAllMixins)}
/> />
{:else} {:else}
<AttributesBar {object} _class={realObjectClass} {keys} /> <AttributesBar {object} _class={realObjectClass} {keys} />
@ -303,7 +317,7 @@
}} }}
/> />
{/if} {/if}
{#each collectionEditors as collection} {#each fieldEditors as collection}
{#if collection.editor} {#if collection.editor}
<div class="mt-6"> <div class="mt-6">
<Component <Component

View File

@ -0,0 +1,41 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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 { Doc } from '@anticrm/core'
import { getAttribute, getClient, KeyedAttribute, updateAttribute } from '@anticrm/presentation'
import { FullDescriptionBox } from '@anticrm/text-editor'
// export let objectId: Ref<Doc>
// export let _class: Ref<Class<Doc>>
export let object: Doc
// export let space: Ref<Space>
export let key: KeyedAttribute
$: description = getAttribute(getClient(), object, key)
</script>
{#key description}
<FullDescriptionBox
label={key.attr.label}
content={description}
on:save={(res) => {
if (res.detail != null) {
updateAttribute(getClient(), object, object._class, key, res.detail)
}
}}
/>
{/key}

View File

@ -52,6 +52,7 @@ import TimestampPresenter from './components/TimestampPresenter.svelte'
import UpDownNavigator from './components/UpDownNavigator.svelte' import UpDownNavigator from './components/UpDownNavigator.svelte'
import ViewletSettingButton from './components/ViewletSettingButton.svelte' import ViewletSettingButton from './components/ViewletSettingButton.svelte'
import ValueSelector from './components/ValueSelector.svelte' import ValueSelector from './components/ValueSelector.svelte'
import HTMLEditor from './components/HTMLEditor.svelte'
import { import {
afterResult, afterResult,
beforeResult, beforeResult,
@ -139,7 +140,8 @@ export default async (): Promise<Resources> => ({
BooleanTruePresenter, BooleanTruePresenter,
EnumEditor, EnumEditor,
FilterTypePopup, FilterTypePopup,
ValueSelector ValueSelector,
HTMLEditor
}, },
popup: { popup: {
PositionElementAlignment PositionElementAlignment

View File

@ -29,7 +29,7 @@ import core, {
} from '@anticrm/core' } from '@anticrm/core'
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import { getResource } 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 { AnyComponent, ErrorPresenter, getCurrentLocation, getPlatformColorForText, locationToUrl } from '@anticrm/ui'
import type { BuildModelOptions, Viewlet } from '@anticrm/view' import type { BuildModelOptions, Viewlet } from '@anticrm/view'
import view, { AttributeModel, BuildModelKey } from '@anticrm/view' import view, { AttributeModel, BuildModelKey } from '@anticrm/view'
@ -406,18 +406,20 @@ export function getFiltredKeys (
return filterKeys(hierarchy, keys, ignoreKeys) return filterKeys(hierarchy, keys, ignoreKeys)
} }
export function collectionsFilter ( export function fieldsFilter (
hierarchy: Hierarchy, hierarchy: Hierarchy,
keys: KeyedAttribute[], keys: KeyedAttribute[],
get: boolean, get: boolean,
include: string[] include: string[]
): KeyedAttribute[] { ): Array<{ key: KeyedAttribute, category: AttributeCategory }> {
const result: KeyedAttribute[] = [] const result: Array<{ key: KeyedAttribute, category: AttributeCategory }> = []
for (const key of keys) { for (const key of keys) {
const cl = getAttributePresenterClass(hierarchy, key.attr)
if (include.includes(key.key)) { if (include.includes(key.key)) {
result.push(key) result.push({ key: key, category: cl.category })
} else if (isCollectionAttr(hierarchy, key) === get) { } else if ((cl.category === 'collection') === get || (cl.category === 'inplace') === get) {
result.push(key) result.push({ key, category: cl.category })
} }
} }
return result return result

View File

@ -98,6 +98,13 @@ export interface CollectionEditor extends Class<Doc> {
inlineEditor?: AnyComponent inlineEditor?: AnyComponent
} }
/**
* @public
*/
export interface InlineAttributEditor extends Class<Doc> {
editor: AnyComponent
}
/** /**
* @public * @public
*/ */
@ -400,6 +407,7 @@ const view = plugin(viewId, {
AttributeEditor: '' as Ref<Mixin<AttributeEditor>>, AttributeEditor: '' as Ref<Mixin<AttributeEditor>>,
CollectionPresenter: '' as Ref<Mixin<CollectionPresenter>>, CollectionPresenter: '' as Ref<Mixin<CollectionPresenter>>,
CollectionEditor: '' as Ref<Mixin<CollectionEditor>>, CollectionEditor: '' as Ref<Mixin<CollectionEditor>>,
InlineAttributEditor: '' as Ref<Mixin<InlineAttributEditor>>,
ArrayEditor: '' as Ref<Mixin<ArrayEditor>>, ArrayEditor: '' as Ref<Mixin<ArrayEditor>>,
AttributePresenter: '' as Ref<Mixin<AttributePresenter>>, AttributePresenter: '' as Ref<Mixin<AttributePresenter>>,
ObjectEditor: '' as Ref<Mixin<ObjectEditor>>, ObjectEditor: '' as Ref<Mixin<ObjectEditor>>,