Custom table (#1734)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-05-13 12:38:54 +06:00 committed by GitHub
parent 5e7c320fb5
commit e91c5d073a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 716 additions and 339 deletions

View File

@ -12330,7 +12330,7 @@ packages:
dev: false
file:projects/model-view.tgz_typescript@4.6.4:
resolution: {integrity: sha512-eOmBJcwkfKRTRYPUIafUMhYYHXQXdaJhsqtEyw5121DxfzuEASwRpEYjb+goTQt32HL5bS8mv/aWInucJ4T7Xg==, tarball: file:projects/model-view.tgz}
resolution: {integrity: sha512-nc18WwMGJkHbrQf1H/3DYbKFLMpnM/GqvIpt8ckVhTvQyW9DAsOYHQc3GlqyU/6Hf1lMdR5UyReqy4JImLQmhQ==, tarball: file:projects/model-view.tgz}
id: file:projects/model-view.tgz
name: '@rush-temp/model-view'
version: 0.0.0
@ -14294,7 +14294,7 @@ packages:
dev: false
file:projects/view-resources.tgz_3b42a51b6c974062237d417c554d9dd7:
resolution: {integrity: sha512-3wR1mN5HyfqA1Kl5zeGA3dZf6UAu35FXWL/RYZN9/wvKAehyLH01K3nEUwI3cDXhghyPirrZ4MSEfs1x5/PBSw==, tarball: file:projects/view-resources.tgz}
resolution: {integrity: sha512-+5Q3eSxOc4MwOBrOolxvoT9s6AdaP3kj8foIwjOPpszUz3TWzavM8efyWyr850tdj8MGhhdXeo3zf6XACxe2Ww==, tarball: file:projects/view-resources.tgz}
id: file:projects/view-resources.tgz
name: '@rush-temp/view-resources'
version: 0.0.0
@ -14307,6 +14307,7 @@ packages:
eslint-plugin-node: 11.1.0_eslint@7.32.0
eslint-plugin-promise: 5.2.0_eslint@7.32.0
eslint-plugin-svelte3: 3.4.1_eslint@7.32.0+svelte@3.48.0
fast-equals: 2.0.4
prettier: 2.6.2
prettier-plugin-svelte: 2.7.0_prettier@2.6.2+svelte@3.48.0
sass: 1.51.0
@ -14329,7 +14330,7 @@ packages:
dev: false
file:projects/view.tgz:
resolution: {integrity: sha512-Jklo2DsV/p/JMq+2JgfoWsxbnRN80cejYdIKuNtw+FT25g7IPwlLMl45iojXyB4PND4xvmkw4Krv1dOaFA1DMw==, tarball: file:projects/view.tgz}
resolution: {integrity: sha512-gOlokrk6mKu6jlbghqfjkgcXqezcPbW5NcnzBhbK2FOUchPiuSLMbpa3e+3sCnYDuoZHjewbLVonizUqbamgJg==, tarball: file:projects/view.tgz}
name: '@rush-temp/view'
version: 0.0.0
dependencies:

View File

@ -16,17 +16,7 @@
// To help typescript locate view plugin properly
import type { Board, Card, CardAction, CardDate, CardLabel, MenuPage, LabelsCompactMode } from '@anticrm/board'
import type { Employee } from '@anticrm/contact'
import {
TxOperations as Client,
Doc,
DOMAIN_MODEL,
FindOptions,
IndexKind,
Ref,
Type,
Timestamp,
Markup
} from '@anticrm/core'
import { DOMAIN_MODEL, IndexKind, Markup, Ref, Timestamp, TxOperations as Client, Type } from '@anticrm/core'
import {
ArrOf,
Builder,
@ -221,10 +211,6 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: board.class.Card,
descriptor: board.viewlet.Kanban,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {}
} as FindOptions<Doc>, // TODO: fix
config: []
})

View File

@ -200,7 +200,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: chunter.class.Message,
descriptor: chunter.viewlet.Chat,
config: {}
config: []
})
builder.createDoc(

View File

@ -160,21 +160,18 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: contact.class.Contact,
descriptor: view.viewlet.Table,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: { _id: { channels: contact.class.Channel }, _class: core.class.Class }
},
config: [
'',
{ key: '$lookup._class.label', label: contact.string.TypeLabel },
'city',
{
key: '',
presenter: attachment.component.AttachmentsPresenter,
label: attachment.string.Files,
sortingKey: 'attachments'
},
'modifiedOn',
{ presenter: view.component.RolePresenter, label: view.string.Role },
{ key: '', presenter: view.component.RolePresenter, label: view.string.Role },
'$lookup.channels'
]
})

View File

@ -36,7 +36,7 @@ import {
Type,
Version
} from '@anticrm/core'
import { Index, Model, Prop, TypeIntlString, TypeRef, TypeString, TypeTimestamp, UX } from '@anticrm/model'
import { Hidden, Index, Model, Prop, TypeIntlString, TypeRef, TypeString, TypeTimestamp, UX } from '@anticrm/model'
import type { IntlString } from '@anticrm/platform'
import core from './component'
@ -75,7 +75,8 @@ export class TAttachedDoc extends TDoc implements AttachedDoc {
@Index(IndexKind.Indexed)
attachedToClass!: Ref<Class<Doc>>
@Prop(TypeString(), 'Collection' as IntlString)
@Prop(TypeString(), core.string.Collection)
@Hidden()
collection!: string
}

View File

@ -1,6 +1,5 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022 Hardcore Engineering Inc.
// 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
@ -14,16 +13,16 @@
// limitations under the License.
//
import { Doc, Domain, FindOptions, IndexKind, Ref } from '@anticrm/core'
import { Domain, IndexKind, Ref } from '@anticrm/core'
import type { Category, Product, Variant } from '@anticrm/inventory'
import { Builder, Collection, Index, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import core, { TAttachedDoc } from '@anticrm/model-core'
import { createAction } from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import type {} from '@anticrm/view'
import view from '@anticrm/view'
import inventory from './plugin'
import { createAction } from '@anticrm/model-view'
export const DOMAIN_INVENTORY = 'inventory' as Domain
@Model(inventory.class.Category, core.class.AttachedDoc, DOMAIN_INVENTORY)
@ -97,10 +96,6 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: inventory.class.Product,
descriptor: view.viewlet.Table,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: { attachedTo: inventory.class.Category }
} as FindOptions<Doc>,
config: ['', '$lookup.attachedTo', 'modifiedOn']
})

View File

@ -15,7 +15,7 @@
// To help typescript locate view plugin properly
import type { Employee } from '@anticrm/contact'
import { Doc, FindOptions, IndexKind, Lookup, Ref } from '@anticrm/core'
import { Doc, FindOptions, IndexKind, Ref } from '@anticrm/core'
import type { Customer, Funnel, Lead } from '@anticrm/lead'
import { Builder, Collection, Index, Mixin, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
@ -121,10 +121,6 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: lead.mixin.Customer,
descriptor: view.viewlet.Table,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: { _id: { channels: contact.class.Channel }, _class: core.class.Class }
} as FindOptions<Doc>, // TODO: fix
config: [
'',
{ key: '$lookup._class.label', label: contact.string.TypeLabel },
@ -134,30 +130,26 @@ export function createModel (builder: Builder): void {
]
})
const leadLookup: Lookup<Lead> = {
attachedTo: [lead.mixin.Customer, { _id: { channels: contact.class.Channel } }],
state: task.class.State,
doneState: task.class.DoneState
}
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: lead.class.Lead,
descriptor: task.viewlet.StatusTable,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: leadLookup
} as FindOptions<Doc>, // TODO: fix
config: [
'',
'$lookup.attachedTo',
'$lookup.state',
'$lookup.doneState',
{
key: '',
presenter: attachment.component.AttachmentsPresenter,
label: attachment.string.Files,
sortingKey: 'attachments'
},
{ presenter: chunter.component.CommentsPresenter, label: chunter.string.Comments, sortingKey: 'comments' },
{
key: '',
presenter: chunter.component.CommentsPresenter,
label: chunter.string.Comments,
sortingKey: 'comments'
},
'modifiedOn',
'$lookup.attachedTo.$lookup.channels'
]

View File

@ -236,32 +236,31 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: recruit.mixin.Candidate,
descriptor: view.viewlet.Table,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {
_id: {
channels: contact.class.Channel
// skills: tags.class.TagReference // Required if TagsItemPresenter is used
}
}
} as FindOptions<Doc>, // TODO: fix
config: [
'',
'title',
'city',
{
key: '',
presenter: recruit.component.ApplicationsPresenter,
label: recruit.string.ApplicationsShort,
sortingKey: 'applications'
},
{
key: '',
presenter: attachment.component.AttachmentsPresenter,
label: attachment.string.Files,
sortingKey: 'attachments'
},
{ presenter: chunter.component.CommentsPresenter, label: chunter.string.Comments, sortingKey: 'comments' },
{
key: '',
presenter: chunter.component.CommentsPresenter,
label: chunter.string.Comments,
sortingKey: 'comments'
},
{
// key: '$lookup.skills', // Required, since presenter require list of tag references or '' and TagsPopupPresenter
key: '',
presenter: tags.component.TagsPresenter, // tags.component.TagsPresenter,
label: recruit.string.SkillsLabel,
sortingKey: 'skills',
@ -275,20 +274,9 @@ export function createModel (builder: Builder): void {
]
})
const applicantTableLookup: Lookup<Applicant> = {
attachedTo: [recruit.mixin.Candidate, { _id: { channels: contact.class.Channel } }],
state: task.class.State,
assignee: contact.class.Employee,
doneState: task.class.DoneState
}
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: recruit.class.Applicant,
descriptor: task.viewlet.StatusTable,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: applicantTableLookup
} as FindOptions<Doc>, // TODO: fix
config: [
'',
'$lookup.attachedTo',
@ -296,11 +284,17 @@ export function createModel (builder: Builder): void {
'$lookup.state',
'$lookup.doneState',
{
key: '',
presenter: attachment.component.AttachmentsPresenter,
label: attachment.string.Files,
sortingKey: 'attachments'
},
{ presenter: chunter.component.CommentsPresenter, label: chunter.string.Comments, sortingKey: 'comments' },
{
key: '',
presenter: chunter.component.CommentsPresenter,
label: chunter.string.Comments,
sortingKey: 'comments'
},
'modifiedOn',
'$lookup.attachedTo.$lookup.channels'
]

View File

@ -117,8 +117,6 @@ function createTableViewlet (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: recruit.class.Review,
descriptor: view.viewlet.Table,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: reviewTableOptions,
config: reviewTableConfig
})

View File

@ -335,14 +335,6 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: task.class.Issue,
descriptor: task.viewlet.StatusTable,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {
assignee: contact.class.Employee,
state: task.class.State,
doneState: task.class.DoneState
}
} as FindOptions<Doc>,
config: [
'',
'name',
@ -350,11 +342,17 @@ export function createModel (builder: Builder): void {
'$lookup.state',
'$lookup.doneState',
{
key: '',
presenter: attachment.component.AttachmentsPresenter,
label: attachment.string.Files,
sortingKey: 'attachments'
},
{ presenter: chunter.component.CommentsPresenter, label: chunter.string.Comments, sortingKey: 'comments' },
{
key: '',
presenter: chunter.component.CommentsPresenter,
label: chunter.string.Comments,
sortingKey: 'comments'
},
'modifiedOn'
]
})

View File

@ -30,6 +30,8 @@
"@anticrm/platform": "~0.6.6",
"@anticrm/ui": "~0.6.0",
"@anticrm/view": "~0.6.0",
"@anticrm/model-core": "~0.6.0"
"@anticrm/view-resources": "~0.6.0",
"@anticrm/model-core": "~0.6.0",
"@anticrm/model-preference": "~0.6.0"
}
}

View File

@ -24,6 +24,7 @@ import type {
ActionCategory,
AttributeEditor,
AttributePresenter,
BuildModelKey,
CollectionEditor,
HTMLPresenter,
IgnoreActions,
@ -41,8 +42,10 @@ import type {
ViewActionInput,
ViewContext,
Viewlet,
ViewletDescriptor
ViewletDescriptor,
ViewletPreference
} from '@anticrm/view'
import preference, { TPreference } from '@anticrm/model-preference'
import view from './plugin'
export { viewOperation } from './migration'
@ -118,6 +121,12 @@ export class TObjectFactory extends TClass implements ObjectFactory {
component!: AnyComponent
}
@Model(view.class.ViewletPreference, preference.class.Preference)
export class TViewletPreference extends TPreference implements ViewletPreference {
attachedTo!: Ref<Viewlet>
config!: (BuildModelKey | string)[]
}
@Model(view.class.ViewletDescriptor, core.class.Doc, DOMAIN_MODEL)
export class TViewletDescriptor extends TDoc implements ViewletDescriptor {
component!: AnyComponent
@ -129,7 +138,7 @@ export class TViewlet extends TDoc implements Viewlet {
attachTo!: Ref<Class<Space>>
descriptor!: Ref<ViewletDescriptor>
open!: AnyComponent
config: any
config!: (BuildModelKey | string)[]
}
@Model(view.class.Action, core.class.Doc, DOMAIN_MODEL)
@ -218,6 +227,7 @@ export function createModel (builder: Builder): void {
TAttributePresenter,
TCollectionEditor,
TObjectEditor,
TViewletPreference,
TViewletDescriptor,
TViewlet,
TAction,
@ -300,7 +310,7 @@ export function createModel (builder: Builder): void {
{
label: view.string.Table,
icon: view.icon.Table,
component: view.component.TableView
component: view.component.TableBrowser
},
view.viewlet.Table
)

View File

@ -16,7 +16,8 @@
import { Ref } from '@anticrm/core'
import { IntlString, mergeIds } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
import view, { Action, ViewAction, viewId } from '@anticrm/view'
import { Action, ViewAction, viewId } from '@anticrm/view'
import view from '@anticrm/view-resources/src/plugin'
export default mergeIds(viewId, view, {
action: {
@ -70,7 +71,7 @@ export default mergeIds(viewId, view, {
TimestampPresenter: '' as AnyComponent,
DateEditor: '' as AnyComponent,
DatePresenter: '' as AnyComponent,
TableView: '' as AnyComponent,
TableBrowser: '' as AnyComponent,
RolePresenter: '' as AnyComponent,
YoutubePresenter: '' as AnyComponent,
GithubPresenter: '' as AnyComponent,
@ -83,7 +84,6 @@ export default mergeIds(viewId, view, {
string: {
Table: '' as IntlString,
Delete: '' as IntlString,
Move: '' as IntlString,
Role: '' as IntlString,
// Keybaord actions
MoveUp: '' as IntlString,

View File

@ -630,7 +630,11 @@ export class LiveQuery extends TxProcessor implements Client {
private async __updateDoc (q: Query, updatedDoc: WithLookup<Doc>, tx: TxUpdateDoc<Doc>): Promise<void> {
TxProcessor.updateDoc2Doc(updatedDoc, tx)
const ops = tx.operations as any
const ops = {
...tx.operations,
modifiedBy: tx.modifiedBy,
modifiedOn: tx.modifiedOn
} as any
for (const key in ops) {
if (!key.startsWith('$')) {
if (q.options !== undefined) {

View File

@ -15,10 +15,10 @@
-->
<script lang="ts">
import { Doc, DocumentQuery } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { Button, Icon, IconAdd, Label, SearchEdit, showPopup } from '@anticrm/ui'
import view, { Viewlet } from '@anticrm/view'
import { ActionContext, TableBrowser } from '@anticrm/view-resources'
import { createQuery, getClient } from '@anticrm/presentation'
import { ActionIcon, Button, Icon, IconAdd, Label, Loading, SearchEdit, showPopup } from '@anticrm/ui'
import view, { Viewlet, ViewletPreference } from '@anticrm/view'
import { ActionContext, TableBrowser, ViewletSetting } from '@anticrm/view-resources'
import contact from '../plugin'
import CreateContact from './CreateContact.svelte'
@ -29,11 +29,34 @@
resultQuery = search === '' ? {} : { $search: search }
}
let viewlet: Viewlet | undefined
let loading = true
const preferenceQuery = createQuery()
let preference: ViewletPreference | undefined
const client = getClient()
const tableDescriptor = client.findOne<Viewlet>(view.class.Viewlet, {
attachTo: contact.class.Contact,
descriptor: view.viewlet.Table
})
client
.findOne<Viewlet>(view.class.Viewlet, {
attachTo: contact.class.Contact,
descriptor: view.viewlet.Table
})
.then((res) => {
viewlet = res
if (res !== undefined) {
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: res._id
},
(res) => {
preference = res[0]
loading = false
},
{ limit: 1 }
)
}
})
function showCreateDialog (ev: Event) {
showPopup(CreateContact, { space: contact.space.Contacts, targetElement: ev.target }, ev.target as HTMLElement)
@ -52,6 +75,17 @@
<span class="ac-header__title"><Label label={contact.string.Contacts} /></span>
</div>
{#if viewlet}
<ActionIcon
icon={view.icon.Setting}
direction={'top'}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet })
}}
/>
{/if}
<SearchEdit
bind:value={search}
on:change={() => {
@ -66,15 +100,17 @@
/>
</div>
{#await tableDescriptor then descr}
{#if descr}
{#if viewlet}
{#if loading}
<Loading />
{:else}
<TableBrowser
_class={contact.class.Contact}
config={descr.config}
options={descr.options}
config={preference?.config ?? viewlet.config}
options={viewlet.options}
query={resultQuery}
showNotification
/>
{/if}
{/await}
{/if}
</div>

View File

@ -20,6 +20,7 @@
import { buildModel } from '@anticrm/view-resources'
import { Category } from '@anticrm/inventory'
import HierarchyElement from './HierarchyElement.svelte'
import { buildConfigLookup } from '@anticrm/view-resources/src/utils'
export let _class: Ref<Class<Category>>
export let query: DocumentQuery<Category> = {}
@ -59,9 +60,11 @@
}
const client = getClient()
$: lookup = buildConfigLookup(client.getHierarchy(), _class, config)
</script>
{#await buildModel({ client, _class, keys: config, options })}
{#await buildModel({ client, _class, keys: config, lookup })}
<Loading />
{:then model}
<table class="table-body">

View File

@ -20,25 +20,49 @@
Icon,
IconSearch,
Label,
Scroller,
showPopup,
IconAdd,
eventToHTMLElement
eventToHTMLElement,
Loading,
ActionIcon
} from '@anticrm/ui'
import CreateProduct from './CreateProduct.svelte'
import inventory from '../plugin'
import { Table } from '@anticrm/view-resources'
import { getClient } from '@anticrm/presentation'
import view, { Viewlet } from '@anticrm/view'
import { TableBrowser, ViewletSetting } from '@anticrm/view-resources'
import { createQuery, getClient } from '@anticrm/presentation'
import view, { Viewlet, ViewletPreference } from '@anticrm/view'
let search = ''
$: resultQuery = search === '' ? {} : { $search: search }
let descr: Viewlet | undefined
let loading = true
const preferenceQuery = createQuery()
let preference: ViewletPreference | undefined
const client = getClient()
const tableDescriptor = client.findOne<Viewlet>(view.class.Viewlet, {
attachTo: inventory.class.Product,
descriptor: view.viewlet.Table
})
client
.findOne<Viewlet>(view.class.Viewlet, {
attachTo: inventory.class.Product,
descriptor: view.viewlet.Table
})
.then((res) => {
descr = res
if (res !== undefined) {
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: res._id
},
(res) => {
preference = res[0]
loading = false
},
{ limit: 1 }
)
}
})
function showCreateDialog (ev: MouseEvent) {
showPopup(CreateProduct, { space: inventory.space.Products }, eventToHTMLElement(ev))
@ -51,6 +75,17 @@
<span class="ac-header__title"><Label label={inventory.string.Products} /></span>
</div>
{#if descr}
<ActionIcon
icon={view.icon.Setting}
direction={'top'}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet: descr })
}}
/>
{/if}
<EditWithIcon
icon={IconSearch}
placeholder={ui.string.Search}
@ -67,17 +102,16 @@
/>
</div>
<Scroller tableFade>
{#await tableDescriptor then descr}
{#if descr}
<Table
_class={inventory.class.Product}
config={descr.config}
options={descr.options}
query={resultQuery}
showNotification
highlightRows
/>
{/if}
{/await}
</Scroller>
{#if descr}
{#if loading}
<Loading />
{:else}
<TableBrowser
_class={inventory.class.Product}
config={preference?.config ?? descr.config}
options={descr.options}
query={resultQuery}
showNotification
/>
{/if}
{/if}

View File

@ -15,20 +15,43 @@
-->
<script lang="ts">
import { Doc, DocumentQuery } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { Icon, Label, Scroller, SearchEdit } from '@anticrm/ui'
import view, { Viewlet } from '@anticrm/view'
import { Table } from '@anticrm/view-resources'
import { createQuery, getClient } from '@anticrm/presentation'
import { ActionIcon, Icon, Label, Loading, showPopup, SearchEdit } from '@anticrm/ui'
import view, { Viewlet, ViewletPreference } from '@anticrm/view'
import { ViewletSetting, TableBrowser } from '@anticrm/view-resources'
import lead from '../plugin'
let search = ''
let resultQuery: DocumentQuery<Doc> = {}
let descr: Viewlet | undefined
let loading = true
const preferenceQuery = createQuery()
let preference: ViewletPreference | undefined
const client = getClient()
const tableDescriptor = client.findOne<Viewlet>(view.class.Viewlet, {
attachTo: lead.mixin.Customer,
descriptor: view.viewlet.Table
})
client
.findOne<Viewlet>(view.class.Viewlet, {
attachTo: lead.mixin.Customer,
descriptor: view.viewlet.Table
})
.then((res) => {
descr = res
if (res !== undefined) {
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: res._id
},
(res) => {
preference = res[0]
loading = false
},
{ limit: 1 }
)
}
})
function updateResultQuery (search: string): void {
resultQuery = search === '' ? {} : { $search: search }
@ -41,6 +64,17 @@
<span class="ac-header__title"><Label label={lead.string.Customers} /></span>
</div>
{#if descr}
<ActionIcon
icon={view.icon.Setting}
direction={'top'}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet: descr })
}}
/>
{/if}
<SearchEdit
bind:value={search}
on:change={() => {
@ -49,17 +83,16 @@
/>
</div>
<Scroller tableFade>
{#await tableDescriptor then descr}
{#if descr}
<Table
_class={lead.mixin.Customer}
config={descr.config}
options={descr.options}
query={resultQuery}
showNotification
highlightRows
/>
{/if}
{/await}
</Scroller>
{#if descr}
{#if loading}
<Loading />
{:else}
<TableBrowser
_class={lead.mixin.Customer}
config={preference?.config ?? descr.config}
options={descr.options}
query={resultQuery}
showNotification
/>
{/if}
{/if}

View File

@ -13,9 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import type { FindOptions, Ref } from '@anticrm/core'
import type { Customer, Lead } from '@anticrm/lead'
import task from '@anticrm/task'
import type { Ref } from '@anticrm/core'
import type { Customer } from '@anticrm/lead'
import { CircleButton, IconAdd, Label, showPopup } from '@anticrm/ui'
import { Table } from '@anticrm/view-resources'
import lead from '../plugin'
@ -28,12 +27,6 @@
const createLead = (ev: MouseEvent): void => {
showPopup(CreateLead, { candidate: objectId, preserveCandidate: true }, ev.target as HTMLElement)
}
const options: FindOptions<Lead> = {
lookup: {
state: task.class.State
}
}
</script>
<div class="applications-container">
@ -42,13 +35,7 @@
<CircleButton icon={IconAdd} size={'small'} selected on:click={createLead} />
</div>
{#if leads !== undefined && leads > 0}
<Table
_class={lead.class.Lead}
config={['', '$lookup.state']}
{options}
query={{ attachedTo: objectId }}
{loadingProps}
/>
<Table _class={lead.class.Lead} config={['', '$lookup.state']} query={{ attachedTo: objectId }} {loadingProps} />
{:else}
<div class="flex-col-center mt-5 createapp-container">
<div class="text-sm content-dark-color mt-2">

View File

@ -14,18 +14,11 @@
// limitations under the License.
-->
<script lang="ts">
import { FindOptions } from '@anticrm/core'
import type { Customer, Lead } from '@anticrm/lead'
import task from '@anticrm/task'
import type { Customer } from '@anticrm/lead'
import { Table } from '@anticrm/view-resources'
import leads from '../plugin'
export let value: Customer
const options: FindOptions<Lead> = {
lookup: {
state: task.class.State
}
}
</script>
<Table _class={leads.class.Lead} config={['', '$lookup.state']} {options} query={{ attachedTo: value._id }} />
<Table _class={leads.class.Lead} config={['', '$lookup.state']} query={{ attachedTo: value._id }} />

View File

@ -13,18 +13,15 @@
// limitations under the License.
-->
<script lang="ts">
import type { Doc, FindOptions, Ref } from '@anticrm/core'
import core from '@anticrm/core'
import task from '@anticrm/task'
import attachment from '@anticrm/attachment'
import chunter from '@anticrm/chunter'
import type { Doc, Ref } from '@anticrm/core'
import { CircleButton, IconAdd, Label, showPopup } from '@anticrm/ui'
import { BuildModelKey } from '@anticrm/view'
import { Table } from '@anticrm/view-resources'
import recruit from '../plugin'
import CreateApplication from './CreateApplication.svelte'
import FileDuo from './icons/FileDuo.svelte'
import chunter from '@anticrm/chunter'
import attachment from '@anticrm/attachment'
import { Applicant } from '@anticrm/recruit'
import { BuildModelKey } from '@anticrm/view'
export let objectId: Ref<Doc>
// export let space: Ref<Space>
@ -35,13 +32,6 @@
const createApp = (ev: MouseEvent): void => {
showPopup(CreateApplication, { candidate: objectId, preserveCandidate: true }, ev.target as HTMLElement)
}
const options: FindOptions<Applicant> = {
lookup: {
state: task.class.State,
space: core.class.Space,
doneState: task.class.DoneState
}
}
const config: (BuildModelKey | string)[] = [
'',
'$lookup.space.name',
@ -66,7 +56,6 @@
<Table
_class={recruit.class.Applicant}
{config}
{options}
query={{ attachedTo: objectId }}
loadingProps={{ length: applications }}
/>

View File

@ -14,26 +14,16 @@
// limitations under the License.
-->
<script lang="ts">
import core, { FindOptions } from '@anticrm/core'
import type { Applicant, Candidate } from '@anticrm/recruit'
import type { Candidate } from '@anticrm/recruit'
import recruit from '@anticrm/recruit'
import task from '@anticrm/task'
import { Table } from '@anticrm/view-resources'
export let value: Candidate
const options: FindOptions<Applicant> = {
lookup: {
state: task.class.State,
space: core.class.Space,
doneState: task.class.DoneState
}
}
</script>
<Table
_class={recruit.class.Applicant}
config={['', '$lookup.space.name', '$lookup.state', '$lookup.doneState']}
{options}
query={{ attachedTo: value._id }}
loadingProps={{ length: value.applications ?? 0 }}
/>

View File

@ -1,28 +1,25 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// 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 attachment from '@anticrm/attachment'
import chunter from '@anticrm/chunter'
import contact from '@anticrm/contact'
import { Doc, DocumentQuery, FindOptions } from '@anticrm/core'
import { Doc, DocumentQuery } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import { Applicant } from '@anticrm/recruit'
import task from '@anticrm/task'
import { Button, Icon, IconAdd, Label, SearchEdit, showPopup } from '@anticrm/ui'
import { BuildModelKey } from '@anticrm/view'
import { TableBrowser } from '@anticrm/view-resources'
import { ActionIcon, Button, Icon, IconAdd, Label, Loading, SearchEdit, showPopup } from '@anticrm/ui'
import view, { Viewlet, ViewletPreference } from '@anticrm/view'
import { TableBrowser, ViewletSetting } from '@anticrm/view-resources'
import recruit from '../plugin'
import CreateApplication from './CreateApplication.svelte'
@ -31,32 +28,35 @@
const baseQuery: DocumentQuery<Applicant> = {
doneState: null
}
const client = getClient()
const config: (BuildModelKey | string)[] = [
'',
'$lookup.space',
'$lookup.attachedTo',
'$lookup.assignee',
'$lookup.state',
{
key: '',
presenter: attachment.component.AttachmentsPresenter,
label: attachment.string.Files,
sortingKey: 'attachments'
},
{ key: '', presenter: chunter.component.CommentsPresenter, label: chunter.string.Comments, sortingKey: 'comments' },
'modifiedOn',
'$lookup.attachedTo.$lookup.channels'
]
let descr: Viewlet | undefined
let loading = true
const options: FindOptions<Applicant> = {
lookup: {
attachedTo: [recruit.mixin.Candidate, { _id: { channels: contact.class.Channel } }],
state: task.class.State,
assignee: contact.class.Employee,
space: recruit.class.Vacancy
}
}
const preferenceQuery = createQuery()
let preference: ViewletPreference | undefined
client
.findOne<Viewlet>(view.class.Viewlet, {
attachTo: recruit.class.Applicant,
descriptor: task.viewlet.StatusTable
})
.then((res) => {
descr = res
if (res !== undefined) {
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: res._id
},
(res) => {
preference = res[0]
loading = false
},
{ limit: 1 }
)
}
})
function showCreateDialog () {
showPopup(CreateApplication, {}, 'top')
@ -73,6 +73,17 @@
<span class="ac-header__title"><Label label={recruit.string.Applications} /></span>
</div>
{#if descr}
<ActionIcon
icon={view.icon.Setting}
direction={'top'}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet: descr })
}}
/>
{/if}
<SearchEdit
bind:value={search}
on:change={() => {
@ -82,4 +93,16 @@
<Button icon={IconAdd} label={recruit.string.ApplicationCreateLabel} kind={'primary'} on:click={showCreateDialog} />
</div>
<TableBrowser _class={recruit.class.Applicant} {config} {options} query={resultQuery} showNotification />
{#if descr}
{#if loading}
<Loading />
{:else}
<TableBrowser
_class={recruit.class.Applicant}
config={preference?.config ?? descr.config}
options={descr.options}
query={resultQuery}
showNotification
/>
{/if}
{/if}

View File

@ -18,19 +18,43 @@
import { Doc, DocumentQuery, Ref } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import tags, { selectedTagElements, TagCategory, TagElement } from '@anticrm/tags'
import { Component, Icon, Label, SearchEdit } from '@anticrm/ui'
import view, { Viewlet } from '@anticrm/view'
import { ActionContext, TableBrowser } from '@anticrm/view-resources'
import { ActionIcon, showPopup, Component, Icon, Label, Loading, SearchEdit } from '@anticrm/ui'
import view, { Viewlet, ViewletPreference } from '@anticrm/view'
import { ActionContext, TableBrowser, ViewletSetting } from '@anticrm/view-resources'
import recruit from '../plugin'
let search = ''
let resultQuery: DocumentQuery<Doc> = {}
const client = getClient()
const tableDescriptor = client.findOne<Viewlet>(view.class.Viewlet, {
attachTo: recruit.mixin.Candidate,
descriptor: view.viewlet.Table
})
let descr: Viewlet | undefined
let loading = true
const preferenceQuery = createQuery()
let preference: ViewletPreference | undefined
client
.findOne<Viewlet>(view.class.Viewlet, {
attachTo: recruit.mixin.Candidate,
descriptor: view.viewlet.Table
})
.then((res) => {
descr = res
if (res !== undefined) {
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: res._id
},
(res) => {
preference = res[0]
loading = false
},
{ limit: 1 }
)
}
})
let category: Ref<TagCategory> | undefined = undefined
@ -63,6 +87,17 @@
<span class="ac-header__title"><Label label={recruit.string.Candidates} /></span>
</div>
{#if descr}
<ActionIcon
icon={view.icon.Setting}
direction={'top'}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet: descr })
}}
/>
{/if}
<SearchEdit
bind:value={search}
on:change={() => {
@ -82,14 +117,16 @@
mode: 'browser'
}}
/>
{#await tableDescriptor then descr}
{#if descr}
{#if descr}
{#if loading}
<Loading />
{:else}
<TableBrowser
_class={recruit.mixin.Candidate}
config={descr.config}
config={preference?.config ?? descr.config}
options={descr.options}
query={resultQuery}
showNotification
/>
{/if}
{/await}
{/if}

View File

@ -13,8 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import contact from '@anticrm/contact'
import core, { Doc, DocumentQuery, Lookup, Ref } from '@anticrm/core'
import core, { Doc, DocumentQuery, Ref } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import { Vacancy } from '@anticrm/recruit'
import { Button, getCurrentLocation, Icon, IconAdd, Label, navigate, SearchEdit, showPopup } from '@anticrm/ui'
@ -31,9 +30,6 @@
let search: string = ''
let resultQuery: DocumentQuery<Doc> = {}
const lookup: Lookup<Vacancy> = {
company: contact.class.Organization
}
$: resultQuery = search === '' ? {} : { $search: search }
@ -118,9 +114,6 @@
sortingFunction: modifiedSorting
}
]}
options={{
lookup
}}
query={{
...resultQuery,
archived: false

View File

@ -14,7 +14,6 @@
-->
<script lang="ts">
import type { Doc, Ref } from '@anticrm/core'
import core from '@anticrm/core'
import { CircleButton, IconAdd, Label, showPopup } from '@anticrm/ui'
import { Table } from '@anticrm/view-resources'
import recruit from '../../plugin'
@ -39,11 +38,6 @@
<Table
_class={recruit.class.Opinion}
config={['', 'value', 'description', '$lookup.modifiedBy']}
options={{
lookup: {
modifiedBy: core.class.Account
}
}}
query={{ attachedTo: objectId }}
loadingProps={{ length: opinions }}
/>

View File

@ -15,8 +15,8 @@
<script lang="ts">
import attachment from '@anticrm/attachment'
import chunter from '@anticrm/chunter'
import contact, { EmployeeAccount } from '@anticrm/contact'
import { Class, DocumentQuery, FindOptions, getCurrentAccount, Ref } from '@anticrm/core'
import { EmployeeAccount } from '@anticrm/contact'
import { Class, DocumentQuery, getCurrentAccount, Ref } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import tags, { selectedTagElements, TagCategory, TagElement } from '@anticrm/tags'
import { DoneState, Task } from '@anticrm/task'
@ -69,14 +69,6 @@
category = detail.category ?? undefined
selectedTagElements.set(Array.from(detail.elements ?? []).map((it) => it._id))
}
const taskOptions: FindOptions<Task> = {
lookup: {
attachedTo: [contact.class.Person, { _id: { channels: contact.class.Channel } }],
state: task.class.State,
assignee: contact.class.Employee,
doneState: task.class.DoneState
}
}
</script>
<div class="ac-header full">
@ -121,7 +113,6 @@
},
'modifiedOn'
]}
options={taskOptions}
query={resultQuery}
showNotification
/>

View File

@ -33,21 +33,19 @@
let doneStatusesView: boolean = false
let state: Ref<State> | undefined = undefined
let selectedDoneStates: Set<Ref<DoneState>> = new Set<Ref<DoneState>>()
let resConfig = config
$: resConfig = updateConfig(config)
let query = {}
let doneStates: DoneState[] = []
let withoutDone: boolean = false
function updateConfig (): void {
function updateConfig (config: string[]): string[] {
if (state !== undefined) {
resConfig = config.filter((p) => p !== '$lookup.state')
return
return config.filter((p) => p !== '$lookup.state')
}
if (selectedDoneStates.size === 1) {
resConfig = config.filter((p) => p !== '$lookup.doneState')
return
return config.filter((p) => p !== '$lookup.doneState')
}
resConfig = config
return config
}
const doneStateQuery = createQuery()
@ -66,7 +64,7 @@
)
async function updateQuery (search: string, selectedDoneStates: Set<Ref<DoneState>>): Promise<void> {
updateConfig()
resConfig = updateConfig(config)
const result = {} as DocumentQuery<Task>
if (search !== '') {
result.$search = search

View File

@ -185,7 +185,7 @@
</div>
</div>
{/if}
{#await buildModel({ client, _class, keys: itemsConfig, options }) then itemModels}
{#await buildModel({ client, _class, keys: itemsConfig, lookup: options.lookup }) then itemModels}
<div class="listRoot">
{#if groupedIssues[category]}
{#each groupedIssues[category] as docObject (docObject._id)}

View File

@ -13,25 +13,15 @@
// limitations under the License.
-->
<script lang="ts">
import { FindOptions, Ref, SortingOrder } from '@anticrm/core'
import { Ref } from '@anticrm/core'
import { Team } from '@anticrm/tracker'
import { Button, IconAdd, Label, showPopup } from '@anticrm/ui'
import { Table } from '@anticrm/view-resources'
import contact from '@anticrm/contact'
import { Project, Team } from '@anticrm/tracker'
import NewProject from './NewProject.svelte'
import plugin from '../../plugin'
import NewProject from './NewProject.svelte'
export let space: Ref<Team>
const options: FindOptions<Project> = {
sort: {
startDate: SortingOrder.Ascending
},
lookup: {
lead: contact.class.Employee
}
}
async function showCreateDialog () {
showPopup(NewProject, { space, targetElement: null }, null)
}
@ -58,7 +48,6 @@
}
]}
query={{}}
{options}
/>
</div>

View File

@ -49,6 +49,9 @@
<symbol id='arrow-right' viewBox="0 0 16 16">
<path d="M13.334 8L13.6875 7.64645L14.0411 8L13.6875 8.35355L13.334 8ZM3.33398 8.5C3.05784 8.5 2.83398 8.27614 2.83398 8C2.83398 7.72386 3.05784 7.5 3.33398 7.5V8.5ZM9.68754 3.64645L13.6875 7.64645L12.9804 8.35355L8.98043 4.35355L9.68754 3.64645ZM13.6875 8.35355L9.68754 12.3536L8.98043 11.6464L12.9804 7.64645L13.6875 8.35355ZM13.334 8.5H3.33398V7.5H13.334V8.5Z"/>
</symbol>
<symbol id="setting" viewBox="0 0 16 16">
<path d="M14,5.8l-1.1-1.9c-0.6-1-0.9-1.5-1.5-1.8c-0.6-0.3-1.2-0.3-2.3-0.4L8,1.7l-1.1,0c-1.1,0-1.8,0-2.3,0.4 C4,2.4,3.7,2.9,3.1,3.9L2,5.8C1.4,6.8,1.1,7.4,1.1,8S1.4,9.2,2,10.2l1.1,1.9c0.6,1,0.9,1.5,1.5,1.8c0.6,0.3,1.2,0.3,2.3,0.4l1.1,0 l1.1,0c1.1,0,1.8,0,2.3-0.4c0.6-0.3,0.9-0.9,1.5-1.8l1.1-1.9c0.6-1,0.9-1.5,0.9-2.2C14.9,7.4,14.6,6.8,14,5.8z M13.1,9.7L12,11.6 c-0.5,0.9-0.8,1.3-1.1,1.5c-0.3,0.2-0.8,0.2-1.8,0.2l-1.1,0l-1.1,0c-1.1,0-1.5,0-1.8-0.2c-0.3-0.2-0.6-0.6-1.1-1.5L2.9,9.7 C2.3,8.8,2.1,8.4,2.1,8c0-0.4,0.2-0.8,0.7-1.7L4,4.4c0.5-0.9,0.8-1.3,1.1-1.5c0.3-0.2,0.8-0.2,1.8-0.2l1.1,0l1.1,0 c1,0,1.5,0,1.8,0.2c0.3,0.2,0.6,0.6,1.1,1.5l1.1,1.9c0.5,0.9,0.7,1.3,0.7,1.7S13.6,8.8,13.1,9.7z"/>
<path d="M8,5.5C6.6,5.5,5.5,6.6,5.5,8c0,1.4,1.1,2.5,2.5,2.5c1.4,0,2.5-1.1,2.5-2.5C10.5,6.6,9.4,5.5,8,5.5z M8,9.5 C7.2,9.5,6.5,8.8,6.5,8S7.2,6.5,8,6.5S9.5,7.2,9.5,8S8.8,9.5,8,9.5z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -36,6 +36,7 @@
"Type": "Type",
"WithTime": "WithTime",
"CreatingAttribute": "Creating an attribute",
"CreatingAttributeConfirm": "Warning: It will not be possible for now to change or delete created attribute."
"CreatingAttributeConfirm": "Warning: It will not be possible for now to change or delete created attribute.",
"CustomizeView": "Customize view"
}
}

View File

@ -33,6 +33,7 @@
"SelectItemAll": "Выбрать все",
"SelectItemNone": "Снять все выделения",
"ShowPreview": "Предпросмотр документа",
"ShowActions": "Показать действия"
"ShowActions": "Показать действия",
"CustomizeView": "Настроить отображение"
}
}

View File

@ -26,6 +26,7 @@ loadMetadata(view.icon, {
Archive: `${icons}#archive`,
Statuses: `${icons}#statuses`,
Open: `${icons}#open`,
Setting: `${icons}#setting`,
ArrowRight: `${icons}#arrow-right`
})

View File

@ -39,8 +39,10 @@
"@anticrm/view": "~0.6.0",
"@anticrm/ui": "~0.6.0",
"@anticrm/task": "~0.6.0",
"@anticrm/preference": "~0.6.0",
"@anticrm/notification": "~0.6.0",
"@anticrm/presentation": "~0.6.2",
"@anticrm/setting": "~0.6.1"
"@anticrm/setting": "~0.6.1",
"fast-equals": "^2.0.3"
}
}

View File

@ -14,14 +14,14 @@
// limitations under the License.
-->
<script lang="ts">
import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
import type { Class, Doc, DocumentQuery, FindOptions, Lookup, Ref } from '@anticrm/core'
import { getObjectValue, SortingOrder } from '@anticrm/core'
import notification from '@anticrm/notification'
import { createQuery, getClient } from '@anticrm/presentation'
import { CheckBox, Component, IconDown, IconUp, Label, Loading, showPopup, Spinner } from '@anticrm/ui'
import { BuildModelKey } from '@anticrm/view'
import { createEventDispatcher } from 'svelte'
import { buildModel, LoadingProps } from '../utils'
import { buildConfigLookup, buildModel, LoadingProps } from '../utils'
import Menu from './Menu.svelte'
export let _class: Ref<Class<Doc>>
@ -39,6 +39,11 @@
export let selection: number | undefined = undefined
export let checked: Doc[] = []
const client = getClient()
const hierarchy = client.getHierarchy()
$: lookup = buildConfigLookup(hierarchy, _class, config)
let sortKey = 'modifiedOn'
let sortOrder = SortingOrder.Descending
let loading = 0
@ -60,6 +65,7 @@
query: DocumentQuery<Doc>,
sortKey: string,
sortOrder: SortingOrder,
lookup: Lookup<Doc>,
options?: FindOptions<Doc>
) {
const update = q.query(
@ -74,15 +80,13 @@
dispatch('content', objects)
loading = loading === 1 ? 0 : -1
},
{ sort: { [sortKey]: sortOrder }, limit: 200, ...options }
{ sort: { [sortKey]: sortOrder }, limit: 200, lookup, ...options }
)
if (update && ++loading > 0) {
objects = []
}
}
$: update(_class, query, sortKey, sortOrder, options)
const client = getClient()
$: update(_class, query, sortKey, sortOrder, lookup, options)
const showMenu = async (ev: MouseEvent, object: Doc, row: number): Promise<void> => {
selection = row
@ -150,7 +154,7 @@
}
</script>
{#await buildModel({ client, _class, keys: config, options })}
{#await buildModel({ client, _class, keys: config, lookup })}
<Loading />
{:then model}
<table class="antiTable" class:metaColumn={enableChecking || showNotification} class:highlightRows>
@ -244,6 +248,7 @@
<svelte:component
this={attribute.presenter}
value={getObjectValue(attribute.key, object) ?? ''}
{object}
{...attribute.props}
/>
<!-- <div
@ -260,6 +265,7 @@
<svelte:component
this={attribute.presenter}
value={getObjectValue(attribute.key, object) ?? ''}
{object}
{...attribute.props}
/>
</td>

View File

@ -0,0 +1,179 @@
<!--
// 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, { Card, createQuery, getAttributePresenterClass, getClient } from '@anticrm/presentation'
import { BuildModelKey, Viewlet, ViewletPreference } from '@anticrm/view'
import core, { ArrOf, Class, Doc, Lookup, Ref, Type } from '@anticrm/core'
import { Grid, MiniToggle } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import { IntlString } from '@anticrm/platform'
import { buildConfigLookup, getLookupClass, getLookupLabel, getLookupProperty } from '../utils'
import { deepEqual } from 'fast-equals'
import preferencePlugin from '@anticrm/preference'
import view from '../plugin'
export let viewlet: Viewlet
let preference: ViewletPreference | undefined
const preferenceQuery = createQuery()
$: viewlet &&
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: viewlet._id
},
(res) => {
preference = res[0]
attributes = getConfig(viewlet, preference)
},
{ limit: 1 }
)
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
let attributes = getConfig(viewlet, preference)
interface AttributeConfig {
enabled: boolean
label: IntlString
value: string | BuildModelKey
}
function getObjectConfig (_class: Ref<Class<Doc>>, param: string): AttributeConfig {
const clazz = client.getHierarchy().getClass(_class)
return {
value: param,
label: clazz.label,
enabled: true
}
}
function getBaseConfig (viewlet: Viewlet): AttributeConfig[] {
const lookup = buildConfigLookup(hierarchy, viewlet.attachTo, viewlet.config)
const result: AttributeConfig[] = []
for (const param of viewlet.config) {
if (typeof param === 'string') {
if (param.length === 0) {
result.push(getObjectConfig(viewlet.attachTo, param))
} else {
result.push({
value: param,
enabled: true,
label: getKeyLabel(viewlet.attachTo, param, lookup)
})
}
} else {
result.push({
value: param,
label: param.label as IntlString,
enabled: true
})
}
}
return result
}
function getValue (name: string, type: Type<any>): string {
if (hierarchy.isDerived(type._class, core.class.RefTo)) {
return '$lookup.' + name
}
if (hierarchy.isDerived(type._class, core.class.ArrOf)) {
return getValue(name, (type as ArrOf<any>).of)
}
return name
}
function getConfig (viewlet: Viewlet, preference: ViewletPreference | undefined): AttributeConfig[] {
const result = getBaseConfig(viewlet)
const allAttributes = hierarchy.getAllAttributes(viewlet.attachTo)
for (const [, attribute] of allAttributes) {
const value = getValue(attribute.name, attribute.type)
if (result.findIndex((p) => p.value === value) !== -1) continue
if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) continue
if (attribute.hidden !== true && attribute.label !== undefined) {
const typeClassId = getAttributePresenterClass(attribute)
const typeClass = hierarchy.getClass(typeClassId)
let presenter = hierarchy.as(typeClass, view.mixin.AttributePresenter).presenter
let parent = typeClass.extends
while (presenter === undefined && parent !== undefined) {
const pclazz = hierarchy.getClass(parent)
presenter = hierarchy.as(pclazz, view.mixin.AttributePresenter).presenter
parent = pclazz.extends
}
if (presenter === undefined) continue
result.push({
value,
label: attribute.label,
enabled: false
})
}
}
return preference === undefined ? result : setStatus(result, preference)
}
async function save (): Promise<void> {
const config = attributes.filter((p) => p.enabled).map((p) => p.value)
if (preference !== undefined) {
await client.update(preference, {
config
})
} else {
await client.createDoc(view.class.ViewletPreference, preferencePlugin.space.Preference, {
attachedTo: viewlet._id,
config
})
}
}
function getKeyLabel<T extends Doc> (_class: Ref<Class<T>>, key: string, lookup: Lookup<T> | undefined): IntlString {
if (key.startsWith('$lookup') && lookup !== undefined) {
const lookupClass = getLookupClass(key, lookup, _class)
const lookupProperty = getLookupProperty(key)
const lookupKey = { key: lookupProperty[0] }
return getLookupLabel(client, lookupClass[1], lookupClass[0], lookupKey, lookupProperty[1])
} else {
const attribute = hierarchy.getAttribute(_class, key)
return attribute.label
}
}
function setStatus (result: AttributeConfig[], preference: ViewletPreference): AttributeConfig[] {
for (const key of result) {
key.enabled = preference.config.findIndex((p) => deepEqual(p, key.value)) !== -1
}
return result
}
</script>
<Card
label={view.string.CustomizeView}
okAction={save}
okLabel={presentation.string.Save}
canSave={true}
on:close={() => {
dispatch('close')
}}
>
<Grid column={3}>
{#each attributes as attribute}
<MiniToggle label={attribute.label} bind:on={attribute.enabled} />
{/each}
</Grid>
</Card>

View File

@ -32,7 +32,6 @@ import NumberPresenter from './components/NumberPresenter.svelte'
import ObjectPresenter from './components/ObjectPresenter.svelte'
import RolePresenter from './components/RolePresenter.svelte'
import Table from './components/Table.svelte'
import TableView from './components/TableView.svelte'
import TimestampPresenter from './components/TimestampPresenter.svelte'
import UpDownNavigator from './components/UpDownNavigator.svelte'
import GithubPresenter from './components/linkPresenters/GithubPresenter.svelte'
@ -45,6 +44,8 @@ import DateTypeEditor from './components/typeEditors/DateTypeEditor.svelte'
import NumberTypeEditor from './components/typeEditors/NumberTypeEditor.svelte'
import RefEditor from './components/typeEditors/RefEditor.svelte'
import DocAttributeBar from './components/DocAttributeBar.svelte'
import ViewletSetting from './components/ViewletSetting.svelte'
import TableBrowser from './components/TableBrowser.svelte'
export { getActions } from './actions'
export { default as ActionContext } from './components/ActionContext.svelte'
@ -58,19 +59,21 @@ export { buildModel, getCollectionCounter, getObjectPresenter, LoadingProps } fr
export {
HTMLPresenter,
Table,
TableView,
DateEditor,
DocAttributeBar,
EditDoc,
ColorsPopup,
Menu,
SpacePresenter,
UpDownNavigator
UpDownNavigator,
ViewletSetting
}
export default async (): Promise<Resources> => ({
actionImpl: actionImpl,
component: {
TableBrowser,
ViewletSetting,
CreateAttribute,
StringTypeEditor,
BooleanTypeEditor,
@ -84,7 +87,6 @@ export default async (): Promise<Resources> => ({
NumberPresenter,
BooleanPresenter,
BooleanEditor,
TableView,
TimestampPresenter,
DateEditor,
DatePresenter,

View File

@ -14,7 +14,19 @@
// limitations under the License.
//
import core, { AttachedDoc, Class, Client, Doc, Hierarchy, Lookup, Obj, Ref, TxOperations } from '@anticrm/core'
import core, {
AttachedDoc,
Class,
Client,
Collection,
Doc,
Hierarchy,
Lookup,
Obj,
Ref,
RefTo,
TxOperations
} from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { getResource } from '@anticrm/platform'
import { getAttributePresenterClass, KeyedAttribute } from '@anticrm/presentation'
@ -142,6 +154,52 @@ async function getPresenter<T extends Doc> (
}
}
function getKeyLookup<T extends Doc> (
hierarchy: Hierarchy,
_class: Ref<Class<T>>,
key: string,
lookup: Lookup<T>,
lastIndex: number = 1
): Lookup<T> {
if (!key.startsWith('$lookup')) return lookup
const parts = key.split('.')
const attrib = parts[1]
const attribute = hierarchy.getAttribute(_class, attrib)
if (hierarchy.isDerived(attribute.type._class, core.class.RefTo)) {
const lookupClass = (attribute.type as RefTo<Doc>).to
const index = key.indexOf('$lookup', lastIndex)
if (index === -1) {
;(lookup as any)[attrib] = lookupClass
} else {
let nested = Array.isArray((lookup as any)[attrib]) ? (lookup as any)[attrib][1] : {}
nested = getKeyLookup(hierarchy, lookupClass, key.slice(index), nested)
;(lookup as any)[attrib] = [lookupClass, nested]
}
} else if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) {
if ((lookup as any)._id === undefined) {
;(lookup as any)._id = {}
}
;(lookup as any)._id[attrib] = (attribute.type as Collection<AttachedDoc>).of
}
return lookup
}
export function buildConfigLookup<T extends Doc> (
hierarchy: Hierarchy,
_class: Ref<Class<T>>,
config: Array<BuildModelKey | string>
): Lookup<T> {
let res: Lookup<T> = {}
for (const key of config) {
if (typeof key === 'string') {
res = getKeyLookup(hierarchy, _class, key, res)
} else {
res = getKeyLookup(hierarchy, _class, key.key, res)
}
}
return res
}
export async function buildModel (options: BuildModelOptions): Promise<AttributeModel[]> {
console.log('building table model for', options)
// eslint-disable-next-line array-callback-return
@ -149,7 +207,7 @@ export async function buildModel (options: BuildModelOptions): Promise<Attribute
.map((key) => (typeof key === 'string' ? { key: key } : key))
.map(async (key) => {
try {
return await getPresenter(options.client, options._class, key, key, options.options?.lookup)
return await getPresenter(options.client, options._class, key, key, options.lookup)
} catch (err: any) {
if (options.ignoreMissing ?? false) {
return undefined
@ -205,7 +263,7 @@ async function getLookupPresenter<T extends Doc> (
return model
}
function getLookupLabel<T extends Doc> (
export function getLookupLabel<T extends Doc> (
client: Client,
_class: Ref<Class<T>>,
lookupClass: Ref<Class<Doc>>,
@ -226,7 +284,7 @@ function getLookupLabel<T extends Doc> (
}
}
function getLookupClass<T extends Doc> (
export function getLookupClass<T extends Doc> (
key: string,
lookup: Lookup<T>,
parent: Ref<Class<T>>
@ -238,7 +296,7 @@ function getLookupClass<T extends Doc> (
return _class
}
function getLookupProperty (key: string): [string, string] {
export function getLookupProperty (key: string): [string, string] {
const parts = key.split('$lookup')
const lastPart = parts[parts.length - 1]
const split = lastPart.split('.').filter((p) => p.length > 0)

View File

@ -28,6 +28,7 @@
"dependencies": {
"@anticrm/platform": "~0.6.6",
"@anticrm/core": "~0.6.16",
"@anticrm/ui": "~0.6.0"
"@anticrm/ui": "~0.6.0",
"@anticrm/preference": "~0.6.0"
}
}

View File

@ -21,6 +21,7 @@ import type {
Doc,
DocumentQuery,
FindOptions,
Lookup,
Mixin,
Obj,
Ref,
@ -31,6 +32,7 @@ import type { Asset, IntlString, Plugin, Resource, Status } from '@anticrm/platf
import { plugin } from '@anticrm/platform'
import type { AnyComponent, AnySvelteComponent } from '@anticrm/ui'
import { PopupPosAlignment } from '@anticrm/ui/src/types'
import type { Preference } from '@anticrm/preference'
/**
* @public
@ -102,7 +104,7 @@ export interface Viewlet extends Doc {
attachTo: Ref<Class<Space>>
descriptor: Ref<ViewletDescriptor>
options?: FindOptions<Doc>
config: any
config: (BuildModelKey | string)[]
}
/**
@ -273,7 +275,7 @@ export interface BuildModelOptions {
client: Client
_class: Ref<Class<Obj>>
keys: (BuildModelKey | string)[]
options?: FindOptions<Doc>
lookup?: Lookup<Doc>
ignoreMissing?: boolean
}
@ -287,6 +289,14 @@ export interface ObjectFactory extends Class<Obj> {
component: AnyComponent
}
/**
* @public
*/
export interface ViewletPreference extends Preference {
attachedTo: Ref<Viewlet>
config: (BuildModelKey | string)[]
}
/**
* @public
*/
@ -307,6 +317,7 @@ const view = plugin(viewId, {
PreviewPresenter: '' as Ref<Mixin<PreviewPresenter>>
},
class: {
ViewletPreference: '' as Ref<Class<ViewletPreference>>,
ViewletDescriptor: '' as Ref<Class<ViewletDescriptor>>,
Viewlet: '' as Ref<Class<Viewlet>>,
Action: '' as Ref<Class<Action>>,
@ -320,8 +331,12 @@ const view = plugin(viewId, {
ObjectPresenter: '' as AnyComponent,
EditDoc: '' as AnyComponent,
CreateAttribute: '' as AnyComponent,
ViewletSetting: '' as AnyComponent,
SpacePresenter: '' as AnyComponent
},
string: {
CustomizeView: '' as IntlString
},
icon: {
Table: '' as Asset,
Card: '' as Asset,
@ -330,6 +345,7 @@ const view = plugin(viewId, {
Move: '' as Asset,
Archive: '' as Asset,
Statuses: '' as Asset,
Setting: '' as Asset,
Open: '' as Asset,
ArrowRight: '' as Asset
},

View File

@ -33,11 +33,6 @@
<TableBrowser
_class={core.class.Space}
config={['', '$lookup._class.label', 'modifiedOn']}
options={{
lookup: {
_class: core.class.Class
}
}}
showNotification
baseMenuClass={core.class.Space}
query={{

View File

@ -15,28 +15,51 @@
-->
<script lang="ts">
import type { Class, Doc, Ref, Space, WithLookup } from '@anticrm/core'
import { Component } from '@anticrm/ui'
import type { Viewlet } from '@anticrm/view'
import { createQuery } from '@anticrm/presentation'
import { Component, Loading } from '@anticrm/ui'
import view, { Viewlet, ViewletPreference } from '@anticrm/view'
export let _class: Ref<Class<Doc>>
export let space: Ref<Space>
export let search: string
export let viewlet: WithLookup<Viewlet> | undefined
const preferenceQuery = createQuery()
let preference: ViewletPreference | undefined
let loading = true
$: viewlet &&
preferenceQuery.query(
view.class.ViewletPreference,
{
attachedTo: viewlet._id
},
(res) => {
preference = res[0]
loading = false
},
{ limit: 1 }
)
</script>
{#if viewlet}
{#key space}
{#if viewlet.$lookup?.descriptor?.component}
<Component
is={viewlet.$lookup?.descriptor?.component}
props={{
_class,
space,
options: viewlet.options,
config: viewlet.config,
search
}}
/>
{#if loading}
<Loading />
{:else}
<Component
is={viewlet.$lookup?.descriptor?.component}
props={{
_class,
space,
options: viewlet.options,
config: preference?.config ?? viewlet.config,
viewlet,
search
}}
/>
{/if}
{/if}
{/key}
{/if}

View File

@ -17,9 +17,19 @@
import core, { WithLookup } from '@anticrm/core'
import { IntlString } from '@anticrm/platform'
import presentation, { createQuery, getClient } from '@anticrm/presentation'
import { AnyComponent, showPanel } from '@anticrm/ui'
import { Button, Icon, SearchEdit, showPopup, Tooltip, IconAdd } from '@anticrm/ui'
import {
ActionIcon,
AnyComponent,
showPanel,
Button,
Icon,
SearchEdit,
showPopup,
Tooltip,
IconAdd
} from '@anticrm/ui'
import view, { Viewlet } from '@anticrm/view'
import { ViewletSetting } from '@anticrm/view-resources'
import { createEventDispatcher } from 'svelte'
import plugin from '../plugin'
import { classIcon } from '../utils'
@ -101,6 +111,17 @@
{/each}
</div>
{/if}
{#if viewlet}
<ActionIcon
icon={view.icon.Setting}
direction={'top'}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet })
}}
/>
{/if}
<SearchEdit
bind:value={search}
on:change={() => {