diff --git a/common/scripts/version.txt b/common/scripts/version.txt index 94d2e3c334..01e636d706 100644 --- a/common/scripts/version.txt +++ b/common/scripts/version.txt @@ -1 +1 @@ -"0.7.320" +"0.7.324" diff --git a/foundations/core/packages/query/src/index.ts b/foundations/core/packages/query/src/index.ts index 165c990f66..464d3870fe 100644 --- a/foundations/core/packages/query/src/index.ts +++ b/foundations/core/packages/query/src/index.ts @@ -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 | 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> | [Ref>, Lookup> | [Ref>, Lookup]): Ref> { return Array.isArray(lookup) ? lookup[0] : lookup } + +function findAssociation ( + associations: AssociationQuery[], + association: Ref +): 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[], + associations: AssociationQuery[] | undefined, + relation: Relation +): + | { + doc: WithLookup + targetId: Ref + } + | 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 + } + } + } + } + } + } +} diff --git a/models/card/src/index.ts b/models/card/src/index.ts index f590c18cf3..022bb013d9 100644 --- a/models/card/src/index.ts +++ b/models/card/src/index.ts @@ -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 }) diff --git a/models/card/src/migration.ts b/models/card/src/migration.ts index 52488014ee..d12001b347 100644 --- a/models/card/src/migration.ts +++ b/models/card/src/migration.ts @@ -63,7 +63,7 @@ export const cardOperation: MigrateOperation = { async upgrade (state: Map>, client: () => Promise, mode): Promise { await tryUpgrade(mode, state, client, cardId, [ { - state: 'migrateViewlets-v5', + state: 'migrateViewlets-v6', func: migrateViewlets }, { diff --git a/models/view/src/index.ts b/models/view/src/index.ts index 03e890e10a..1bda21002c 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -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, diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index 448c0c4bbe..b04f2fc597 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -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, diff --git a/plugins/card-resources/src/components/CardsPresenter.svelte b/plugins/card-resources/src/components/CardsPresenter.svelte index 75d5a46720..9cd18af6d2 100644 --- a/plugins/card-resources/src/components/CardsPresenter.svelte +++ b/plugins/card-resources/src/components/CardsPresenter.svelte @@ -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] {#if value} -
- {#each value as card} +
+ {#each arr as card} {/each}
diff --git a/plugins/card-resources/src/components/settings/view/CreateView.svelte b/plugins/card-resources/src/components/settings/view/CreateView.svelte index 9db48cbf68..6b9cbc18e4 100644 --- a/plugins/card-resources/src/components/settings/view/CreateView.svelte +++ b/plugins/card-resources/src/components/settings/view/CreateView.svelte @@ -32,7 +32,6 @@ let title: string let descriptor: Ref | undefined = undefined - const association: Association | undefined = undefined let viewConfigs: MasterDetailConfig[] = [] $: viewConfigs = [ { diff --git a/plugins/card-resources/src/components/settings/view/DescriptorBox.svelte b/plugins/card-resources/src/components/settings/view/DescriptorBox.svelte index 81b247c8a8..62026825a9 100644 --- a/plugins/card-resources/src/components/settings/view/DescriptorBox.svelte +++ b/plugins/card-resources/src/components/settings/view/DescriptorBox.svelte @@ -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 ] diff --git a/plugins/card-resources/src/plugin.ts b/plugins/card-resources/src/plugin.ts index edcb856cfc..acf5a8e67e 100644 --- a/plugins/card-resources/src/plugin.ts +++ b/plugins/card-resources/src/plugin.ts @@ -87,7 +87,8 @@ export default mergeIds(cardId, card, { CardList: '' as Ref, CardChildList: '' as Ref, CardFeedDescriptor: '' as Ref, - CardFeed: '' as Ref + CardFeed: '' as Ref, + CardRelationshipTable: '' as Ref }, string: { CreateMasterTag: '' as IntlString, diff --git a/plugins/card-resources/src/utils.ts b/plugins/card-resources/src/utils.ts index c06702874b..e105e73df2 100644 --- a/plugins/card-resources/src/utils.ts +++ b/plugins/card-resources/src/utils.ts @@ -142,14 +142,14 @@ export async function duplicateCard (origin: Card): Promise { } 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 diff --git a/plugins/view-assets/lang/cs.json b/plugins/view-assets/lang/cs.json index 79d1f36414..ab23cd9013 100644 --- a/plugins/view-assets/lang/cs.json +++ b/plugins/view-assets/lang/cs.json @@ -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ů" } } diff --git a/plugins/view-assets/lang/de.json b/plugins/view-assets/lang/de.json index 5696d664c5..da08ddd574 100644 --- a/plugins/view-assets/lang/de.json +++ b/plugins/view-assets/lang/de.json @@ -153,6 +153,7 @@ "ValueIsSet": "Wert ist gesetzt", "RoleLabel": "Rolle: {role}", "ForbidAttributeChanges": "Änderungen verbieten: ", - "AllowAttributeChanges": "Änderungen erlauben: " + "AllowAttributeChanges": "Änderungen erlauben: ", + "RelationshipTable": "Beziehungstabelle" } } diff --git a/plugins/view-assets/lang/en.json b/plugins/view-assets/lang/en.json index f0653a2588..6d4922cc59 100644 --- a/plugins/view-assets/lang/en.json +++ b/plugins/view-assets/lang/en.json @@ -153,6 +153,7 @@ "ValueIsSet": "Value is set", "RoleLabel": "Role: {role}", "ForbidAttributeChanges": "Forbid changes: ", - "AllowAttributeChanges": "Allow changes: " + "AllowAttributeChanges": "Allow changes: ", + "RelationshipTable": "Relationship table" } } diff --git a/plugins/view-assets/lang/es.json b/plugins/view-assets/lang/es.json index 0b6ae204fe..34f1b2a34f 100644 --- a/plugins/view-assets/lang/es.json +++ b/plugins/view-assets/lang/es.json @@ -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" } } diff --git a/plugins/view-assets/lang/fr.json b/plugins/view-assets/lang/fr.json index 682e6231e7..a9c8f6afa1 100644 --- a/plugins/view-assets/lang/fr.json +++ b/plugins/view-assets/lang/fr.json @@ -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" } } diff --git a/plugins/view-assets/lang/it.json b/plugins/view-assets/lang/it.json index 6d9833dc13..c51ac7dcd2 100644 --- a/plugins/view-assets/lang/it.json +++ b/plugins/view-assets/lang/it.json @@ -148,6 +148,7 @@ "ValueIsSet": "Il valore è impostato", "RoleLabel": "Ruolo: {role}", "ForbidAttributeChanges": "Vieta modifiche: ", - "AllowAttributeChanges": "Consenti modifiche: " + "AllowAttributeChanges": "Consenti modifiche: ", + "RelationshipTable": "Tabella delle relazioni" } } diff --git a/plugins/view-assets/lang/ja.json b/plugins/view-assets/lang/ja.json index fb26ba07d9..be6ad3e6e3 100644 --- a/plugins/view-assets/lang/ja.json +++ b/plugins/view-assets/lang/ja.json @@ -148,6 +148,7 @@ "ValueIsSet": "値が設定されています", "RoleLabel": "役割: {role}", "ForbidAttributeChanges": "属性の変更を禁止: ", - "AllowAttributeChanges": "属性の変更を許可: " + "AllowAttributeChanges": "属性の変更を許可: ", + "RelationshipTable": "関係テーブル" } } diff --git a/plugins/view-assets/lang/pt.json b/plugins/view-assets/lang/pt.json index d4164e4f59..70b1270b11 100644 --- a/plugins/view-assets/lang/pt.json +++ b/plugins/view-assets/lang/pt.json @@ -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" } } diff --git a/plugins/view-assets/lang/ru.json b/plugins/view-assets/lang/ru.json index 25e2b46c20..aedecd4e46 100644 --- a/plugins/view-assets/lang/ru.json +++ b/plugins/view-assets/lang/ru.json @@ -150,6 +150,7 @@ "ValueIsSet": "Значение установлено", "RoleLabel": "Роль: {role}", "ForbidAttributeChanges": "Запретить изменение: ", - "AllowAttributeChanges": "Разрешить изменение: " + "AllowAttributeChanges": "Разрешить изменение: ", + "RelationshipTable": "Таблица связей" } } diff --git a/plugins/view-assets/lang/tr.json b/plugins/view-assets/lang/tr.json index eafad8979f..093f99d52d 100644 --- a/plugins/view-assets/lang/tr.json +++ b/plugins/view-assets/lang/tr.json @@ -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" } } diff --git a/plugins/view-assets/lang/zh.json b/plugins/view-assets/lang/zh.json index 8685e44ee3..8b21ee05c0 100644 --- a/plugins/view-assets/lang/zh.json +++ b/plugins/view-assets/lang/zh.json @@ -153,6 +153,7 @@ "ValueIsSet": "值已设置", "RoleLabel": "角色:{role}", "ForbidAttributeChanges": "禁止更改属性: ", - "AllowAttributeChanges": "允许更改属性: " + "AllowAttributeChanges": "允许更改属性: ", + "RelationshipTable": "关系表" } } diff --git a/plugins/view-resources/src/components/RelationsSelectorPopup.svelte b/plugins/view-resources/src/components/RelationsSelectorPopup.svelte new file mode 100644 index 0000000000..61eabe2727 --- /dev/null +++ b/plugins/view-resources/src/components/RelationsSelectorPopup.svelte @@ -0,0 +1,80 @@ + + + + + + + + diff --git a/plugins/view-resources/src/components/RelationshipTable.svelte b/plugins/view-resources/src/components/RelationshipTable.svelte new file mode 100644 index 0000000000..bc79be74ef --- /dev/null +++ b/plugins/view-resources/src/components/RelationshipTable.svelte @@ -0,0 +1,720 @@ + + + +{#if !model || isBuildingModel} + +{:else} + { + width = element.clientWidth + }} + class="antiTable" + class:highlightRows + > + {#if !hiddenHeader} + + + {#each model as attribute, i} + + {/each} + + + {/if} + {#if viewModel.length > 0 || objectsRecieved} + + {#each viewModel as r, row} + { + if (val && row >= rowLimit) { + rowLimit = row + 10 + } + }} + > + {#if row < rowLimit} + {#each r.cells as cell, i} + + {/each} + {/if} + + {/each} + + {/if} +
{ + changeSorting(attribute.sortingKey) + }} + > +
+ {#if attribute.label} +
+
{ + clickHandler(e, cell) + }} + > + {#if cell.object} + + {/if} +
+ {#if loading > 0}{/if} +{/if} +{#if showFooter} +
+ +{/if} + + diff --git a/plugins/view-resources/src/components/RelationshipTableBrowser.svelte b/plugins/view-resources/src/components/RelationshipTableBrowser.svelte new file mode 100644 index 0000000000..01ff6cede2 --- /dev/null +++ b/plugins/view-resources/src/components/RelationshipTableBrowser.svelte @@ -0,0 +1,61 @@ + + + + + + + + + + diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index 012d4fdad3..3bfb63720d 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -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 => ({ TreeView, MasterDetailBrowser, ReadOnlyNotification, - ForbiddenNotification + ForbiddenNotification, + RelationshipTableBrowser }, popup: { PositionElementAlignment diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index 721ad5c471..25e2e02b77 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -164,7 +164,8 @@ const view = plugin(viewId, { List: '' as Ref, MasterDetail: '' as Ref, Tree: '' as Ref, - Document: '' as Ref + Document: '' as Ref, + RelationshipTable: '' as Ref }, component: { ActionsPopup: '' as AnyComponent,