Files
huly-platform/plugins/view-resources/src/components/EditDoc.svelte
T
2022-10-26 12:16:32 +07:00

353 lines
12 KiB
Svelte

<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 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 contact, { formatName } from '@hcengineering/contact'
import { Class, ClassifierKind, Doc, Mixin, Obj, Ref } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { Asset, getResource, translate } from '@hcengineering/platform'
import {
AttributeCategory,
AttributeCategoryOrder,
AttributesBar,
createQuery,
getAttributePresenterClass,
getClient,
KeyedAttribute
} from '@hcengineering/presentation'
import { AnyComponent, Button, Component, IconMoreH, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { createEventDispatcher, onDestroy } from 'svelte'
import { ContextMenu } from '..'
import { categorizeFields, getCollectionCounter, getFiltredKeys } from '../utils'
import ActionContext from './ActionContext.svelte'
import DocAttributeBar from './DocAttributeBar.svelte'
import UpDownNavigator from './UpDownNavigator.svelte'
export let _id: Ref<Doc>
export let _class: Ref<Class<Doc>>
let realObjectClass: Ref<Class<Doc>> = _class
let lastId: Ref<Doc> = _id
let lastClass: Ref<Class<Doc>> = _class
let object: Doc
let parentClass: Ref<Class<Doc>>
const client = getClient()
const hierarchy = client.getHierarchy()
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
$: read(_id)
function read (_id: Ref<Doc>) {
if (lastId !== _id) {
const prev = lastId
const prevClass = lastClass
lastId = _id
lastClass = _class
notificationClient.then((client) => client.updateLastView(prev, prevClass))
}
}
onDestroy(async () => {
notificationClient.then((client) => client.updateLastView(_id, _class))
})
const query = createQuery()
$: if (_id && _class) {
query.query(_class, { _id }, (result) => {
object = result[0]
realObjectClass = object._class
})
} else {
query.unsubscribe()
}
let keys: KeyedAttribute[] = []
let fieldEditors: { key: KeyedAttribute; editor: AnyComponent; category: AttributeCategory }[] = []
let mixins: Mixin<Doc>[] = []
let showAllMixins = false
const dispatch = createEventDispatcher()
function getMixins (parentClass: Ref<Class<Doc>>, object: Doc, showAllMixins: boolean): void {
if (object === undefined || parentClass === undefined) return
const descendants = hierarchy.getDescendants(parentClass).map((p) => hierarchy.getClass(p))
mixins = descendants.filter(
(m) =>
m.kind === ClassifierKind.MIXIN &&
!ignoreMixins.has(m._id) &&
(hierarchy.hasMixin(object, m._id) || showAllMixins)
)
}
$: getMixins(parentClass, object, showAllMixins)
let ignoreKeys: string[] = []
let allowedCollections: string[] = []
let collectionArrays: string[] = []
let inplaceAttributes: string[] = []
let ignoreMixins: Set<Ref<Mixin<Doc>>> = new Set<Ref<Mixin<Doc>>>()
async function updateKeys (showAllMixins: boolean): Promise<void> {
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)
for (const key of mkeys) {
keysMap.set(key.attr._id, key)
}
}
const filtredKeys = Array.from(keysMap.values())
const { attributes, collections } = categorizeFields(hierarchy, filtredKeys, collectionArrays, allowedCollections)
keys = attributes.map((it) => it.key)
const editors: { key: KeyedAttribute; editor: AnyComponent; category: AttributeCategory }[] = []
const newInplaceAttributes = []
for (const k of collections) {
if (allowedCollections.includes(k.key.key)) continue
const editor = await getFieldEditor(k.key)
if (editor === undefined) continue
if (k.category === 'inplace') {
newInplaceAttributes.push(k.key.key)
}
editors.push({ key: k.key, editor, category: k.category })
}
inplaceAttributes = newInplaceAttributes
fieldEditors = editors.sort((a, b) => AttributeCategoryOrder[a.category] - AttributeCategoryOrder[b.category])
}
async function getEditor (_class: Ref<Class<Doc>>): Promise<AnyComponent> {
const clazz = hierarchy.getClass(_class)
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditor)
if (editorMixin?.editor == null && clazz.extends != null) return getEditor(clazz.extends)
return editorMixin.editor
}
let mainEditor: AnyComponent | undefined
$: getEditorOrDefault(realObjectClass, showAllMixins)
async function getEditorOrDefault (_class: Ref<Class<Doc>>, showAllMixins: boolean): Promise<void> {
parentClass = getParentClass(_class)
mainEditor = await getEditor(_class)
updateKeys(showAllMixins)
}
async function getFieldEditor (key: KeyedAttribute): Promise<AnyComponent | undefined> {
const attrClass = getAttributePresenterClass(hierarchy, key.attr)
const clazz = hierarchy.getClass(attrClass.attrClass)
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
}
function getIcon (_class: Ref<Class<Obj>> | undefined): Asset | undefined {
if (_class === undefined) return undefined
let clazz = hierarchy.getClass(_class)
if (clazz.icon !== undefined) return clazz.icon
while (clazz.extends !== undefined) {
clazz = hierarchy.getClass(clazz.extends)
if (clazz.icon !== undefined) {
return clazz.icon
}
}
throw new Error(`Icon not found for ${_class}`)
}
$: icon = getIcon(realObjectClass)
function getParentClass (_class: Ref<Class<Doc>>): Ref<Class<Doc>> {
const baseDomain = hierarchy.getDomain(_class)
const ancestors = hierarchy.getAncestors(_class)
let result: Ref<Class<Doc>> = _class
for (const ancestor of ancestors) {
try {
const domain = hierarchy.getClass(ancestor).domain
if (domain === baseDomain) {
result = ancestor
}
} catch {}
}
return result
}
let title: string = ''
$: if (object !== undefined) {
getTitle(object).then((t) => {
title = t
})
}
async function getTitle (object: Doc): Promise<string> {
const name = (object as any).name
if (name !== undefined) {
if (hierarchy.isDerived(object._class, contact.class.Person)) {
return formatName(name)
}
return name
}
const label = hierarchy.getClass(object._class).label
return await translate(label, {})
}
async function getHeaderEditor (_class: Ref<Class<Doc>>): Promise<AnyComponent | undefined> {
const clazz = hierarchy.getClass(_class)
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditorHeader)
if (editorMixin.editor != null) return editorMixin.editor
if (clazz.extends != null) return getHeaderEditor(clazz.extends)
}
let headerEditor: AnyComponent | undefined = undefined
let headerLoading = false
$: {
headerLoading = true
getHeaderEditor(realObjectClass).then((r) => {
headerEditor = r
headerLoading = false
})
}
const _update = (result: any): void => {
dispatch('update', result)
}
let panelWidth: number = 0
let innerWidth: number = 0
function showMenu (ev?: Event): void {
if (object !== undefined) {
showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement)
}
}
</script>
<ActionContext
context={{
mode: 'editor'
}}
/>
{#if object !== undefined && title !== undefined}
<Panel
{icon}
{title}
{object}
isHeader={false}
isAside={true}
bind:panelWidth
bind:innerWidth
on:update={(ev) => _update(ev.detail)}
on:close={() => {
dispatch('close')
}}
>
<svelte:fragment slot="navigator">
<UpDownNavigator element={object} />
</svelte:fragment>
<svelte:fragment slot="tools">
<div class="p-1">
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
</div>
</svelte:fragment>
<svelte:fragment slot="attributes" let:direction={dir}>
<div class="flex flex-reverse flex-grow">
<Button
kind={'transparent'}
shape={'round'}
selected={showAllMixins}
on:click={() => {
showAllMixins = !showAllMixins
}}
>
<svelte:fragment slot="content">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.66602" y="2.66663" width="10.6667" height="4.66667" rx="1" stroke="white" />
<path
d="M2.66602 11.3334C2.66602 10.3906 2.66602 9.91916 2.95891 9.62627C3.2518 9.33337 3.72321 9.33337 4.66602 9.33337H6.66602V11.3334C6.66602 12.2762 6.66602 12.7476 6.37312 13.0405C6.37312 13.0405 6.37312 13.0405 6.37312 13.0405C6.08023 13.3334 5.60882 13.3334 4.66602 13.3334V13.3334C3.72321 13.3334 3.2518 13.3334 2.95891 13.0405C2.95891 13.0405 2.95891 13.0405 2.95891 13.0405C2.66602 12.7476 2.66602 12.2762 2.66602 11.3334V11.3334Z"
stroke="white"
/>
<path
d="M9.33398 9.33337H11.334C12.2768 9.33337 12.7482 9.33337 13.0411 9.62627C13.334 9.91916 13.334 10.3906 13.334 11.3334V11.3334C13.334 12.2762 13.334 12.7476 13.0411 13.0405C12.7482 13.3334 12.2768 13.3334 11.334 13.3334V13.3334C10.3912 13.3334 9.91977 13.3334 9.62688 13.0405C9.33398 12.7476 9.33398 12.2762 9.33398 11.3334V9.33337Z"
stroke="white"
/>
</svg>
</svelte:fragment>
</Button>
</div>
{#if !headerLoading}
{#if headerEditor !== undefined}
<Component
is={headerEditor}
props={{ object, keys, mixins, ignoreKeys, vertical: dir === 'column', allowedCollections }}
on:update={() => updateKeys(showAllMixins)}
/>
{:else if dir === 'column'}
<DocAttributeBar
{object}
{mixins}
ignoreKeys={[...ignoreKeys, ...collectionArrays, ...inplaceAttributes]}
{allowedCollections}
on:update={() => updateKeys(showAllMixins)}
/>
{:else}
<AttributesBar {object} _class={realObjectClass} {keys} />
{/if}
{/if}
</svelte:fragment>
{#if mainEditor}
<Component
is={mainEditor}
props={{ object }}
on:open={(ev) => {
ignoreKeys = ev.detail.ignoreKeys
ignoreMixins = new Set(ev.detail.ignoreMixins)
allowedCollections = ev.detail.allowedCollections ?? []
collectionArrays = ev.detail.collectionArrays ?? []
getMixins(parentClass, object, showAllMixins)
updateKeys(showAllMixins)
}}
/>
{/if}
{#each fieldEditors as collection}
{#if collection.editor}
<div class="mt-6 clear-mins">
<Component
is={collection.editor}
props={{
objectId: object._id,
_class: collection.key.attr.attributeOf,
object,
space: object.space,
key: collection.key,
[collection.key.key]: getCollectionCounter(hierarchy, object, collection.key)
}}
/>
</div>
{/if}
{/each}
</Panel>
{/if}