Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 2883f3e

Browse files
Show relations (#10133)
1 parent 654e0f8 commit 2883f3e

File tree

7 files changed

+169
-11
lines changed

7 files changed

+169
-11
lines changed

‎models/card/src/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,10 @@ export function createModel (builder: Builder): void {
585585
presenter: card.component.CardPresenter
586586
})
587587

588+
builder.mixin(card.class.Card, core.class.Class, view.mixin.CollectionPresenter, {
589+
presenter: card.component.CardsPresenter
590+
})
591+
588592
builder.mixin(card.class.FavoriteCard, core.class.Class, view.mixin.ObjectPresenter, {
589593
presenter: card.component.FavoriteCardPresenter
590594
})
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!--
2+
// Copyright © 2025 Hardcore Engineering Inc.
3+
//
4+
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License. You may
6+
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
//
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
-->
15+
<script lang="ts">
16+
import { Card } from '@hcengineering/card'
17+
import { Asset } from '@hcengineering/platform'
18+
import { AnySvelteComponent } from '@hcengineering/ui'
19+
import { ObjectPresenterType } from '@hcengineering/view'
20+
import CardPresenter from './CardPresenter.svelte'
21+
22+
export let value: Card[]
23+
export let disabled: boolean = false
24+
export let noUnderline: boolean = disabled
25+
export let colorInherit: boolean = false
26+
export let noSelect: boolean = true
27+
export let inline = false
28+
export let showParent: boolean = false
29+
export let type: ObjectPresenterType = 'link'
30+
export let icon: Asset | AnySvelteComponent | undefined = undefined
31+
</script>
32+
33+
{#if value}
34+
{#each value as card}
35+
<CardPresenter value={card} {noUnderline} {noSelect} {colorInherit} {inline} {showParent} {type} {icon} />
36+
{/each}
37+
{/if}

‎plugins/card-resources/src/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { type Resources } from '@hcengineering/platform'
1515
import MasterTags from './components/MasterTags.svelte'
1616
import CreateTag from './components/CreateTag.svelte'
1717
import CardPresenter from './components/CardPresenter.svelte'
18+
import CardsPresenter from './components/CardsPresenter.svelte'
1819
import EditCard from './components/EditCard.svelte'
1920
import Main from './components/Main.svelte'
2021
import {
@@ -108,6 +109,7 @@ export default async (): Promise<Resources> => ({
108109
EditView,
109110
CardEditor,
110111
CardRefPresenter,
112+
CardsPresenter,
111113
ChangeType,
112114
CreateCardButton,
113115
CardArrayEditor,

‎plugins/card-resources/src/plugin.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default mergeIds(cardId, card, {
2828
MasterTags: '' as AnyComponent,
2929
CreateTag: '' as AnyComponent,
3030
CardPresenter: '' as AnyComponent,
31+
CardsPresenter: '' as AnyComponent,
3132
FavoriteCardPresenter: '' as AnyComponent,
3233
EditCard: '' as AnyComponent,
3334
Main: '' as AnyComponent,

‎plugins/view-resources/src/components/Table.svelte‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<script lang="ts">
1717
import core, {
1818
AnyAttribute,
19+
AssociationQuery,
1920
Class,
2021
Doc,
2122
DocumentQuery,
@@ -45,7 +46,7 @@
4546
import { createEventDispatcher } from 'svelte'
4647
import { showMenu } from '../actions'
4748
import view from '../plugin'
48-
import { LoadingProps, buildConfigLookup, buildModel, restrictionStore } from '../utils'
49+
import { LoadingProps, buildConfigAssociation, buildConfigLookup, buildModel, restrictionStore } from '../utils'
4950
import IconUpDown from './icons/UpDown.svelte'
5051
import { getResultOptions, getResultQuery } from '../viewOptions'
5152
import { canEditSpace } from '../visibilityTester'
@@ -80,7 +81,11 @@
8081
const client = getClient()
8182
const hierarchy = client.getHierarchy()
8283
84+
let lookup = buildConfigLookup(hierarchy, _class, config, options?.lookup)
85+
let associations = buildConfigAssociation(config)
86+
8387
$: lookup = buildConfigLookup(hierarchy, _class, config, options?.lookup)
88+
$: associations = buildConfigAssociation(config)
8489
8590
let _sortKey = prefferedSorting
8691
let userSorting = false
@@ -130,6 +135,7 @@
130135
sortKey: string | string[],
131136
sortOrder: SortingOrder,
132137
lookup: Lookup<Doc>,
138+
associations: AssociationQuery[] | undefined,
133139
limit: number,
134140
options: FindOptions<Doc> | undefined
135141
) {
@@ -148,12 +154,12 @@
148154
objectsRecieved = true
149155
loading = 0
150156
},
151-
{ limit, ...options, sort: getSort(sortKey), lookup, total: false }
157+
{ limit, ...options, sort: getSort(sortKey), lookup, associations, total: false }
152158
)
153159
? 1
154160
: 0
155161
})
156-
$: void update(_class, query, _sortKey, sortOrder, lookup, limit, resultOptions)
162+
$: void update(_class, query, _sortKey, sortOrder, lookup, associations, limit, resultOptions)
157163
158164
$: void getResultOptions(options, viewOptionsConfig, viewOptions).then((p) => {
159165
resultOptions = p
@@ -171,7 +177,7 @@
171177
gtotal = total
172178
}
173179
},
174-
{ limit: 1, ...resultOptions, sort: getSort(_sortKey), lookup, total: true }
180+
{ limit: 1, ...resultOptions, sort: getSort(_sortKey), lookup, associations, total: true }
175181
)
176182
177183
const totalQueryQ = createQuery()

‎plugins/view-resources/src/components/ViewletSetting.svelte‎

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@
1313
// limitations under the License.
1414
-->
1515
<script lang="ts">
16-
import core, { AnyAttribute, Class, Doc, Ref, Type } from '@hcengineering/core'
17-
import { Asset, IntlString } from '@hcengineering/platform'
16+
import core, { AnyAttribute, Association, Class, Doc, Ref, Type } from '@hcengineering/core'
17+
import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform'
1818
import { createQuery, getAttributePresenterClass, getClient, hasResource } from '@hcengineering/presentation'
19-
import { Loading, resizeObserver } from '@hcengineering/ui'
20-
import DropdownLabelsIntl from '@hcengineering/ui/src/components/DropdownLabelsIntl.svelte'
19+
import { DropdownLabelsIntl, Loading, resizeObserver } from '@hcengineering/ui'
2120
import { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
2221
import { deepEqual } from 'fast-equals'
2322
import { createEventDispatcher } from 'svelte'
@@ -235,6 +234,33 @@
235234
return false
236235
}
237236
237+
function processAssociation (association: Association, direction: 'A' | 'B', result: Config[]): void {
238+
const associationValue = `$associations.${association._id}.${direction}`
239+
const name = direction === 'A' ? association.nameA : association.nameB
240+
const targetClass = direction === 'A' ? association.classA : association.classB
241+
242+
if (name.trim().length === 0) return
243+
244+
for (const res of result) {
245+
const key = typeof res.value === 'string' ? res.value : res.value?.key
246+
if (key === associationValue) return
247+
}
248+
249+
const clazz = hierarchy.getClass(targetClass)
250+
const newValue: AttributeConfig = {
251+
type: 'attribute',
252+
value: associationValue,
253+
label: getEmbeddedLabel(name),
254+
enabled: false,
255+
_class: targetClass,
256+
icon: clazz.icon
257+
}
258+
259+
if (!isExist(result, newValue)) {
260+
result.push(newValue)
261+
}
262+
}
263+
238264
function getConfig (viewlet: Viewlet, preference: ViewletPreference | undefined): Config[] {
239265
const result = getBaseConfig(viewlet)
240266
@@ -264,6 +290,20 @@
264290
processAttribute(attr, result, true)
265291
})
266292
})
293+
294+
// Process associations
295+
296+
const allClasses = [...ancestors, ...parentMixins.map((it) => it._id)]
297+
298+
const associationsB = client.getModel().findAllSync(core.class.Association, { classA: { $in: allClasses } })
299+
const associationsA = client.getModel().findAllSync(core.class.Association, { classB: { $in: allClasses } })
300+
301+
associationsB.forEach((a) => {
302+
processAssociation(a, 'B', result)
303+
})
304+
associationsA.forEach((a) => {
305+
processAssociation(a, 'A', result)
306+
})
267307
}
268308
269309
return preference === undefined ? result : setStatus(result, preference)

‎plugins/view-resources/src/utils.ts‎

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import { Analytics } from '@hcengineering/analytics'
1818
import core, {
1919
AccountRole,
20-
type ApplyOperations,
2120
ClassifierKind,
2221
DocManager,
2322
Hierarchy,
@@ -27,6 +26,9 @@ import core, {
2726
getObjectValue,
2827
type AggregateValue,
2928
type AnyAttribute,
29+
type ApplyOperations,
30+
type Association,
31+
type AssociationQuery,
3032
type AttachedDoc,
3133
type CategoryType,
3234
type Class,
@@ -58,7 +60,7 @@ import core, {
5860
} from '@hcengineering/core'
5961
import { type Restrictions } from '@hcengineering/guest'
6062
import type { Asset, IntlString } from '@hcengineering/platform'
61-
import { getMetadata, getResource, translate } from '@hcengineering/platform'
63+
import { getEmbeddedLabel,getMetadata, getResource, translate } from '@hcengineering/platform'
6264
import presentation, {
6365
createQuery,
6466
getAttributePresenterClass,
@@ -505,6 +507,20 @@ function getKeyLookup<T extends Doc> (
505507
return lookup
506508
}
507509

510+
export function buildConfigAssociation (config: Array<BuildModelKey | string>): AssociationQuery[] | undefined {
511+
const res: AssociationQuery[] = []
512+
for (const key of config) {
513+
const k = typeof key === 'string' ? key : key.key
514+
if (k.startsWith('$associations')) {
515+
const parts = k.split('.')
516+
if (parts.length === 3) {
517+
res.push([parts[1] as Ref<Association>, parts[2] === 'A' ? -1 : 1])
518+
}
519+
}
520+
}
521+
return res.length > 0 ? res : undefined
522+
}
523+
508524
export function buildConfigLookup<T extends Doc> (
509525
hierarchy: Hierarchy,
510526
_class: Ref<Class<T>>,
@@ -542,6 +558,9 @@ export async function buildModel (options: BuildModelOptions): Promise<Attribute
542558
// Check if it is a mixin attribute configuration
543559
const pos = key.key.lastIndexOf('.')
544560
if (pos !== -1) {
561+
if (key.key.startsWith('$associations')) {
562+
return await getRelationPresenter(options.client, key)
563+
}
545564
const mixinName = key.key.substring(0, pos) as Ref<Class<Doc>>
546565
if (!mixinName.includes('$lookup')) {
547566
const realKey = key.key.substring(pos + 1)
@@ -578,6 +597,43 @@ export async function buildModel (options: BuildModelOptions): Promise<Attribute
578597
return (await Promise.all(model)).filter((a) => a !== undefined)
579598
}
580599

600+
async function getRelationPresenter (client: Client, key: BuildModelKey): Promise<AttributeModel> {
601+
const parts = key.key.split('.')
602+
if (parts.length !== 3) {
603+
throw new Error('invalid relation key ' + key.key)
604+
}
605+
const assocId = parts[1] as Ref<Association>
606+
const assoc = client.getModel().findObject(assocId)
607+
if (assoc === undefined) {
608+
throw new Error('association not found for ' + assocId)
609+
}
610+
const _class = parts[2] === 'A' ? assoc.classA : assoc.classB
611+
const name = parts[2] === 'A' ? assoc.nameA : assoc.nameB
612+
613+
const hierarchy = client.getHierarchy()
614+
615+
const presenterMixin = hierarchy.classHierarchyMixin(_class, view.mixin.CollectionPresenter)
616+
if (presenterMixin?.presenter === undefined) {
617+
console.error(
618+
`object presenter not found for class=${_class}, mixin=${view.mixin.ObjectPresenter}, preserve key ${JSON.stringify(key)}`
619+
)
620+
throw new Error('presenter not found for ' + _class)
621+
}
622+
const presenter = await getResource(presenterMixin.presenter)
623+
624+
return {
625+
key: `${parts[0]}.${parts[1]}`,
626+
sortingKey: '',
627+
_class,
628+
label: getEmbeddedLabel(name),
629+
presenter,
630+
props: key.props,
631+
displayProps: key.displayProps,
632+
collectionAttr: false,
633+
isLookup: true
634+
}
635+
}
636+
581637
export async function deleteObject (client: TxOperations, object: Doc): Promise<void> {
582638
const currentAcc = getCurrentAccount()
583639
const socialStrings = new Set(await getAllSocialStringsByPersonRef(client, getCurrentEmployee()))
@@ -1049,7 +1105,19 @@ export function getKeyLabel<T extends Doc> (
10491105
key: string,
10501106
lookup: Lookup<T> | undefined
10511107
): IntlString {
1052-
if (key.startsWith('$lookup') && lookup !== undefined) {
1108+
if (key.startsWith('$relation')) {
1109+
// Handle association: $relation.[associationId]
1110+
const parts = key.split('.')
1111+
if (parts.length === 3) {
1112+
const associationId = parts[1] as Ref<Association>
1113+
const association = client.getModel().findObject(associationId)
1114+
if (association !== undefined) {
1115+
const name = parts[2] === 'A' ? association.nameA : association.nameB
1116+
return getEmbeddedLabel(name)
1117+
}
1118+
}
1119+
return key as IntlString
1120+
} else if (key.startsWith('$lookup') && lookup !== undefined) {
10531121
const lookupClass = getLookupClass(key, lookup, _class)
10541122
const lookupProperty = getLookupProperty(key)
10551123
const lookupKey = { key: lookupProperty[0] }

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /