diff --git a/models/view/src/index.ts b/models/view/src/index.ts
index 70d8d8784e..fa6067528d 100644
--- a/models/view/src/index.ts
+++ b/models/view/src/index.ts
@@ -161,7 +161,8 @@ export class TObjectValidator extends TClass implements ObjectValidator {
 
 @Mixin(view.mixin.ObjectFactory, core.class.Class)
 export class TObjectFactory extends TClass implements ObjectFactory {
-  component!: AnyComponent
+  component?: AnyComponent
+  create?: Resource<() => Promise<void>>
 }
 
 @Mixin(view.mixin.ObjectTitle, core.class.Class)
diff --git a/plugins/contact-resources/src/components/CreateContact.svelte b/plugins/contact-resources/src/components/CreateContact.svelte
index 64779868e4..cf70e61abf 100644
--- a/plugins/contact-resources/src/components/CreateContact.svelte
+++ b/plugins/contact-resources/src/components/CreateContact.svelte
@@ -1,5 +1,5 @@
 <script lang="ts">
-  import { Asset } from '@hcengineering/platform'
+  import { Asset, getResource } from '@hcengineering/platform'
   import { getClient } from '@hcengineering/presentation'
   import { Menu, Action, showPopup, closePopup } from '@hcengineering/ui'
   import view from '@hcengineering/view'
@@ -13,18 +13,28 @@
   client
     .getHierarchy()
     .getDescendants(contact.class.Contact)
-    .forEach((v) => {
+    .forEach(async (v) => {
       const cl = hierarchy.getClass(v)
       if (hierarchy.hasMixin(cl, view.mixin.ObjectFactory)) {
-        const f = hierarchy.as(cl, view.mixin.ObjectFactory)
-        actions.push({
-          icon: cl.icon as Asset,
-          label: cl.label,
-          action: async () => {
+        const { component, create } = hierarchy.as(cl, view.mixin.ObjectFactory)
+        let action: (() => Promise<void>) | undefined
+
+        if (component) {
+          action = async () => {
             closePopup()
-            showPopup(f.component, {}, 'top')
+            showPopup(component, {}, 'top')
           }
-        })
+        } else if (create) {
+          action = await getResource(create)
+        }
+
+        if (action) {
+          actions.push({
+            icon: cl.icon as Asset,
+            label: cl.label,
+            action
+          })
+        }
       }
     })
 </script>
diff --git a/plugins/view-resources/src/components/list/SortableList.svelte b/plugins/view-resources/src/components/list/SortableList.svelte
new file mode 100644
index 0000000000..913309e1ce
--- /dev/null
+++ b/plugins/view-resources/src/components/list/SortableList.svelte
@@ -0,0 +1,219 @@
+<!--
+// 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 { Class, Doc, DocumentQuery, FindOptions, FindResult, Ref } from '@hcengineering/core'
+  import { getResource, IntlString } from '@hcengineering/platform'
+  import presentation, { createQuery, getClient } from '@hcengineering/presentation'
+  import { calcRank, DocWithRank } from '@hcengineering/task'
+  import { Button, Component, IconAdd, Label, Loading } from '@hcengineering/ui'
+  import view, { AttributeModel, ObjectFactory } from '@hcengineering/view'
+  import { flip } from 'svelte/animate'
+  import { getObjectPresenter } from '../../utils'
+  import SortableListItem from './SortableListItem.svelte'
+
+  /*
+  How to use:
+  
+  We must add presenter for the "_class" via "AttributePresenter" mixin
+  to be able display the rows list.
+
+  To create a new items, we should add "ObjectFactory" mixin also.
+
+  We can create a custom list items or editor based on "SortableListItem"
+  and "SortableListItemPresenter"
+
+  Important: the "ObjectFactory" component must emit the "close" event
+*/
+
+  export let _class: Ref<Class<Doc>>
+  export let label: IntlString | undefined = undefined
+  export let query: DocumentQuery<Doc> = {}
+  export let queryOptions: FindOptions<Doc> | undefined = undefined
+  export let presenterProps: Record<string, any> = {}
+  export let flipDuration = 200
+
+  const client = getClient()
+  const hierarchy = client.getHierarchy()
+  const itemsQuery = createQuery()
+
+  let isModelLoading = false
+  let areItemsloading = true
+  let areItemsSorting = false
+
+  let model: AttributeModel | undefined
+  let objectFactory: ObjectFactory | undefined
+  let items: FindResult<Doc> | undefined
+
+  let draggingIndex: number | null = null
+  let hoveringIndex: number | null = null
+
+  let isCreating = false
+
+  async function updateModel (modelClassRef: Ref<Class<Doc>>) {
+    try {
+      isModelLoading = true
+      model = await getObjectPresenter(client, modelClassRef, { key: '', props: presenterProps })
+    } finally {
+      isModelLoading = false
+    }
+  }
+
+  function updateObjectFactory (objectFactoryClassRef: Ref<Class<Doc>>) {
+    const objectFactoryClass = hierarchy.getClass(objectFactoryClassRef)
+
+    if (hierarchy.hasMixin(objectFactoryClass, view.mixin.ObjectFactory)) {
+      objectFactory = hierarchy.as(objectFactoryClass, view.mixin.ObjectFactory)
+    }
+  }
+
+  function updateItems (newItems: FindResult<Doc>): void {
+    items = newItems
+    areItemsloading = false
+  }
+
+  function handleDragStart (ev: DragEvent, itemIndex: number) {
+    if (ev.dataTransfer) {
+      ev.dataTransfer.effectAllowed = 'move'
+      ev.dataTransfer.dropEffect = 'move'
+    }
+
+    draggingIndex = itemIndex
+  }
+
+  function handleDragOver (ev: DragEvent, itemIndex: number) {
+    ev.preventDefault()
+
+    hoveringIndex = itemIndex
+  }
+
+  async function handleDrop (itemIndex: number) {
+    if (isSortable && items && draggingIndex !== null && draggingIndex !== itemIndex) {
+      const item = items[draggingIndex] as DocWithRank
+      const [prev, next] = [
+        items[draggingIndex < itemIndex ? itemIndex : itemIndex - 1] as DocWithRank,
+        items[draggingIndex < itemIndex ? itemIndex + 1 : itemIndex] as DocWithRank
+      ]
+
+      try {
+        areItemsSorting = true
+        await client.update(item, { rank: calcRank(prev, next) })
+      } finally {
+        areItemsSorting = false
+      }
+    }
+
+    resetDrag()
+  }
+
+  async function create () {
+    if (objectFactory?.create) {
+      const createFn = await getResource(objectFactory.create)
+      await createFn()
+      return
+    }
+
+    isCreating = true
+  }
+
+  function resetDrag () {
+    draggingIndex = null
+    hoveringIndex = null
+  }
+
+  $: updateModel(_class)
+  $: updateObjectFactory(_class)
+  $: itemsQuery.query(_class, query, updateItems, { ...queryOptions, limit: Math.max(queryOptions?.limit ?? 0, 200) })
+
+  $: isLoading = isModelLoading || areItemsloading
+  $: isSortable = hierarchy.getAllAttributes(_class).has('rank')
+</script>
+
+<div class="flex-col">
+  {#if label}
+    <div class="flex mb-4">
+      {#if label}
+        <div class="title-wrapper">
+          <span class="wrapped-title text-base content-accent-color">
+            <Label {label} />
+          </span>
+        </div>
+      {/if}
+      {#if objectFactory}
+        <div class="ml-auto">
+          <Button
+            showTooltip={{ label: presentation.string.Add }}
+            disabled={isLoading}
+            width="min-content"
+            icon={IconAdd}
+            size="small"
+            kind="transparent"
+            on:click={create}
+          />
+        </div>
+      {/if}
+    </div>
+  {/if}
+
+  {#if isLoading}
+    <Loading />
+  {:else if model && items}
+    <div class="flex-col flex-gap-1">
+      {#each items as item, index (item._id)}
+        {@const isDraggable = isSortable && items.length > 1 && !areItemsSorting}
+        <div
+          class="row"
+          class:is-dragged-over-up={draggingIndex !== null && index === hoveringIndex && index < draggingIndex}
+          class:is-dragged-over-down={draggingIndex !== null && index === hoveringIndex && index > draggingIndex}
+          draggable={isDraggable}
+          animate:flip={{ duration: flipDuration }}
+          on:dragstart={(ev) => handleDragStart(ev, index)}
+          on:dragover={(ev) => handleDragOver(ev, index)}
+          on:drop={() => handleDrop(index)}
+          on:dragend={resetDrag}
+        >
+          <SortableListItem {isDraggable}>
+            <svelte:component this={model.presenter} {...model.props ?? {}} value={item} />
+          </SortableListItem>
+        </div>
+      {/each}
+
+      {#if objectFactory?.component && isCreating}
+        <!-- Important: the "close" event must be specified -->
+        <Component is={objectFactory.component} showLoading on:close={() => (isCreating = false)} />
+      {/if}
+    </div>
+  {/if}
+</div>
+
+<style lang="scss">
+  .row {
+    position: relative;
+    overflow: hidden;
+
+    &.is-dragged-over-up::before {
+      position: absolute;
+      content: '';
+      inset: 0;
+      border-top: 1px solid var(--theme-bg-check);
+    }
+
+    &.is-dragged-over-down::before {
+      position: absolute;
+      content: '';
+      inset: 0;
+      border-bottom: 1px solid var(--theme-bg-check);
+    }
+  }
+</style>
diff --git a/plugins/view-resources/src/components/list/SortableListItem.svelte b/plugins/view-resources/src/components/list/SortableListItem.svelte
new file mode 100644
index 0000000000..5cc4c4d1c1
--- /dev/null
+++ b/plugins/view-resources/src/components/list/SortableListItem.svelte
@@ -0,0 +1,52 @@
+<!--
+// 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 Circles from '../icons/Circles.svelte'
+
+  export let isDraggable = false
+</script>
+
+<div class="root flex background-button-bg-color border-radius-1">
+  <div class="flex-center ml-2">
+    <div class="flex-no-shrink circles-mark" class:isDraggable><Circles /></div>
+  </div>
+
+  <slot />
+</div>
+
+<style lang="scss">
+  .root {
+    &:hover {
+      .circles-mark.isDraggable {
+        cursor: grab;
+        opacity: 0.4;
+      }
+    }
+  }
+
+  .circles-mark {
+    position: relative;
+    opacity: 0;
+    width: 0.375rem;
+    height: 1rem;
+    transition: opacity 0.1s;
+
+    &.isDraggable::before {
+      position: absolute;
+      content: '';
+      inset: -0.5rem;
+    }
+  }
+</style>
diff --git a/plugins/view-resources/src/components/list/SortableListItemPresenter.svelte b/plugins/view-resources/src/components/list/SortableListItemPresenter.svelte
new file mode 100644
index 0000000000..3f9e7c9de4
--- /dev/null
+++ b/plugins/view-resources/src/components/list/SortableListItemPresenter.svelte
@@ -0,0 +1,106 @@
+<!--
+// 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 presentation from '@hcengineering/presentation'
+  import { Icon, IconEdit, IconClose, tooltip, Button } from '@hcengineering/ui'
+  import { createEventDispatcher } from 'svelte'
+
+  export let isEditable = false
+  export let isDeletable = false
+  export let isEditing = false
+  export let isSaving = false
+  export let canSave = false
+
+  const dispatch = createEventDispatcher()
+
+  $: areButtonsVisible = isEditable || isDeletable || isEditing
+</script>
+
+<div
+  class="root flex flex-between items-center w-full p-2"
+  on:dblclick|preventDefault={isEditable && !isEditing ? () => dispatch('edit') : undefined}
+>
+  <div class="content w-full">
+    <slot />
+  </div>
+
+  {#if areButtonsVisible}
+    <div class="ml-auto pl-2 buttons-group small-gap flex-no-shrink">
+      {#if isEditing}
+        <Button label={presentation.string.Cancel} kind="secondary" on:click={() => dispatch('cancel')} />
+        <Button
+          label={presentation.string.Save}
+          kind="primary"
+          loading={isSaving}
+          disabled={!canSave}
+          on:click={() => dispatch('save')}
+        />
+      {:else}
+        {#if isEditable}
+          <button
+            class="btn"
+            use:tooltip={{ label: presentation.string.Edit }}
+            on:click|preventDefault={() => dispatch('edit')}
+          >
+            <Icon icon={IconEdit} size="small" />
+          </button>
+        {/if}
+        {#if isDeletable}
+          <button
+            class="btn"
+            use:tooltip={{ label: presentation.string.Remove }}
+            on:click|preventDefault={() => dispatch('delete')}
+          >
+            <Icon icon={IconClose} size="small" />
+          </button>
+        {/if}
+      {/if}
+    </div>
+  {/if}
+</div>
+
+<style lang="scss">
+  .root {
+    overflow: hidden;
+
+    &:hover {
+      .btn {
+        opacity: 1;
+      }
+    }
+  }
+
+  .content {
+    overflow: hidden;
+  }
+
+  .btn {
+    position: relative;
+    opacity: 0;
+    cursor: pointer;
+    color: var(--content-color);
+    transition: opacity 0.15s;
+
+    &:hover {
+      color: var(--caption-color);
+    }
+
+    &::before {
+      position: absolute;
+      content: '';
+      inset: -0.5rem;
+    }
+  }
+</style>
diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts
index 74b01d7ba4..8693f2562a 100644
--- a/plugins/view-resources/src/index.ts
+++ b/plugins/view-resources/src/index.ts
@@ -54,6 +54,9 @@ 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 SortableList from './components/list/SortableList.svelte'
+import SortableListItem from './components/list/SortableListItem.svelte'
+import SortableListItemPresenter from './components/list/SortableListItemPresenter.svelte'
 import {
   afterResult,
   beforeResult,
@@ -110,7 +113,10 @@ export {
   BooleanPresenter,
   NumberEditor,
   NumberPresenter,
-  TimestampPresenter
+  TimestampPresenter,
+  SortableList,
+  SortableListItem,
+  SortableListItemPresenter
 }
 
 export default async (): Promise<Resources> => ({
diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts
index a9fb44dca3..3823690e6e 100644
--- a/plugins/view/src/index.ts
+++ b/plugins/view/src/index.ts
@@ -376,7 +376,8 @@ export interface BuildModelOptions {
  *
  */
 export interface ObjectFactory extends Class<Obj> {
-  component: AnyComponent
+  component?: AnyComponent
+  create?: Resource<() => Promise<void>>
 }
 
 /**