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
 }