diff --git a/packages/ui/src/components/Component.svelte b/packages/ui/src/components/Component.svelte index 8722325c05..f9340fd459 100644 --- a/packages/ui/src/components/Component.svelte +++ b/packages/ui/src/components/Component.svelte @@ -20,6 +20,7 @@ // import Icon from './Icon.svelte' import Loading from './Loading.svelte' import ErrorBoundary from './internal/ErrorBoundary' +import ErrorPresenter from './ErrorPresenter.svelte'; export let is: AnyComponent export let props = {} @@ -36,8 +37,8 @@ </Ctor> </ErrorBoundary> {:catch err} - ERROR: {console.log(err, JSON.stringify(component))} - {props} - {err} + <pre style='max-height: 140px; overflow: auto;'> + <ErrorPresenter error={err}/> + </pre> <!-- <Icon icon={ui.icon.Error} size="32" /> --> {/await} diff --git a/packages/ui/src/components/ErrorPopup.svelte b/packages/ui/src/components/ErrorPopup.svelte new file mode 100644 index 0000000000..1d8b0de2e3 --- /dev/null +++ b/packages/ui/src/components/ErrorPopup.svelte @@ -0,0 +1,28 @@ +<!-- +// 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"> + +export let error: any + +</script> + +{error.message} +<pre> + {#if error.status.params} + {JSON.stringify(error.status.params, undefined, 2)} + {/if} +</pre> diff --git a/packages/ui/src/components/ErrorPresenter.svelte b/packages/ui/src/components/ErrorPresenter.svelte new file mode 100644 index 0000000000..ad82a257bf --- /dev/null +++ b/packages/ui/src/components/ErrorPresenter.svelte @@ -0,0 +1,32 @@ +<!-- +// 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 Tooltip from './Tooltip.svelte' + import ErrorPopup from './ErrorPopup.svelte' + export let error: any +</script> + +<Tooltip component={ErrorPopup} props={{ error: error }}> + <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle cx="10" cy="10" r="7.275" stroke="#EE7A7A" stroke-width="1.2" /> + <path + d="M9.51371 11.6902L9.51636 11.7875H9.61367H10.4137H10.511L10.5136 11.6902L10.6886 5.27772L10.6914 5.17499H10.5887H9.43867H9.33591L9.33871 5.27772L9.51371 11.6902ZM10.0012 14.375C10.4929 14.375 10.9012 13.9812 10.9012 13.475C10.9012 12.9687 10.4929 12.575 10.0012 12.575C9.50947 12.575 9.10117 12.9687 9.10117 13.475C9.10117 13.9812 9.50947 14.375 10.0012 14.375Z" + fill="#EE7A7A" + stroke="#EE7A7A" + stroke-width="0.2" + /> + </svg> +</Tooltip> diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 7de0a87889..aa1eb2995a 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -15,9 +15,9 @@ import { SvelteComponent } from 'svelte' import type { AnySvelteComponent, AnyComponent, PopupAlignment, LabelAndProps, TooltipAligment } from './types' -import { getResource, IntlString } from '@anticrm/platform' -import { addStringsLoader } from '@anticrm/platform' -import { uiId } from './plugin' +import { getResource, IntlString, addStringsLoader } from '@anticrm/platform' +import { uiId } from './plugin' +import { writable, readable } from 'svelte/store' import Root from './components/internal/Root.svelte' @@ -79,11 +79,10 @@ export { default as IconDelete } from './components/icons/Delete.svelte' export { default as IconEdit } from './components/icons/Edit.svelte' export { default as IconInfo } from './components/icons/Info.svelte' export { default as Menu } from './components/Menu.svelte' +export { default as ErrorPresenter } from './components/ErrorPresenter.svelte' export * from './utils' -import { writable, readable } from 'svelte/store' - export function createApp (target: HTMLElement): SvelteComponent { return new Root({ target }) } diff --git a/plugins/view-resources/src/components/Table.svelte b/plugins/view-resources/src/components/Table.svelte index a01a7f8ba0..aa7fe425cb 100644 --- a/plugins/view-resources/src/components/Table.svelte +++ b/plugins/view-resources/src/components/Table.svelte @@ -15,16 +15,14 @@ --> <script lang="ts"> - import type { Ref, Class, Doc, Space, FindOptions, DocumentQuery } from '@anticrm/core' + import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@anticrm/core' import { SortingOrder } from '@anticrm/core' + import { createQuery, getClient } from '@anticrm/presentation' + import { IconDown, IconUp, Label, Loading, showPopup } from '@anticrm/ui' import { buildModel } from '../utils' - import { getClient } from '@anticrm/presentation' - import { Label, showPopup, Loading, CheckBox, IconDown, IconUp } from '@anticrm/ui' import MoreV from './icons/MoreV.svelte' import Menu from './Menu.svelte' - import { createQuery } from '@anticrm/presentation' - export let _class: Ref<Class<Doc>> export let query: DocumentQuery<Doc> export let options: FindOptions<Doc> | undefined @@ -39,13 +37,14 @@ const q = createQuery() $: q.query(_class, query, result => { objects = result }, { sort: { [sortKey]: sortOrder }, ...options }) - function getValue(doc: Doc, key: string): any { - if (key.length === 0) + function getValue (doc: Doc, key: string): any { + if (key.length === 0) { return doc + } const path = key.split('.') const len = path.length let obj = doc as any - for (let i=0; i<len; i++){ + for (let i = 0; i < len; i++) { obj = obj?.[path[i]] } return obj ?? '' @@ -55,12 +54,13 @@ const showMenu = (ev: MouseEvent, object: Doc, row: number): void => { selectRow = row - showPopup(Menu, { object }, ev.target as HTMLElement, (() => { selectRow = undefined })) + showPopup(Menu, { object }, ev.target as HTMLElement, () => { selectRow = undefined }) } - function changeSorting(key: string) { - if (key === '') + function changeSorting (key: string): void { + if (key === '') { return + } if (key !== sortKey) { sortKey = key sortOrder = SortingOrder.Ascending @@ -69,8 +69,7 @@ } } </script> - -{#await buildModel({client, _class, keys: config, options})} +{#await buildModel({ client, _class, keys: config, options })} <Loading/> {:then model} <table class="table-body"> @@ -100,12 +99,12 @@ <tr class="tr-body" class:fixed={row === selectRow}> {#each model as attribute, cell} {#if !cell} - <td><div class="firstCell"> - <svelte:component this={attribute.presenter} value={getValue(object, attribute.key)}/> + <td><div class="firstCell"> + <svelte:component this={attribute.presenter} value={getValue(object, attribute.key)} {...attribute.props}/> <div class="menuRow" on:click={(ev) => showMenu(ev, object, row)}><MoreV size={'small'} /></div> </div></td> {:else} - <td><svelte:component this={attribute.presenter} value={getValue(object, attribute.key)}/></td> + <td><svelte:component this={attribute.presenter} value={getValue(object, attribute.key)} {...attribute.props}/></td> {/if} {/each} </tr> diff --git a/plugins/view-resources/src/components/TableView.svelte b/plugins/view-resources/src/components/TableView.svelte index 98060fa685..0f1eedf0c0 100644 --- a/plugins/view-resources/src/components/TableView.svelte +++ b/plugins/view-resources/src/components/TableView.svelte @@ -13,19 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. --> - <script lang="ts"> - import { createEventDispatcher } from 'svelte' - import type { Ref, Class, Doc, Space, FindOptions } from '@anticrm/core' + import type { Class, Doc, FindOptions, Ref, Space } from '@anticrm/core' import { SortingOrder } from '@anticrm/core' + import { createQuery, getClient } from '@anticrm/presentation' + import { CheckBox, IconDown, IconUp, Label, Loading, ScrollBox, showPopup } from '@anticrm/ui' import { buildModel } from '../utils' - import { getClient } from '@anticrm/presentation' - import { Label, showPopup, Loading, ScrollBox, CheckBox, IconDown, IconUp } from '@anticrm/ui' import MoreV from './icons/MoreV.svelte' import Menu from './Menu.svelte' - import { createQuery } from '@anticrm/presentation' - export let _class: Ref<Class<Doc>> export let space: Ref<Space> export let options: FindOptions<Doc> | undefined @@ -39,15 +35,23 @@ let objects: Doc[] const query = createQuery() - $: query.query(_class, search === '' ? { space } : { $search: search }, result => { objects = result }, { sort: { [sortKey]: sortOrder }, ...options }) + $: query.query( + _class, + search === '' ? { space } : { $search: search }, + (result) => { + objects = result + }, + { sort: { [sortKey]: sortOrder }, ...options } + ) - function getValue(doc: Doc, key: string): any { - if (key.length === 0) + function getValue (doc: Doc, key: string): any { + if (key.length === 0) { return doc + } const path = key.split('.') const len = path.length let obj = doc as any - for (let i=0; i<len; i++){ + for (let i = 0; i < len; i++) { obj = obj?.[path[i]] } return obj ?? '' @@ -58,76 +62,96 @@ const showMenu = (ev: MouseEvent, object: Doc, row: number): void => { selectRow = row - showPopup(Menu, { object }, ev.target as HTMLElement, (() => { selectRow = undefined })) + showPopup(Menu, { object }, ev.target as HTMLElement, () => { + selectRow = undefined + }) } - function changeSorting(key: string) { - if (key === '') + function changeSorting (key: string): void { + if (key === '') { return + } if (key !== sortKey) { sortKey = key sortOrder = SortingOrder.Ascending } else { - sortOrder = (sortOrder === SortingOrder.Ascending) ? SortingOrder.Descending : SortingOrder.Ascending + sortOrder = sortOrder === SortingOrder.Ascending ? SortingOrder.Descending : SortingOrder.Ascending } } </script> -{#await buildModel({client, _class, keys: config, options})} - <Loading/> +{#await buildModel({ client, _class, keys: config, options })} + <Loading /> {:then model} -<div class="container"> - <ScrollBox vertical stretch noShift> - <table class="table-body"> - <thead> - <tr class="tr-head"> - {#each model as attribute, cellHead} - {#if !cellHead} - <th> - <div class="checkCell" class:checkall={checking}> - <CheckBox symbol={'minus'} /> + <div class="container"> + <ScrollBox vertical stretch noShift> + <table class="table-body"> + <thead> + <tr class="tr-head"> + {#each model as attribute, cellHead} + {#if !cellHead} + <th> + <div class="checkCell" class:checkall={checking}> + <CheckBox symbol={'minus'} /> + </div> + </th> + {/if} + + <th + class:sortable={attribute.key} + class:sorted={attribute.key === sortKey} + on:click={() => changeSorting(attribute.key)} + > + <div class="flex-row-center"> + <Label label={attribute.label} /> + {#if attribute.key === sortKey} + <div class="icon"> + {#if sortOrder === SortingOrder.Ascending} + <IconUp size={'small'} /> + {:else} + <IconDown size={'small'} /> + {/if} + </div> + {/if} </div> </th> - {/if} - <th class:sortable={attribute.key} class:sorted={attribute.key === sortKey} on:click={() => changeSorting(attribute.key)}> - <div class="flex-row-center"> - <Label label = {attribute.label}/> - {#if attribute.key === sortKey} - <div class="icon"> - {#if sortOrder === SortingOrder.Ascending} - <IconUp size={'small'} /> - {:else} - <IconDown size={'small'} /> - {/if} - </div> - {/if} - </div> - </th> - {/each} - </tr> - </thead> - {#if objects} - <tbody> - {#each objects as object, row (object._id)} - <tr class="tr-body" class:checking class:fixed={row === selectRow}> - {#each model as attribute, cell} - {#if !cell} - <td><div class="checkCell"><CheckBox bind:checked={checking} /></div></td> - <td><div class="firstCell"> - <svelte:component this={attribute.presenter} value={getValue(object, attribute.key)}/> - <div class="menuRow" on:click={(ev) => showMenu(ev, object, row)}><MoreV size={'small'} /></div> - </div></td> - {:else} - <td><svelte:component this={attribute.presenter} value={getValue(object, attribute.key)}/></td> - {/if} - {/each} - </tr> - {/each} - </tbody> - {/if} - </table> - </ScrollBox> -</div> + {/each} + </tr> + </thead> + {#if objects} + <tbody> + {#each objects as object, row (object._id)} + <tr class="tr-body" class:checking class:fixed={row === selectRow}> + {#each model as attribute, cell} + {#if !cell} + <td><div class="checkCell"><CheckBox bind:checked={checking} /></div></td> + <td + ><div class="firstCell"> + <svelte:component + this={attribute.presenter} + value={getValue(object, attribute.key)} + {...attribute.props} + /> + <div class="menuRow" on:click={(ev) => showMenu(ev, object, row)}><MoreV size={'small'} /></div> + </div></td + > + {:else} + <td + ><svelte:component + this={attribute.presenter} + value={getValue(object, attribute.key)} + {...attribute.props} + /></td + > + {/if} + {/each} + </tr> + {/each} + </tbody> + {/if} + </table> + </ScrollBox> + </div> {/await} <style lang="scss"> @@ -138,7 +162,9 @@ height: 100%; } - .table-body { width: 100%; } + .table-body { + width: 100%; + } .firstCell { display: flex; @@ -146,10 +172,12 @@ align-items: center; .menuRow { visibility: hidden; - margin-left: .5rem; - opacity: .6; + margin-left: 0.5rem; + opacity: 0.6; cursor: pointer; - &:hover { opacity: 1; } + &:hover { + opacity: 1; + } } } .checkCell { @@ -158,11 +186,12 @@ align-items: center; } - th, td { - padding: .5rem 1.5rem; + th, + td { + padding: 0.5rem 1.5rem; text-align: left; &:first-child { - padding: 0 .75rem; + padding: 0 0.75rem; width: 2.5rem; } &:nth-child(2) { @@ -176,36 +205,51 @@ top: 0; height: 2.5rem; font-weight: 500; - font-size: .75rem; + font-size: 0.75rem; color: var(--theme-content-dark-color); background-color: var(--theme-bg-color); box-shadow: inset 0 -1px 0 0 var(--theme-bg-focused-color); user-select: none; z-index: 5; - &.sortable { cursor: pointer; } - &.sorted .icon { - margin-left: .25rem; - opacity: .6; + &.sortable { + cursor: pointer; + } + &.sorted .icon { + margin-left: 0.25rem; + opacity: 0.6; + } + .checkall { + visibility: visible; } - .checkall { visibility: visible; } } .tr-body { height: 3.25rem; color: var(--theme-caption-color); border-bottom: 1px solid var(--theme-button-border-hovered); - &:hover, &.checking { + &:hover, + &.checking { background-color: var(--theme-table-bg-hover); - .checkCell { visibility: visible; } + .checkCell { + visibility: visible; + } + } + &:hover .firstCell .menuRow { + visibility: visible; + } + &:last-child { + border-bottom: none; } - &:hover .firstCell .menuRow { visibility: visible; } - &:last-child { border-bottom: none; } } .fixed { background-color: var(--theme-table-bg-hover); - .checkCell { visibility: visible; } - .menuRow { visibility: visible; } + .checkCell { + visibility: visible; + } + .menuRow { + visibility: visible; + } } </style> diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts index 6feb4abd44..aa2e7a4330 100644 --- a/plugins/view-resources/src/utils.ts +++ b/plugins/view-resources/src/utils.ts @@ -14,37 +14,37 @@ // limitations under the License. // -import core, { Class, Client, Doc, FindOptions, FindResult, Obj, Ref, AttachedDoc, TxOperations, Collection } from '@anticrm/core' +import core, { AttachedDoc, Class, Client, Collection, Doc, FindOptions, FindResult, Obj, Ref, TxOperations } from '@anticrm/core' import type { IntlString } from '@anticrm/platform' import { getResource } from '@anticrm/platform' import { getAttributePresenterClass } from '@anticrm/presentation' -import type { AnyComponent } from '@anticrm/ui' import type { Action, ActionTarget, BuildModelOptions } from '@anticrm/view' -import view, { AttributeModel } from '@anticrm/view' +import view, { AttributeModel, BuildModelKey } from '@anticrm/view' +import { ErrorPresenter } from '@anticrm/ui' /** * @public */ -export async function getObjectPresenter (client: Client, _class: Ref<Class<Obj>>, preserveKey: string): Promise<AttributeModel> { +export async function getObjectPresenter (client: Client, _class: Ref<Class<Obj>>, preserveKey: BuildModelKey): Promise<AttributeModel> { const clazz = client.getHierarchy().getClass(_class) const presenterMixin = client.getHierarchy().as(clazz, view.mixin.AttributePresenter) if (presenterMixin.presenter === undefined) { if (clazz.extends !== undefined) { return await getObjectPresenter(client, clazz.extends, preserveKey) } else { - throw new Error('object presenter not found for ' + preserveKey) + throw new Error('object presenter not found for ' + JSON.stringify(preserveKey)) } } const presenter = await getResource(presenterMixin.presenter) return { - key: preserveKey, + key: typeof preserveKey === 'string' ? preserveKey : '', _class, label: clazz.label, presenter } } -async function getAttributePresenter (client: Client, _class: Ref<Class<Obj>>, key: string, preserveKey: string): Promise<AttributeModel> { +async function getAttributePresenter (client: Client, _class: Ref<Class<Obj>>, key: string, preserveKey: BuildModelKey): Promise<AttributeModel> { const attribute = client.getHierarchy().getAttribute(_class, key) let attrClass = getAttributePresenterClass(attribute) const clazz = client.getHierarchy().getClass(attrClass) @@ -57,25 +57,25 @@ async function getAttributePresenter (client: Client, _class: Ref<Class<Obj>>, k parent = pclazz.extends } if (presenterMixin.presenter === undefined) { - throw new Error('attribute presenter not found for ' + preserveKey) + throw new Error('attribute presenter not found for ' + JSON.stringify(preserveKey)) } const presenter = await getResource(presenterMixin.presenter) return { - key: preserveKey, + key: key, _class: attrClass, label: attribute.label, presenter } } -async function getPresenter (client: Client, _class: Ref<Class<Obj>>, key: string, preserveKey: string, options?: FindOptions<Doc>): Promise<AttributeModel> { +async function getPresenter (client: Client, _class: Ref<Class<Obj>>, key: BuildModelKey, preserveKey: BuildModelKey, options?: FindOptions<Doc>): Promise<AttributeModel> { if (typeof key === 'object') { const { presenter, label } = key return { key: '', _class, label: label as IntlString, - presenter: await getResource(presenter as AnyComponent) + presenter: await getResource(presenter) } } if (key.length === 0) { @@ -103,16 +103,25 @@ async function getPresenter (client: Client, _class: Ref<Class<Obj>>, key: strin } export async function buildModel (options: BuildModelOptions): Promise<AttributeModel[]> { - console.log('building table model for', options._class) + console.log('building table model for', options) // eslint-disable-next-line array-callback-return - const model = options.keys.map(key => { + const model = options.keys.map(async key => { try { - const result = getPresenter(options.client, options._class, key, key, options.options) - return result + return await getPresenter(options.client, options._class, key, key, options.options) } catch (err: any) { - if (!(options.ignoreMissing ?? false)) { - throw err + if ((options.ignoreMissing ?? false)) { + return undefined } + const stringKey = (typeof key === 'string') ? key : key.label + console.error('Failed to find presenter for', key, err) + const errorPresenter: AttributeModel = { + key: '', + presenter: ErrorPresenter, + label: stringKey as IntlString, + _class: core.class.TypeString, + props: { error: err } + } + return errorPresenter } }) console.log(model) @@ -134,7 +143,7 @@ export async function getActions (client: Client, _class: Ref<Class<Obj>>): Prom return await client.findAll(view.class.Action, { _id: { $in: filterActions(client, _class, targets) } }) } -export async function deleteObject (client: Client & TxOperations, object: Doc) { +export async function deleteObject (client: Client & TxOperations, object: Doc): Promise<void> { const hierarchy = client.getHierarchy() const attributes = hierarchy.getAllAttributes(object._class) for (const [name, attribute] of attributes) { @@ -142,7 +151,7 @@ export async function deleteObject (client: Client & TxOperations, object: Doc) const collection = attribute.type as Collection<AttachedDoc> const allAttached = await client.findAll(collection.of, { attachedTo: object._id }) for (const attached of allAttached) { - deleteObject(client, attached) + deleteObject(client, attached).catch(err => console.log('failed to delete', name, err)) } } } diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 82c7b6f677..be9680548a 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -103,6 +103,14 @@ export interface Sequence extends Doc { */ export const viewId = 'view' as Plugin +/** + * @public + */ +export type BuildModelKey = string | { + presenter: AnyComponent + label: string +} + /** * @public */ @@ -111,6 +119,8 @@ export interface AttributeModel { label: IntlString _class: Ref<Class<Doc>> presenter: AnySvelteComponent + // Extra properties for component + props?: Record<string, any> } /** @@ -119,7 +129,7 @@ export interface AttributeModel { export interface BuildModelOptions { client: Client _class: Ref<Class<Obj>> - keys: string[] + keys: BuildModelKey[] options?: FindOptions<Doc> ignoreMissing?: boolean }