mirror of
https://github.com/hcengineering/platform.git
synced 2026-10-01 14:05:05 +02:00
Relationship table (#10329)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
"0.7.320"
|
||||
"0.7.324"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import core, {
|
||||
Association,
|
||||
AssociationQuery,
|
||||
BulkUpdateEvent,
|
||||
Class,
|
||||
Client,
|
||||
@@ -1116,28 +1117,44 @@ export class LiveQuery implements WithTx, Client {
|
||||
if (q.options?.associations === undefined) return
|
||||
if (doc._class !== core.class.Relation) return
|
||||
const relation = doc as Relation
|
||||
const assoc = q.options.associations.find((p) => p[0] === relation.association)
|
||||
const assoc = findAssociation(q.options.associations, relation.association)
|
||||
if (assoc !== undefined) {
|
||||
if (q.result instanceof Promise) {
|
||||
q.result = await q.result
|
||||
}
|
||||
const direct = assoc[1] === 1
|
||||
const res = q.result.findDoc(direct ? relation.docA : relation.docB)
|
||||
const res = findRelationDoc(q.result.getDocs(), q.options.associations, relation)
|
||||
if (res === undefined) return
|
||||
const association = this.getModel().findObject(assoc[0])
|
||||
if (association === undefined) return
|
||||
const docToPush = await this.findOne(direct ? association.classB : association.classA, {
|
||||
_id: direct ? relation.docB : relation.docA
|
||||
})
|
||||
const nestedAssoc = assoc[2]
|
||||
const options: FindOptions<Doc> | undefined =
|
||||
nestedAssoc !== undefined
|
||||
? {
|
||||
associations: nestedAssoc
|
||||
}
|
||||
: undefined
|
||||
const docToPush = await this.findOne(
|
||||
direct ? association.classB : association.classA,
|
||||
{
|
||||
_id: res.targetId
|
||||
},
|
||||
options
|
||||
)
|
||||
if (docToPush === undefined) return
|
||||
if (res?.$associations === undefined) {
|
||||
res.$associations = {}
|
||||
if (res.doc.$associations === undefined) {
|
||||
res.doc.$associations = {}
|
||||
}
|
||||
const key = direct ? 'b' : 'a'
|
||||
const arr = res?.$associations?.[`${relation.association}_${key}`] ?? []
|
||||
arr.push(docToPush)
|
||||
res.$associations[`${relation.association}_${key}`] = arr
|
||||
q.result.updateDoc(res, false)
|
||||
const arr = res.doc.$associations?.[`${assoc[0]}_${key}`] ?? []
|
||||
const exists = arr.findIndex((p) => p._id === docToPush._id)
|
||||
if (exists !== -1) {
|
||||
arr[exists] = docToPush
|
||||
} else {
|
||||
arr.push(docToPush)
|
||||
}
|
||||
res.doc.$associations[`${assoc[0]}_${key}`] = arr
|
||||
q.result.updateDoc(res.doc, false)
|
||||
this.queriesToUpdate.set(q.id, q)
|
||||
}
|
||||
}
|
||||
@@ -1611,3 +1628,64 @@ function getNestedLookup (lookup: Ref<Class<Doc>> | [Ref<Class<Doc>>, Lookup<Doc
|
||||
function getLookupClass (lookup: Ref<Class<Doc>> | [Ref<Class<Doc>>, Lookup<Doc>]): Ref<Class<Doc>> {
|
||||
return Array.isArray(lookup) ? lookup[0] : lookup
|
||||
}
|
||||
|
||||
function findAssociation (
|
||||
associations: AssociationQuery[],
|
||||
association: Ref<Association>
|
||||
): AssociationQuery | undefined {
|
||||
for (const assoc of associations) {
|
||||
if (assoc[0] === association) return assoc
|
||||
}
|
||||
for (const assoc of associations) {
|
||||
if (assoc[2] !== undefined) {
|
||||
const res = findAssociation(assoc[2], association)
|
||||
if (res !== undefined) {
|
||||
return assoc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findRelationDoc (
|
||||
result: WithLookup<Doc>[],
|
||||
associations: AssociationQuery[] | undefined,
|
||||
relation: Relation
|
||||
):
|
||||
| {
|
||||
doc: WithLookup<Doc>
|
||||
targetId: Ref<Doc>
|
||||
}
|
||||
| undefined {
|
||||
if (associations === undefined) return
|
||||
for (const assoc of associations) {
|
||||
if (assoc[0] === relation.association) {
|
||||
const sourceId = assoc[1] === 1 ? relation.docA : relation.docB
|
||||
const targetId = assoc[1] === 1 ? relation.docB : relation.docA
|
||||
for (const doc of result) {
|
||||
if (doc._id === sourceId) {
|
||||
return {
|
||||
doc,
|
||||
targetId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const assoc of associations) {
|
||||
if (assoc[2] !== undefined) {
|
||||
const key = assoc[1] === 1 ? 'b' : 'a'
|
||||
for (const doc of result) {
|
||||
const arr = doc.$associations?.[`${assoc[0]}_${key}`] ?? []
|
||||
if (arr.length > 0) {
|
||||
const res = findRelationDoc(arr, assoc[2], relation)
|
||||
if (res !== undefined) {
|
||||
return {
|
||||
doc,
|
||||
targetId: res.doc._id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,6 +632,21 @@ export function createModel (builder: Builder): void {
|
||||
card.viewlet.CardFeed
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: card.class.Card,
|
||||
descriptor: view.viewlet.RelationshipTable,
|
||||
configOptions: {
|
||||
hiddenKeys: ['content', 'title'],
|
||||
sortable: true
|
||||
},
|
||||
config: ['']
|
||||
},
|
||||
card.viewlet.CardRelationshipTable
|
||||
)
|
||||
|
||||
builder.mixin(card.class.Card, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: card.component.CardPresenter
|
||||
})
|
||||
|
||||
@@ -63,7 +63,7 @@ export const cardOperation: MigrateOperation = {
|
||||
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>, mode): Promise<void> {
|
||||
await tryUpgrade(mode, state, client, cardId, [
|
||||
{
|
||||
state: 'migrateViewlets-v5',
|
||||
state: 'migrateViewlets-v6',
|
||||
func: migrateViewlets
|
||||
},
|
||||
{
|
||||
|
||||
@@ -598,6 +598,17 @@ export function createModel (builder: Builder): void {
|
||||
view.viewlet.Table
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.ViewletDescriptor,
|
||||
core.space.Model,
|
||||
{
|
||||
label: view.string.RelationshipTable,
|
||||
icon: view.icon.Table2,
|
||||
component: view.component.RelationshipTableBrowser
|
||||
},
|
||||
view.viewlet.RelationshipTable
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.ViewletDescriptor,
|
||||
core.space.Model,
|
||||
|
||||
@@ -68,6 +68,7 @@ export default mergeIds(viewId, view, {
|
||||
TimestampPresenter: '' as AnyComponent,
|
||||
DateTimePresenter: '' as AnyComponent,
|
||||
TableBrowser: '' as AnyComponent,
|
||||
RelationshipTableBrowser: '' as AnyComponent,
|
||||
YoutubePresenter: '' as AnyComponent,
|
||||
GithubPresenter: '' as AnyComponent,
|
||||
ClassPresenter: '' as AnyComponent,
|
||||
@@ -101,6 +102,7 @@ export default mergeIds(viewId, view, {
|
||||
},
|
||||
string: {
|
||||
Table: '' as IntlString,
|
||||
RelationshipTable: '' as IntlString,
|
||||
Role: '' as IntlString,
|
||||
// Keybaord actions
|
||||
MoveUp: '' as IntlString,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { ObjectPresenterType } from '@hcengineering/view'
|
||||
import CardPresenter from './CardPresenter.svelte'
|
||||
|
||||
export let value: Card[]
|
||||
export let value: Card[] | Card | undefined
|
||||
export let disabled: boolean = false
|
||||
export let noUnderline: boolean = disabled
|
||||
export let colorInherit: boolean = false
|
||||
@@ -28,11 +28,13 @@
|
||||
export let showParent: boolean = false
|
||||
export let type: ObjectPresenterType = 'link'
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
|
||||
$: arr = Array.isArray(value) ? value : [value]
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="flex-col-center">
|
||||
{#each value as card}
|
||||
<div class="flex-col">
|
||||
{#each arr as card}
|
||||
<CardPresenter value={card} {noUnderline} {noSelect} {colorInherit} {inline} {showParent} {type} {icon} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
|
||||
let title: string
|
||||
let descriptor: Ref<ViewletDescriptor> | undefined = undefined
|
||||
const association: Association | undefined = undefined
|
||||
let viewConfigs: MasterDetailConfig[] = []
|
||||
$: viewConfigs = [
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
view.viewlet.Tree,
|
||||
view.viewlet.MasterDetail,
|
||||
card.viewlet.CardFeedDescriptor,
|
||||
view.viewlet.RelationshipTable,
|
||||
view.viewlet.Document
|
||||
]
|
||||
: [
|
||||
@@ -36,7 +37,8 @@
|
||||
view.viewlet.List,
|
||||
view.viewlet.Tree,
|
||||
view.viewlet.MasterDetail,
|
||||
card.viewlet.CardFeedDescriptor
|
||||
card.viewlet.CardFeedDescriptor,
|
||||
view.viewlet.RelationshipTable
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -87,7 +87,8 @@ export default mergeIds(cardId, card, {
|
||||
CardList: '' as Ref<Viewlet>,
|
||||
CardChildList: '' as Ref<Viewlet>,
|
||||
CardFeedDescriptor: '' as Ref<ViewletDescriptor>,
|
||||
CardFeed: '' as Ref<Viewlet>
|
||||
CardFeed: '' as Ref<Viewlet>,
|
||||
CardRelationshipTable: '' as Ref<Viewlet>
|
||||
},
|
||||
string: {
|
||||
CreateMasterTag: '' as IntlString,
|
||||
|
||||
@@ -142,14 +142,14 @@ export async function duplicateCard (origin: Card): Promise<void> {
|
||||
}
|
||||
|
||||
for (const rel of relationsA) {
|
||||
await ops.createDoc(core.class.Relation, origin.space, {
|
||||
await ops.createDoc(core.class.Relation, core.space.Workspace, {
|
||||
docA: targetId,
|
||||
docB: rel.docB,
|
||||
association: rel.association
|
||||
})
|
||||
}
|
||||
for (const rel of relationsB) {
|
||||
await ops.createDoc(core.class.Relation, origin.space, {
|
||||
await ops.createDoc(core.class.Relation, core.space.Workspace, {
|
||||
docA: rel.docA,
|
||||
docB: targetId,
|
||||
association: rel.association
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"ValueIsSet": "Hodnota je nastavena",
|
||||
"RoleLabel": "Role: {role}",
|
||||
"ForbidAttributeChanges": "Zakázat změny: ",
|
||||
"AllowAttributeChanges": "Povolit změny: "
|
||||
"AllowAttributeChanges": "Povolit změny: ",
|
||||
"RelationshipTable": "Tabulka vztahů"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"ValueIsSet": "Wert ist gesetzt",
|
||||
"RoleLabel": "Rolle: {role}",
|
||||
"ForbidAttributeChanges": "Änderungen verbieten: ",
|
||||
"AllowAttributeChanges": "Änderungen erlauben: "
|
||||
"AllowAttributeChanges": "Änderungen erlauben: ",
|
||||
"RelationshipTable": "Beziehungstabelle"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"ValueIsSet": "Value is set",
|
||||
"RoleLabel": "Role: {role}",
|
||||
"ForbidAttributeChanges": "Forbid changes: ",
|
||||
"AllowAttributeChanges": "Allow changes: "
|
||||
"AllowAttributeChanges": "Allow changes: ",
|
||||
"RelationshipTable": "Relationship table"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
"ValueIsSet": "El valor está establecido",
|
||||
"RoleLabel": "Role: {role}",
|
||||
"ForbidAttributeChanges": "Prohibir cambios: ",
|
||||
"AllowAttributeChanges": "Permitir cambios: "
|
||||
"AllowAttributeChanges": "Permitir cambios: ",
|
||||
"RelationshipTable": "Tabla de relaciones"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
"ValueIsSet": "La valeur est définie",
|
||||
"RoleLabel": "Rôle : {role}",
|
||||
"ForbidAttributeChanges": "Interdire les modifications: ",
|
||||
"AllowAttributeChanges": "Autoriser les modifications: "
|
||||
"AllowAttributeChanges": "Autoriser les modifications: ",
|
||||
"RelationshipTable": "Table des relations"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
"ValueIsSet": "Il valore è impostato",
|
||||
"RoleLabel": "Ruolo: {role}",
|
||||
"ForbidAttributeChanges": "Vieta modifiche: ",
|
||||
"AllowAttributeChanges": "Consenti modifiche: "
|
||||
"AllowAttributeChanges": "Consenti modifiche: ",
|
||||
"RelationshipTable": "Tabella delle relazioni"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
"ValueIsSet": "値が設定されています",
|
||||
"RoleLabel": "役割: {role}",
|
||||
"ForbidAttributeChanges": "属性の変更を禁止: ",
|
||||
"AllowAttributeChanges": "属性の変更を許可: "
|
||||
"AllowAttributeChanges": "属性の変更を許可: ",
|
||||
"RelationshipTable": "関係テーブル"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
"ValueIsSet": "O valor está definido",
|
||||
"RoleLabel": "Cargo: {role}",
|
||||
"ForbidAttributeChanges": "Proibir alterações: ",
|
||||
"AllowAttributeChanges": "Permitir alterações: "
|
||||
"AllowAttributeChanges": "Permitir alterações: ",
|
||||
"RelationshipTable": "Tabela de relacionamentos"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
"ValueIsSet": "Значение установлено",
|
||||
"RoleLabel": "Роль: {role}",
|
||||
"ForbidAttributeChanges": "Запретить изменение: ",
|
||||
"AllowAttributeChanges": "Разрешить изменение: "
|
||||
"AllowAttributeChanges": "Разрешить изменение: ",
|
||||
"RelationshipTable": "Таблица связей"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
"ValueIsSet": "Değer ayarlandı",
|
||||
"RoleLabel": "Rol: {role}",
|
||||
"ForbidAttributeChanges": "Özellik yasakla: ",
|
||||
"AllowAttributeChanges": "Özellik izin ver: "
|
||||
"AllowAttributeChanges": "Özellik izin ver: ",
|
||||
"RelationshipTable": "İlişki tablosu"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"ValueIsSet": "值已设置",
|
||||
"RoleLabel": "角色:{role}",
|
||||
"ForbidAttributeChanges": "禁止更改属性: ",
|
||||
"AllowAttributeChanges": "允许更改属性: "
|
||||
"AllowAttributeChanges": "允许更改属性: ",
|
||||
"RelationshipTable": "关系表"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<!--
|
||||
// Copyright © 2025 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 core, { Association, Doc, Ref, Relation } from '@hcengineering/core'
|
||||
import { createQuery, getClient, ObjectPopup } from '@hcengineering/presentation'
|
||||
import ObjectPresenter from './ObjectPresenter.svelte'
|
||||
|
||||
export let association: string
|
||||
export let target: Doc
|
||||
|
||||
const parts = association.split('_')
|
||||
const associationId = parts[0] as Ref<Association>
|
||||
const direction = parts[1]
|
||||
|
||||
let relations: Relation[] = []
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const assoc = client.getModel().getObject(associationId)
|
||||
|
||||
const multiSelect = assoc.type === 'N:N' || (assoc.type === '1:N' && direction === 'b')
|
||||
|
||||
const targetClass = direction === 'a' ? assoc.classA : assoc.classB
|
||||
|
||||
$: selectedObjects = relations.map((p) => (direction === 'a' ? p.docA : p.docB))
|
||||
|
||||
const query = createQuery()
|
||||
query.query(
|
||||
core.class.Relation,
|
||||
{
|
||||
association: associationId,
|
||||
[direction === 'a' ? 'docB' : 'docA']: target._id
|
||||
},
|
||||
(res) => {
|
||||
relations = res
|
||||
}
|
||||
)
|
||||
|
||||
async function handler (e: CustomEvent<any>): Promise<void> {
|
||||
if (e.detail != null) {
|
||||
const currentSet = new Set(selectedObjects)
|
||||
const newSet = new Set(Array.isArray(e.detail) ? e.detail : [e.detail])
|
||||
for (const _id of newSet) {
|
||||
if (!currentSet.has(_id)) {
|
||||
await client.createDoc(core.class.Relation, core.space.Workspace, {
|
||||
association: associationId,
|
||||
docA: direction === 'a' ? _id : target._id,
|
||||
docB: direction === 'b' ? _id : target._id
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const _id of currentSet) {
|
||||
if (!newSet.has(_id)) {
|
||||
const relation = relations.find((p) => p[direction === 'b' ? 'docB' : 'docA'] === _id)
|
||||
if (relation !== undefined) {
|
||||
await client.remove(relation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ObjectPopup _class={targetClass} {multiSelect} {selectedObjects} on:close={handler} on:update={handler}>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
<ObjectPresenter value={item} props={{ type: 'text' }} />
|
||||
</svelte:fragment>
|
||||
</ObjectPopup>
|
||||
@@ -0,0 +1,720 @@
|
||||
<!--
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { PermissionsStore } from '@hcengineering/contact'
|
||||
import core, {
|
||||
AnyAttribute,
|
||||
AssociationQuery,
|
||||
Class,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
FindOptions,
|
||||
Lookup,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
TxOperations,
|
||||
TypedSpace,
|
||||
WithLookup,
|
||||
getObjectValue,
|
||||
mergeQueries
|
||||
} from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient, reduceCalls, updateAttribute } from '@hcengineering/presentation'
|
||||
import ui, {
|
||||
Button,
|
||||
Label,
|
||||
Loading,
|
||||
eventToHTMLElement,
|
||||
lazyObserver,
|
||||
resizeObserver,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { AttributeModel, BuildModelKey, BuildModelOptions, ViewOptionModel, ViewOptions } from '@hcengineering/view'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { Readable } from 'svelte/store'
|
||||
import { showMenu } from '../actions'
|
||||
import { canChangeAttribute } from '../permissions'
|
||||
import view from '../plugin'
|
||||
import { buildConfigAssociation, buildConfigLookup, buildModel, restrictionStore } from '../utils'
|
||||
import { getResultOptions, getResultQuery } from '../viewOptions'
|
||||
import IconUpDown from './icons/UpDown.svelte'
|
||||
import RelationsSelectorPopup from './RelationsSelectorPopup.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let query: DocumentQuery<Doc>
|
||||
export let highlightRows: boolean = false
|
||||
export let hiddenHeader: boolean = false
|
||||
export let options: FindOptions<Doc> | undefined = undefined
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let config: Array<BuildModelKey | string>
|
||||
export let tableId: string | undefined = undefined
|
||||
export let readonly = false
|
||||
export let showFooter = false
|
||||
export let viewOptionsConfig: ViewOptionModel[] | undefined = undefined
|
||||
export let viewOptions: ViewOptions | undefined = undefined
|
||||
|
||||
export let totalQuery: DocumentQuery<Doc> | undefined = undefined
|
||||
|
||||
export let prefferedSorting: string = 'modifiedOn'
|
||||
|
||||
export let limit = 200
|
||||
|
||||
const assoc = '$associations'
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
let lookup = buildConfigLookup(hierarchy, _class, config, options?.lookup)
|
||||
let associations = buildConfigAssociation(config)
|
||||
|
||||
$: lookup = buildConfigLookup(hierarchy, _class, config, options?.lookup)
|
||||
$: associations = buildConfigAssociation(config)
|
||||
|
||||
let _sortKey = prefferedSorting
|
||||
let userSorting = false
|
||||
$: if (!userSorting) {
|
||||
_sortKey = prefferedSorting
|
||||
}
|
||||
|
||||
let sortOrder = SortingOrder.Descending
|
||||
let loading = 0
|
||||
|
||||
let objects: Doc[] = []
|
||||
let gtotal: number = 0
|
||||
let total: number = 0
|
||||
let objectsRecieved = false
|
||||
|
||||
let rowLimit = 1
|
||||
|
||||
const oldClass = _class
|
||||
$: if (oldClass !== _class) {
|
||||
rowLimit = 1 // delayed show
|
||||
}
|
||||
|
||||
const q = createQuery()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: sortingFunction = (config.find((it) => typeof it !== 'string' && it.sortingKey === _sortKey) as BuildModelKey)
|
||||
?.sortingFunction
|
||||
|
||||
function getSort (sortKey: string | string[]) {
|
||||
return Array.isArray(sortKey)
|
||||
? sortKey.reduce((acc: Record<string, SortingOrder>, val) => {
|
||||
acc[val] = sortOrder
|
||||
return acc
|
||||
}, {})
|
||||
: { ...(options?.sort ?? {}), [sortKey]: sortOrder }
|
||||
}
|
||||
|
||||
let resultOptions = options
|
||||
|
||||
const update = reduceCalls(async function (
|
||||
_class: Ref<Class<Doc>>,
|
||||
query: DocumentQuery<Doc>,
|
||||
sortKey: string | string[],
|
||||
sortOrder: SortingOrder,
|
||||
lookup: Lookup<Doc>,
|
||||
associations: AssociationQuery[] | undefined,
|
||||
limit: number,
|
||||
options: FindOptions<Doc> | undefined
|
||||
) {
|
||||
const p = await getResultQuery(hierarchy, query, viewOptionsConfig, viewOptions)
|
||||
const resultQuery = mergeQueries(p, query)
|
||||
loading += q.query(
|
||||
_class,
|
||||
resultQuery,
|
||||
(result) => {
|
||||
if (sortingFunction !== undefined) {
|
||||
const sf = sortingFunction
|
||||
objects = result.sort((a, b) => -1 * sortOrder * sf(a, b))
|
||||
} else {
|
||||
objects = result
|
||||
}
|
||||
objectsRecieved = true
|
||||
loading = 0
|
||||
},
|
||||
{ limit, ...options, sort: getSort(sortKey), lookup, associations, total: false }
|
||||
)
|
||||
? 1
|
||||
: 0
|
||||
})
|
||||
$: void update(_class, query, _sortKey, sortOrder, lookup, associations, limit, resultOptions)
|
||||
|
||||
$: void getResultOptions(options, viewOptionsConfig, viewOptions).then((p) => {
|
||||
resultOptions = p
|
||||
})
|
||||
|
||||
$: dispatch('content', objects)
|
||||
|
||||
const qSlow = createQuery()
|
||||
$: qSlow.query(
|
||||
_class,
|
||||
query,
|
||||
(result) => {
|
||||
total = result.total
|
||||
if (totalQuery === undefined) {
|
||||
gtotal = total
|
||||
}
|
||||
},
|
||||
{ limit: 1, ...resultOptions, sort: getSort(_sortKey), lookup, associations, total: true }
|
||||
)
|
||||
|
||||
const totalQueryQ = createQuery()
|
||||
$: if (totalQuery !== undefined) {
|
||||
totalQueryQ.query(
|
||||
_class,
|
||||
totalQuery,
|
||||
(result) => {
|
||||
gtotal = result.total === -1 ? 0 : result.total
|
||||
},
|
||||
{
|
||||
...resultOptions,
|
||||
lookup,
|
||||
limit: 1,
|
||||
total: true
|
||||
}
|
||||
)
|
||||
} else {
|
||||
gtotal = total
|
||||
}
|
||||
|
||||
const showContextMenu = async (ev: MouseEvent, object: Doc, row: number): Promise<void> => {
|
||||
showMenu(ev, { object, baseMenuClass })
|
||||
}
|
||||
|
||||
function changeSorting (key: string | string[]): void {
|
||||
if (key === '') {
|
||||
return
|
||||
}
|
||||
userSorting = true
|
||||
if (key !== _sortKey) {
|
||||
_sortKey = Array.isArray(key) ? key[0] : key
|
||||
sortOrder = SortingOrder.Ascending
|
||||
} else {
|
||||
sortOrder = sortOrder === SortingOrder.Ascending ? SortingOrder.Descending : SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
|
||||
const joinProps = (attribute: AttributeModel, object: Doc, readonly: boolean) => {
|
||||
const readonlyParams =
|
||||
readonly || (attribute?.attribute?.readonly ?? false)
|
||||
? {
|
||||
readonly: true,
|
||||
editable: false,
|
||||
disabled: true
|
||||
}
|
||||
: {
|
||||
readonly: false,
|
||||
editable: true
|
||||
}
|
||||
if (attribute.collectionAttr || attribute.attribute?.type?._class === core.class.TypeIdentifier) {
|
||||
return { object, ...attribute.props, ...readonlyParams }
|
||||
}
|
||||
if (attribute.attribute?.type._class === core.class.EnumOf) {
|
||||
return { ...attribute.props, type: attribute.attribute.type, ...readonlyParams }
|
||||
}
|
||||
return { ...attribute.props, space: object.space, ...readonlyParams }
|
||||
}
|
||||
function getValue (attribute: AttributeModel, object: Doc): any {
|
||||
if (attribute.castRequest) {
|
||||
return getObjectValue(
|
||||
attribute.key.substring(attribute.castRequest.length + 1),
|
||||
client.getHierarchy().as(object, attribute.castRequest)
|
||||
)
|
||||
}
|
||||
if (attribute.key.startsWith(assoc)) {
|
||||
return object
|
||||
}
|
||||
return getObjectValue(attribute.key, object)
|
||||
}
|
||||
|
||||
function onChange (value: any, doc: Doc, key: string, attribute: AnyAttribute): void {
|
||||
updateAttribute(client, doc, _class, { key, attr: attribute }, value)
|
||||
}
|
||||
|
||||
function getOnChange (doc: Doc, attribute: AttributeModel) {
|
||||
const attr = attribute.attribute
|
||||
if (attr === undefined) return
|
||||
if (attribute.collectionAttr) return
|
||||
if (attribute.isLookup) return
|
||||
if (attribute?.attribute?.readonly === true) return
|
||||
const key = attribute.castRequest ? attribute.key.substring(attribute.castRequest.length + 1) : attribute.key
|
||||
return (value: any) => {
|
||||
onChange(value, doc, key, attr)
|
||||
}
|
||||
}
|
||||
|
||||
let width: number
|
||||
|
||||
let isBuildingModel = true
|
||||
let model: AttributeModel[] | undefined
|
||||
let modelOptions: BuildModelOptions | undefined
|
||||
|
||||
const updateModelOptions = reduceCalls(async function updateModelOptions (
|
||||
client: TxOperations,
|
||||
_class: Ref<Class<Doc>>,
|
||||
config: Array<string | BuildModelKey>,
|
||||
lookup?: Lookup<Doc>
|
||||
): Promise<void> {
|
||||
const newModelOpts = { client, _class, keys: config, lookup }
|
||||
if (modelOptions == null || !deepEqual(modelOptions, newModelOpts)) {
|
||||
modelOptions = newModelOpts
|
||||
await build(modelOptions)
|
||||
}
|
||||
})
|
||||
$: void updateModelOptions(client, _class, config, lookup)
|
||||
|
||||
async function build (modelOptions: BuildModelOptions): Promise<void> {
|
||||
isBuildingModel = true
|
||||
const res = await buildModel(modelOptions)
|
||||
model = res
|
||||
isBuildingModel = false
|
||||
}
|
||||
|
||||
function contextHandler (object: Doc, row: number): (ev: MouseEvent) => void {
|
||||
return (ev) => {
|
||||
if (!readonly) {
|
||||
void showContextMenu(ev, object, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let permissionsStore: Readable<PermissionsStore> | undefined = undefined
|
||||
|
||||
onMount(async () => {
|
||||
permissionsStore = await getResource(contact.store.Permissions)
|
||||
})
|
||||
|
||||
function canChangeAttr (
|
||||
object: Doc,
|
||||
attr: AnyAttribute | undefined,
|
||||
permissionsStore: PermissionsStore | undefined
|
||||
): boolean {
|
||||
if (permissionsStore === undefined) return true
|
||||
if (attr === undefined) return true
|
||||
return canChangeAttribute(attr, object.space as Ref<TypedSpace>, permissionsStore, object._class)
|
||||
}
|
||||
|
||||
interface CellModel {
|
||||
attribute: AttributeModel
|
||||
rowSpan: number
|
||||
object: Doc | undefined
|
||||
parentObject: Doc | undefined
|
||||
}
|
||||
|
||||
interface RowModel {
|
||||
cells: CellModel[]
|
||||
}
|
||||
|
||||
interface WalkResult {
|
||||
rows: RowModel[]
|
||||
rowCount: number
|
||||
}
|
||||
|
||||
function getView (objects: Doc[], model: AttributeModel[] | undefined): RowModel[] {
|
||||
if (model === undefined) return []
|
||||
const res: RowModel[] = []
|
||||
for (const obj of objects) {
|
||||
res.push(...walk(obj, model, undefined).rows)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
$: viewModel = getView(objects, model)
|
||||
|
||||
function getOwnAttributes (model: AttributeModel[], associationId?: string): AttributeModel[] {
|
||||
return model.filter((attr) => {
|
||||
if (associationId) return attr.key.startsWith(associationId) && !attr.key.startsWith(`${associationId}.${assoc}`)
|
||||
return !attr.key.startsWith(assoc)
|
||||
})
|
||||
}
|
||||
|
||||
function createLeafResult (doc: Doc | undefined, attrs: AttributeModel[], parent: Doc | undefined): WalkResult {
|
||||
return {
|
||||
rowCount: 1,
|
||||
rows: [
|
||||
{
|
||||
cells: attrs.map((a) => ({
|
||||
attribute: a,
|
||||
rowSpan: 1,
|
||||
object: doc,
|
||||
parentObject: parent
|
||||
}))
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function processBranches (
|
||||
doc: Doc | undefined,
|
||||
model: AttributeModel[],
|
||||
associations: string[],
|
||||
associationId?: string
|
||||
): Map<string, WalkResult[]> {
|
||||
const branchesByRelation = new Map<string, WalkResult[]>()
|
||||
|
||||
for (const relKey of associations) {
|
||||
const relationBranches: WalkResult[] = []
|
||||
|
||||
if (doc !== undefined) {
|
||||
const key = associationId ? relKey.substring(associationId.length + 1) : relKey
|
||||
const relDocs = getObjectValue(key, doc)
|
||||
if (relDocs !== undefined && relDocs.length > 0) {
|
||||
for (const child of relDocs) {
|
||||
relationBranches.push(walk(child as WithLookup<Doc>, model, relKey, doc))
|
||||
}
|
||||
} else {
|
||||
relationBranches.push(walk(undefined, model, relKey, doc))
|
||||
}
|
||||
} else {
|
||||
relationBranches.push(walk(undefined, model, relKey, doc))
|
||||
}
|
||||
|
||||
branchesByRelation.set(relKey, relationBranches)
|
||||
}
|
||||
|
||||
return branchesByRelation
|
||||
}
|
||||
|
||||
function distributeExtraRows (totalRows: number, existingRows: number): number[] {
|
||||
const diff = totalRows - existingRows
|
||||
const extraPerRow = new Array(existingRows).fill(0)
|
||||
|
||||
if (diff <= 0) return extraPerRow
|
||||
|
||||
const base = Math.floor(diff / existingRows)
|
||||
let remainder = diff % existingRows
|
||||
|
||||
for (let i = 0; i < existingRows; i++) {
|
||||
extraPerRow[i] = base + (remainder > 0 ? 1 : 0)
|
||||
if (remainder > 0) remainder--
|
||||
}
|
||||
|
||||
return extraPerRow
|
||||
}
|
||||
|
||||
function mergeRows (
|
||||
doc: Doc | undefined,
|
||||
ownAttrs: AttributeModel[],
|
||||
branchesByRelation: Map<string, WalkResult[]>,
|
||||
parentObject: Doc | undefined
|
||||
): WalkResult {
|
||||
let maxRows = 0
|
||||
const relationRowCounts = new Map<string, number>()
|
||||
const relationExtraRows = new Map<string, number[]>()
|
||||
|
||||
for (const [relKey, branches] of branchesByRelation) {
|
||||
const count = branches.reduce((s, b) => s + b.rowCount, 0)
|
||||
relationRowCounts.set(relKey, count)
|
||||
maxRows = Math.max(maxRows, count)
|
||||
}
|
||||
|
||||
for (const [relKey, branches] of branchesByRelation) {
|
||||
const count = relationRowCounts.get(relKey) || 0
|
||||
const extraRows = distributeExtraRows(maxRows, count)
|
||||
relationExtraRows.set(relKey, extraRows)
|
||||
}
|
||||
|
||||
const rows: RowModel[] = []
|
||||
|
||||
for (let rowIdx = 0; rowIdx < maxRows; rowIdx++) {
|
||||
const cellMap = new Map<string, CellModel>()
|
||||
|
||||
if (rowIdx === 0) {
|
||||
for (const attr of ownAttrs) {
|
||||
cellMap.set(attr.key, {
|
||||
attribute: attr,
|
||||
rowSpan: maxRows,
|
||||
object: doc,
|
||||
parentObject
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const [relKey, branches] of branchesByRelation) {
|
||||
const allBranchRows = branches.flatMap((b) => b.rows)
|
||||
const extraRows = relationExtraRows.get(relKey) || []
|
||||
|
||||
let accumulatedExtra = 0
|
||||
let branchRowIdx = -1
|
||||
|
||||
for (let i = 0; i < allBranchRows.length; i++) {
|
||||
const rowSpan = 1 + (extraRows[i] || 0)
|
||||
if (rowIdx >= accumulatedExtra && rowIdx < accumulatedExtra + rowSpan) {
|
||||
branchRowIdx = i
|
||||
break
|
||||
}
|
||||
accumulatedExtra += rowSpan
|
||||
}
|
||||
|
||||
if (branchRowIdx >= 0) {
|
||||
const branchRow = allBranchRows[branchRowIdx]
|
||||
|
||||
let correctAccumulated = 0
|
||||
for (let i = 0; i < branchRowIdx; i++) {
|
||||
correctAccumulated += 1 + (extraRows[i] || 0)
|
||||
}
|
||||
const isFirst = rowIdx === correctAccumulated
|
||||
|
||||
if (isFirst) {
|
||||
for (const cell of branchRow.cells) {
|
||||
const additionalSpan = extraRows[branchRowIdx] || 0
|
||||
cellMap.set(cell.attribute.key, {
|
||||
...cell,
|
||||
rowSpan: cell.rowSpan + additionalSpan
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows.push({
|
||||
cells: Array.from(cellMap.values())
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
rowCount: maxRows,
|
||||
rows
|
||||
}
|
||||
}
|
||||
|
||||
function getAssociations (model: AttributeModel[], associationId?: string): string[] {
|
||||
return model
|
||||
.filter((p) => {
|
||||
if (associationId) {
|
||||
return (
|
||||
p.key.startsWith(`${associationId}.${assoc}`) &&
|
||||
p.key.split(assoc).filter((p) => p.length).length ===
|
||||
associationId.split(assoc).filter((p) => p.length).length + 1
|
||||
)
|
||||
} else {
|
||||
return p.key.startsWith(assoc) && p.key.split(assoc).filter((p) => p.length).length === 1
|
||||
}
|
||||
})
|
||||
.map((p) => p.key)
|
||||
}
|
||||
|
||||
function walk (
|
||||
doc: WithLookup<Doc> | undefined,
|
||||
model: AttributeModel[],
|
||||
associationId?: string,
|
||||
parent?: Doc
|
||||
): WalkResult {
|
||||
const associations = getAssociations(model, associationId)
|
||||
const ownAttrs = getOwnAttributes(model, associationId)
|
||||
|
||||
if (associations.length === 0) {
|
||||
return createLeafResult(doc, ownAttrs, parent)
|
||||
}
|
||||
|
||||
const branchesByRelation = processBranches(doc, model, associations, associationId)
|
||||
|
||||
if (branchesByRelation.size === 0) {
|
||||
return createLeafResult(doc, ownAttrs, parent)
|
||||
}
|
||||
|
||||
const res = mergeRows(doc, ownAttrs, branchesByRelation, parent)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
function clickHandler (e: MouseEvent, cell: CellModel): void {
|
||||
if (cell.parentObject === undefined) return
|
||||
const parts = cell.attribute.key.split('$associations.')
|
||||
const association = parts.pop()
|
||||
showPopup(
|
||||
RelationsSelectorPopup,
|
||||
{
|
||||
association,
|
||||
target: cell.parentObject
|
||||
},
|
||||
eventToHTMLElement(e)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !model || isBuildingModel}
|
||||
<Loading />
|
||||
{:else}
|
||||
<table
|
||||
id={tableId}
|
||||
use:resizeObserver={(element) => {
|
||||
width = element.clientWidth
|
||||
}}
|
||||
class="antiTable"
|
||||
class:highlightRows
|
||||
>
|
||||
{#if !hiddenHeader}
|
||||
<thead class="scroller-thead">
|
||||
<tr class="scroller-thead__tr">
|
||||
{#each model as attribute, i}
|
||||
<th
|
||||
class:sortable={attribute.sortingKey}
|
||||
class:sorted={attribute.sortingKey === _sortKey}
|
||||
class:align-left={attribute.displayProps?.align === 'left'}
|
||||
class:align-center={attribute.displayProps?.align === 'center'}
|
||||
class:align-right={attribute.displayProps?.align === 'right'}
|
||||
class:first={i === 0}
|
||||
on:click={() => {
|
||||
changeSorting(attribute.sortingKey)
|
||||
}}
|
||||
>
|
||||
<div class="antiTable-cells">
|
||||
{#if attribute.label}
|
||||
<Label label={attribute.label} />
|
||||
{/if}
|
||||
{#if attribute.sortingKey === _sortKey}
|
||||
<div class="icon">
|
||||
<IconUpDown size={'small'} descending={sortOrder === SortingOrder.Descending} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
{#if viewModel.length > 0 || objectsRecieved}
|
||||
<tbody>
|
||||
{#each viewModel as r, row}
|
||||
<tr
|
||||
class="antiTable-body__row"
|
||||
use:lazyObserver={(val) => {
|
||||
if (val && row >= rowLimit) {
|
||||
rowLimit = row + 10
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if row < rowLimit}
|
||||
{#each r.cells as cell, i}
|
||||
<td
|
||||
class:align-left={cell.attribute.displayProps?.align === 'left'}
|
||||
class:align-center={cell.attribute.displayProps?.align === 'center'}
|
||||
class:align-right={cell.attribute.displayProps?.align === 'right'}
|
||||
rowspan={cell.rowSpan}
|
||||
class:empty={cell.object === undefined}
|
||||
class:first={i === 0}
|
||||
class:cursor-pointer={cell.parentObject}
|
||||
on:click={(e) => {
|
||||
clickHandler(e, cell)
|
||||
}}
|
||||
>
|
||||
{#if cell.object}
|
||||
<svelte:component
|
||||
this={cell.attribute.presenter}
|
||||
value={getValue(cell.attribute, cell.object)}
|
||||
onChange={getOnChange(cell.object, cell.attribute)}
|
||||
label={cell.attribute.label}
|
||||
attribute={cell.attribute.attribute}
|
||||
{...joinProps(
|
||||
cell.attribute,
|
||||
cell.object,
|
||||
readonly ||
|
||||
$restrictionStore.readonly ||
|
||||
!canChangeAttr(cell.object, cell.attribute.attribute, $permissionsStore)
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
{/if}
|
||||
</table>
|
||||
{#if loading > 0}<Loading />{/if}
|
||||
{/if}
|
||||
{#if showFooter}
|
||||
<div class="space" />
|
||||
<div class="footer" style="width: {width}px;">
|
||||
<div class="content">
|
||||
<span class="select-text">
|
||||
<Label label={view.string.Total} params={{ total: gtotal }} />
|
||||
</span>
|
||||
|
||||
{#if objects.length > 0 && (total !== gtotal || objects.length < total)}
|
||||
<span class="select-text ml-2">
|
||||
<Label
|
||||
label={view.string.Shown}
|
||||
params={{
|
||||
total: objects.length === total || total === gtotal ? -1 : total,
|
||||
len: objects.length
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if objects.length > 0 && objects.length < total}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<Button
|
||||
label={ui.string.ShowMore}
|
||||
kind={'ghost'}
|
||||
size={'small'}
|
||||
on:click={() => {
|
||||
limit = limit + 100
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.space {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-right: 1px solid var(--theme-trans-color);
|
||||
|
||||
&.first {
|
||||
border-left: 1px solid var(--theme-trans-color);
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
background-color: var(--theme-link-button-hover);
|
||||
}
|
||||
|
||||
.antiTable-body__row {
|
||||
border-bottom: 1px solid var(--theme-trans-color);
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
background-color: var(--theme-comp-header-color);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 2.5rem;
|
||||
z-index: 2;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: max-content;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<!--
|
||||
// Copyright © 2025 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, Ref } from '@hcengineering/core'
|
||||
import { ActionContext } from '@hcengineering/presentation'
|
||||
import { Scroller, tableSP } from '@hcengineering/ui'
|
||||
import { BuildModelKey, Viewlet, ViewOptionModel, ViewOptions } from '@hcengineering/view'
|
||||
import { onMount } from 'svelte'
|
||||
import RelationshipTable from './RelationshipTable.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let query: DocumentQuery<Doc>
|
||||
export let totalQuery: DocumentQuery<Doc> | undefined = undefined
|
||||
export let options: FindOptions<Doc> | undefined = undefined
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let config: Array<BuildModelKey | string>
|
||||
export let viewOptions: ViewOptions | undefined = undefined
|
||||
export let viewOptionsConfig: ViewOptionModel[] | undefined = undefined
|
||||
export let viewlet: Viewlet | undefined = undefined
|
||||
export let readonly = false
|
||||
|
||||
onMount(() => {
|
||||
;(document.activeElement as HTMLElement)?.blur()
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window />
|
||||
|
||||
<ActionContext
|
||||
context={{
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
|
||||
<Scroller fade={tableSP} horizontal>
|
||||
<RelationshipTable
|
||||
{_class}
|
||||
{config}
|
||||
{options}
|
||||
{query}
|
||||
{totalQuery}
|
||||
{baseMenuClass}
|
||||
highlightRows
|
||||
showFooter
|
||||
{viewOptions}
|
||||
viewOptionsConfig={viewOptionsConfig ?? viewlet?.viewOptions?.other}
|
||||
{readonly}
|
||||
/>
|
||||
</Scroller>
|
||||
@@ -116,6 +116,7 @@ import ImageViewer from './components/viewer/ImageViewer.svelte'
|
||||
import PDFViewer from './components/viewer/PDFViewer.svelte'
|
||||
import TextViewer from './components/viewer/TextViewer.svelte'
|
||||
import VideoViewer from './components/viewer/VideoViewer.svelte'
|
||||
import RelationshipTableBrowser from './components/RelationshipTableBrowser.svelte'
|
||||
|
||||
import { blobImageMetadata, blobVideoMetadata } from './blob'
|
||||
|
||||
@@ -341,7 +342,8 @@ export default async (): Promise<Resources> => ({
|
||||
TreeView,
|
||||
MasterDetailBrowser,
|
||||
ReadOnlyNotification,
|
||||
ForbiddenNotification
|
||||
ForbiddenNotification,
|
||||
RelationshipTableBrowser
|
||||
},
|
||||
popup: {
|
||||
PositionElementAlignment
|
||||
|
||||
@@ -164,7 +164,8 @@ const view = plugin(viewId, {
|
||||
List: '' as Ref<ViewletDescriptor>,
|
||||
MasterDetail: '' as Ref<ViewletDescriptor>,
|
||||
Tree: '' as Ref<ViewletDescriptor>,
|
||||
Document: '' as Ref<ViewletDescriptor>
|
||||
Document: '' as Ref<ViewletDescriptor>,
|
||||
RelationshipTable: '' as Ref<ViewletDescriptor>
|
||||
},
|
||||
component: {
|
||||
ActionsPopup: '' as AnyComponent,
|
||||
|
||||
Reference in New Issue
Block a user