mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-12 04:37:44 +02:00
Load RefTo fields in markdown table (#10642)
* Load RefTo fields in markdown table Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix Polynomial regexp Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix formatting Signed-off-by: Artem Savchenko <armisav@gmail.com> --------- Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
@@ -30,6 +30,11 @@ jest.mock('../data/personLoader', () => ({
|
||||
loadPersonName: jest.fn(async (personId: string) => personId)
|
||||
}))
|
||||
|
||||
// Avoid pulling in Svelte/UI dependencies via createMarkdownLink
|
||||
jest.mock('../markdown/link', () => ({
|
||||
createMarkdownLink: jest.fn(async (_hierarchy: any, _doc: any, text: string) => text)
|
||||
}))
|
||||
|
||||
describe('formatter/valueFormatter', () => {
|
||||
const baseCard = {
|
||||
_id: 'card-1',
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
DocumentAttributeKey,
|
||||
DateFormatOption
|
||||
} from './utils'
|
||||
import { createMarkdownLink } from '../markdown/link'
|
||||
import { loadPersonName } from '../data/personLoader'
|
||||
import type { ValueFormatter } from '../types'
|
||||
|
||||
@@ -251,14 +252,10 @@ export async function formatCustomAttributeValue (
|
||||
if (isRef && attribute !== undefined) {
|
||||
const cardWithLookup = card as any
|
||||
const lookupData = cardWithLookup.$lookup?.[attribute.name]
|
||||
if (lookupData !== undefined && lookupData !== null) {
|
||||
if (typeof lookupData === 'object' && 'title' in lookupData) {
|
||||
const title = lookupData.title ?? ''
|
||||
if (typeof title === 'string' && isIntlString(title)) {
|
||||
return await translate(title as unknown as IntlString, {}, language)
|
||||
}
|
||||
return String(title)
|
||||
}
|
||||
if (lookupData !== undefined && lookupData !== null && typeof lookupData === 'object') {
|
||||
const title = await extractObjectTitleOrName(lookupData as Doc, language)
|
||||
const text = title !== '' ? title : value
|
||||
return await createMarkdownLink(hierarchy, lookupData as Doc, text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +291,9 @@ async function formatValueFallback (
|
||||
return ''
|
||||
}
|
||||
|
||||
const isCustomAttribute = attr.key === '' && typeof attr.label === 'string' && attr.label.startsWith('custom')
|
||||
const isCustomAttribute =
|
||||
(ctx.attribute as any)?.isCustom === true ||
|
||||
(attr.key === '' && typeof attr.label === 'string' && attr.label.startsWith('custom'))
|
||||
if (isCustomAttribute) {
|
||||
return await formatCustomAttributeValue(value, ctx.attribute, card, hierarchy, language)
|
||||
}
|
||||
@@ -334,15 +333,10 @@ async function formatValueFallback (
|
||||
const isRef = attrType?._class === core.class.RefTo
|
||||
if (isRef) {
|
||||
const lookupData = getLookupData(card, ctx.lookupKey, attribute?.name ?? '', attr.key)
|
||||
if (lookupData !== undefined && lookupData !== null) {
|
||||
const resolvedObj = lookupData
|
||||
if (typeof resolvedObj === 'object' && resolvedObj !== null && 'title' in resolvedObj) {
|
||||
const title = resolvedObj[DocumentAttributeKey.Title] ?? ''
|
||||
if (typeof title === 'string' && isIntlString(title)) {
|
||||
return await translate(title as unknown as IntlString, {}, language)
|
||||
}
|
||||
return String(title)
|
||||
}
|
||||
if (lookupData !== undefined && lookupData !== null && typeof lookupData === 'object') {
|
||||
const title = await extractObjectTitleOrName(lookupData as Doc, language)
|
||||
const text = title !== '' ? title : value
|
||||
return await createMarkdownLink(hierarchy, lookupData as Doc, text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Class, Client, Doc, Hierarchy, Ref, PersonId } from '@hcengineering/core'
|
||||
import core, { type Class, type Client, type Doc, type Hierarchy, type Ref, type PersonId } from '@hcengineering/core'
|
||||
import { getCurrentLanguage } from '@hcengineering/theme'
|
||||
import type {
|
||||
AttributeModel,
|
||||
@@ -31,6 +31,74 @@ import { rebuildRelationshipTableViewModel, isRelationshipTable } from '../data'
|
||||
import { escapeMarkdownLinkText } from './escape'
|
||||
import { createMarkdownLink } from './link'
|
||||
|
||||
async function preloadRefLookups (
|
||||
docs: Doc[],
|
||||
model: AttributeModel[],
|
||||
hierarchy: Hierarchy,
|
||||
client: Client
|
||||
): Promise<void> {
|
||||
const refAttrs = model.filter((attr) => {
|
||||
const a = attr.attribute as any
|
||||
if (a?.type === undefined || a.type === null) return false
|
||||
const t = a.type
|
||||
if (t._class === core.class.RefTo) return true
|
||||
if (t._class === core.class.ArrOf && t.of?._class === core.class.RefTo) return true
|
||||
return false
|
||||
})
|
||||
|
||||
if (refAttrs.length === 0) return
|
||||
|
||||
for (const attr of refAttrs) {
|
||||
const a = attr.attribute as any
|
||||
const t = a.type
|
||||
const isArray = t._class === core.class.ArrOf
|
||||
const refType = isArray ? t.of : t
|
||||
const targetClass = refType.to as Ref<Class<Doc>>
|
||||
|
||||
const idSet = new Set<string>()
|
||||
for (const doc of docs) {
|
||||
const raw = getAttributeValue(attr, doc, hierarchy)
|
||||
if (raw === undefined || raw === null) continue
|
||||
if (Array.isArray(raw)) {
|
||||
for (const v of raw) {
|
||||
if (typeof v === 'string' && v.trim() !== '') idSet.add(v)
|
||||
}
|
||||
} else if (typeof raw === 'string' && raw.trim() !== '') {
|
||||
idSet.add(raw)
|
||||
}
|
||||
}
|
||||
|
||||
if (idSet.size === 0) continue
|
||||
|
||||
const ids = Array.from(idSet)
|
||||
const refDocs = await client.findAll(targetClass, { _id: { $in: ids as any } })
|
||||
const byId = new Map<string, Doc>()
|
||||
for (const d of refDocs) {
|
||||
byId.set(d._id as string, d)
|
||||
}
|
||||
|
||||
for (const doc of docs) {
|
||||
const raw = getAttributeValue(attr, doc, hierarchy)
|
||||
if (raw === undefined || raw === null) continue
|
||||
const cardWithLookup = doc as any
|
||||
cardWithLookup.$lookup ??= {}
|
||||
if (isArray && Array.isArray(raw)) {
|
||||
const resolved = raw
|
||||
.map((v: any) => (typeof v === 'string' ? byId.get(v) : undefined))
|
||||
.filter((v): v is Doc => v !== undefined)
|
||||
if (resolved.length > 0) {
|
||||
cardWithLookup.$lookup[a.name] = resolved
|
||||
}
|
||||
} else if (!isArray && typeof raw === 'string') {
|
||||
const resolved = byId.get(raw)
|
||||
if (resolved !== undefined) {
|
||||
cardWithLookup.$lookup[a.name] = resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function buildRelationshipTablePropsFromMetadata (
|
||||
docs: Doc[],
|
||||
metadata: BuildMarkdownTableMetadata,
|
||||
@@ -156,6 +224,9 @@ export async function buildMarkdownTableFromDocs (
|
||||
return ''
|
||||
}
|
||||
|
||||
// Preload referenced documents for RefTo / ArrOf<RefTo> attributes into $lookup
|
||||
await preloadRefLookups(docs, displayableModel, hierarchy, client)
|
||||
|
||||
const language = getCurrentLanguage()
|
||||
const userCache = new Map<PersonId, string>()
|
||||
const firstDocClass = docs.length > 0 ? docs[0]._class : props.cardClass
|
||||
@@ -182,7 +253,10 @@ export async function buildMarkdownTableFromDocs (
|
||||
const linkValue = await createMarkdownLink(hierarchy, card, value)
|
||||
row.push(linkValue)
|
||||
} else {
|
||||
row.push(escapeMarkdownLinkText(value))
|
||||
// If formatter already returned a markdown link, do not escape it again.
|
||||
const looksLikeMarkdownLink =
|
||||
typeof value === 'string' && value.startsWith('[') && value.includes('](') && value.endsWith(')')
|
||||
row.push(looksLikeMarkdownLink ? value : escapeMarkdownLinkText(value))
|
||||
}
|
||||
}
|
||||
rows.push(row)
|
||||
|
||||
Reference in New Issue
Block a user