mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
enable support for association-based attributes in markdown table (#10811)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
@@ -49,6 +49,7 @@ jest.mock('@hcengineering/view-resources', () => ({
|
||||
copyMarkdown: jest.fn(),
|
||||
buildModel: jest.fn(),
|
||||
buildConfigLookup: jest.fn(() => ({})),
|
||||
buildConfigAssociation: jest.fn(),
|
||||
getObjectLinkFragment: jest.fn()
|
||||
}))
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ jest.mock('@hcengineering/platform', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@hcengineering/presentation', () => ({
|
||||
getClient: jest.fn()
|
||||
}))
|
||||
|
||||
jest.mock('../data/personLoader', () => ({
|
||||
loadPersonName: jest.fn(async (personId: string) => personId)
|
||||
}))
|
||||
|
||||
@@ -188,10 +188,13 @@ export async function rebuildRelationshipTableViewModel (
|
||||
continue
|
||||
}
|
||||
|
||||
const span = rowSpanByLevel[0][rowIdx]
|
||||
const isFirstInSpan =
|
||||
rowIdx === 0 || expandedRows[rowIdx - 1].docsByLevel[0]?._id !== rowData.docsByLevel[0]?._id
|
||||
cells.push({
|
||||
attribute: attr,
|
||||
rowSpan: 1,
|
||||
object: rowIdx === 0 ? parentDoc : undefined,
|
||||
rowSpan: isFirstInSpan ? span : 0,
|
||||
object: isFirstInSpan ? parentDoc : undefined,
|
||||
parentObject: undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,9 +20,11 @@ import core, {
|
||||
type Hierarchy,
|
||||
type Ref,
|
||||
type PersonId,
|
||||
type Association,
|
||||
getDisplayTime,
|
||||
getObjectValue
|
||||
} from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { translate, type IntlString, getResource } from '@hcengineering/platform'
|
||||
import type { AttributeModel } from '@hcengineering/view'
|
||||
import converter from '@hcengineering/converter'
|
||||
@@ -188,6 +190,49 @@ function resolveDisplayContext (
|
||||
} else {
|
||||
value = undefined
|
||||
}
|
||||
} else if (attr.key.startsWith('$associations')) {
|
||||
const parts = attr.key.split('.')
|
||||
// Find the last association segment
|
||||
let lastAssocIndex = -1
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (parts[i] === '$associations' && i + 1 < parts.length) {
|
||||
lastAssocIndex = i
|
||||
}
|
||||
}
|
||||
if (lastAssocIndex !== -1) {
|
||||
const assocIdWithDirection = parts[lastAssocIndex + 1]
|
||||
const fragments = assocIdWithDirection.split('_')
|
||||
const assocId = fragments[0] as Ref<Association>
|
||||
const direction = fragments[1] === 'a' ? -1 : 1
|
||||
|
||||
const client = getClient()
|
||||
const assoc = client.getModel().findObject(assocId)
|
||||
if (assoc !== undefined) {
|
||||
const targetClass = direction === -1 ? assoc.classA : assoc.classB
|
||||
const subFieldParts = parts.slice(lastAssocIndex + 2)
|
||||
|
||||
const cardWithAssociations = card as any
|
||||
const assocData = cardWithAssociations.$associations?.[assocIdWithDirection]
|
||||
|
||||
if (assocData !== undefined) {
|
||||
const firstDoc = Array.isArray(assocData) ? assocData[0] : assocData
|
||||
if (firstDoc !== undefined) {
|
||||
if (subFieldParts.length > 0) {
|
||||
const subKey = subFieldParts.join('.')
|
||||
if (Array.isArray(assocData)) {
|
||||
value = assocData.map((d) => getObjectValue(subKey, d)).filter((v) => v !== undefined)
|
||||
} else {
|
||||
value = getObjectValue(subKey, assocData)
|
||||
}
|
||||
} else {
|
||||
value = assocData
|
||||
}
|
||||
displayDoc = firstDoc
|
||||
displayClass = targetClass
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value = getObjectValue(attr.key, card)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
BuildModelKey
|
||||
} from '@hcengineering/view'
|
||||
import viewPlugin from '@hcengineering/view'
|
||||
import { buildConfigLookup, buildModel, getAttributeValue } from '@hcengineering/view-resources'
|
||||
import { buildConfigLookup, buildModel, getAttributeValue, buildConfigAssociation } from '@hcengineering/view-resources'
|
||||
import type { CopyAsMarkdownTableProps, CopyRelationshipTableAsMarkdownProps } from '../types'
|
||||
import { formatValue } from '../formatter'
|
||||
import { generateHeaders, loadViewletConfig, buildTableModel } from '../model'
|
||||
@@ -99,6 +99,36 @@ async function preloadRefLookups (
|
||||
}
|
||||
}
|
||||
|
||||
async function preloadAssociations (docs: Doc[], model: AttributeModel[], client: Client): Promise<void> {
|
||||
const associationQueries = buildConfigAssociation(model.map((m) => m.key))
|
||||
if (associationQueries === undefined || associationQueries.length === 0) return
|
||||
|
||||
const ids = docs.map((d) => d._id)
|
||||
const firstDoc = docs[0]
|
||||
if (firstDoc === undefined) return
|
||||
|
||||
try {
|
||||
const refreshedDocs = await client.findAll(
|
||||
firstDoc._class,
|
||||
{ _id: { $in: ids as any } },
|
||||
{
|
||||
associations: associationQueries
|
||||
}
|
||||
)
|
||||
|
||||
const refreshedMap = new Map(refreshedDocs.map((d) => [d._id, d]))
|
||||
|
||||
for (const doc of docs) {
|
||||
const refreshed = refreshedMap.get(doc._id)
|
||||
if (refreshed !== undefined) {
|
||||
;(doc as any).$associations = (refreshed as any).$associations
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to preload associations for markdown table', error)
|
||||
}
|
||||
}
|
||||
|
||||
function collectRelationshipDocsForRefPreload (
|
||||
props: CopyRelationshipTableAsMarkdownProps,
|
||||
hierarchy: Hierarchy
|
||||
@@ -252,6 +282,8 @@ export async function buildMarkdownTableFromDocs (
|
||||
|
||||
// Preload referenced documents for RefTo / ArrOf<RefTo> attributes into $lookup
|
||||
await preloadRefLookups(docs, displayableModel, hierarchy, client)
|
||||
// Preload associations for $associations keys
|
||||
await preloadAssociations(docs, displayableModel, client)
|
||||
|
||||
const language = getCurrentLanguage()
|
||||
const userCache = new Map<PersonId, string>()
|
||||
@@ -358,45 +390,51 @@ export async function buildRelationshipTableMarkdown (
|
||||
continue
|
||||
}
|
||||
|
||||
const rawValue = getAttributeValue(cell.attribute, doc, hierarchy)
|
||||
|
||||
let docToUse = doc
|
||||
let docToUse: Doc | undefined = doc
|
||||
let docClass = props.cardClass
|
||||
let attributeToUse = cell.attribute
|
||||
|
||||
if (isAssociationKey) {
|
||||
if (rawValue !== undefined && rawValue !== null && typeof rawValue === 'object' && '_class' in rawValue) {
|
||||
docToUse = rawValue as Doc
|
||||
if (cell.object !== undefined) {
|
||||
docToUse = cell.object
|
||||
docClass = docToUse._class
|
||||
const parts = cell.attribute.key.split('$associations.')
|
||||
if (parts.length > 1) {
|
||||
const afterAssoc = parts[1].substring(1)
|
||||
const dotIndex = afterAssoc.indexOf('.')
|
||||
if (dotIndex > 0) {
|
||||
const attributeName = afterAssoc.substring(dotIndex + 1)
|
||||
attributeToUse = {
|
||||
...cell.attribute,
|
||||
key: attributeName
|
||||
}
|
||||
} else {
|
||||
attributeToUse = {
|
||||
...cell.attribute,
|
||||
key: ''
|
||||
}
|
||||
|
||||
// Strip association prefix for formatValue
|
||||
const parts = cell.attribute.key.split('.')
|
||||
let lastAssocIndex = -1
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (parts[i] === '$associations' && i + 1 < parts.length) {
|
||||
lastAssocIndex = i
|
||||
}
|
||||
}
|
||||
if (lastAssocIndex !== -1) {
|
||||
attributeToUse = {
|
||||
...cell.attribute,
|
||||
key: parts.slice(lastAssocIndex + 2).join('.')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
docToUse = cell.parentObject
|
||||
if (docToUse !== undefined) {
|
||||
docClass = docToUse._class
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (docToUse === undefined) {
|
||||
if (cell.rowSpan === 0) continue
|
||||
row[attrIndex] = ''
|
||||
continue
|
||||
}
|
||||
|
||||
const isFirstColumn = attrIndex === 0
|
||||
const allowEmptyKey = isFirstColumn || isAssociationKey
|
||||
let value = await formatValue(
|
||||
attributeToUse,
|
||||
docToUse,
|
||||
hierarchy,
|
||||
docClass,
|
||||
language,
|
||||
allowEmptyKey,
|
||||
isFirstColumn || isAssociationKey,
|
||||
userCache,
|
||||
props.valueFormatter
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user