1313// limitations under the License.
1414//
1515
16- import core , { type AnyAttribute , type Class , type Doc , type Ref } from '@hcengineering/core'
16+ import contact from '@hcengineering/contact'
17+ import core , {
18+ getDisplayTime ,
19+ type AnyAttribute ,
20+ type Class ,
21+ type Doc ,
22+ type Hierarchy ,
23+ type PersonId ,
24+ type Ref
25+ } from '@hcengineering/core'
1726import { translate , type IntlString } from '@hcengineering/platform'
27+ import { markupToJSON } from '@hcengineering/text'
28+ import { markupToMarkdown } from '@hcengineering/text-markdown'
29+ import { loadPersonNameByRef } from '../data/personLoader'
1830
1931export enum DocumentAttributeKey {
2032 CreatedBy = 'createdBy' ,
@@ -30,11 +42,114 @@ export enum DateFormatOption {
3042 Short = 'short'
3143}
3244
45+ export function formatDateValue (
46+ value : number | string | Date ,
47+ isDateOnly : boolean ,
48+ language : string | undefined
49+ ) : string | undefined {
50+ if ( ! isDateOnly && typeof value === 'number' ) {
51+ return getDisplayTime ( value )
52+ }
53+
54+ const parsedDate = value instanceof Date ? value : new Date ( value )
55+ if ( Number . isNaN ( parsedDate . getTime ( ) ) ) {
56+ return undefined
57+ }
58+
59+ const options : Intl . DateTimeFormatOptions = {
60+ year : DateFormatOption . Numeric ,
61+ month : DateFormatOption . Short ,
62+ day : DateFormatOption . Numeric
63+ }
64+
65+ return parsedDate . toLocaleDateString ( language ?? 'default' , options )
66+ }
67+
3368/**
34- * Check if a value is an IntlString id ({@link Id}: {@code plugin:resourceKind:key}) or
35- * {@link getEmbeddedLabel} output ({@code embedded:embedded:...}).
36- *
69+ * Format a single value of any supported type.
3770 */
71+ export async function formatSingleValue (
72+ value : any ,
73+ attrType : any ,
74+ hierarchy : Hierarchy ,
75+ language : string | undefined ,
76+ userCache ?: Map < PersonId , string > ,
77+ elementFormatter ?: ( doc : Doc , title : string ) => Promise < string >
78+ ) : Promise < string > {
79+ if ( value === null || value === undefined ) {
80+ return ''
81+ }
82+
83+ if (
84+ typeof value === 'number' &&
85+ ( attrType ?. _class === core . class . TypeTimestamp || attrType ?. _class === core . class . TypeDate )
86+ ) {
87+ return formatDateValue ( value , attrType ?. _class === core . class . TypeDate , language ) ?? ''
88+ }
89+
90+ if ( value instanceof Date ) {
91+ return formatDateValue ( value , true , language ) ?? ''
92+ }
93+
94+ if (
95+ typeof value === 'string' &&
96+ ( attrType ?. _class === core . class . TypeTimestamp || attrType ?. _class === core . class . TypeDate )
97+ ) {
98+ return formatDateValue ( value , attrType ?. _class === core . class . TypeDate , language ) ?? ''
99+ }
100+
101+ const isMarkup =
102+ attrType ?. _class === core . class . TypeMarkup ||
103+ attrType ?. _class === core . class . TypeCollaborativeDoc ||
104+ ( typeof value === 'object' && value !== null && ( value . type === 'doc' || value . _class === 'core:class:Markup' ) )
105+
106+ if ( isMarkup ) {
107+ try {
108+ return markupToMarkdown ( markupToJSON ( value ) )
109+ } catch ( e ) {
110+ // fallback
111+ }
112+ }
113+
114+ if ( typeof value === 'object' && value !== null ) {
115+ if ( 'title' in value || 'name' in value ) {
116+ const title = value . title ?? value . name ?? ''
117+ const text =
118+ typeof title === 'string' && isIntlString ( title )
119+ ? await translate ( title as unknown as IntlString , { } , language )
120+ : String ( title )
121+ if ( elementFormatter !== undefined ) {
122+ return await elementFormatter ( value as Doc , text )
123+ }
124+ return text
125+ }
126+ }
127+
128+ if ( typeof value === 'boolean' ) {
129+ return value ? '✅ Yes' : '❌ No'
130+ }
131+
132+ if ( typeof value === 'number' ) {
133+ return String ( value )
134+ }
135+
136+ if ( typeof value === 'string' ) {
137+ if ( isIntlString ( value ) ) {
138+ return await translate ( value as unknown as IntlString , { } , language )
139+ }
140+
141+ const isRef = attrType ?. _class === core . class . RefTo
142+ if ( isRef ) {
143+ if ( attrType . to !== undefined && hierarchy . isDerived ( attrType . to , contact . mixin . Employee ) ) {
144+ const name = await loadPersonNameByRef ( value as any , hierarchy , userCache as any )
145+ return name
146+ }
147+ }
148+ }
149+
150+ return String ( value )
151+ }
152+
38153export function isIntlString ( value : unknown ) : value is string {
39154 if ( typeof value !== 'string' || value . length === 0 ) {
40155 return false
@@ -60,60 +175,46 @@ export function isIntlString (value: unknown): value is string {
60175 return true
61176}
62177
63- /**
64- * Format an array of values, handling reference lookups if needed
65- */
66178export async function formatArrayValue (
67179 value : any [ ] ,
68180 attrType : any ,
69181 attribute : AnyAttribute | undefined ,
70182 attrKey : string ,
71183 card : Doc ,
72- language : string | undefined
184+ hierarchy : Hierarchy ,
185+ language : string | undefined ,
186+ userCache ?: Map < PersonId , string > ,
187+ elementFormatter ?: ( doc : Doc , title : string ) => Promise < string >
73188) : Promise < string > {
74- const isRefArray =
75- attrType ?. _class === core . class . ArrOf &&
76- ( attrType as { of ?: { _class ?: Ref < Class < Doc > > } } ) ?. of ?. _class === core . class . RefTo
189+ const isRef =
190+ attrType ?. _class === core . class . RefTo ||
191+ ( attrType ?. _class === core . class . ArrOf &&
192+ ( attrType as { of ?: { _class ?: Ref < Class < Doc > > } } ) ?. of ?. _class === core . class . RefTo )
77193
78- if ( isRefArray && ( attribute !== undefined || attrKey !== '' ) ) {
79- const cardWithLookup = card as any
80- const lookupKey = attribute ?. name ?? attrKey
81- const lookupData = cardWithLookup . $lookup ?. [ lookupKey ]
194+ const cardWithLookup = card as any
195+ const lookupKey = attribute ?. name ?? attrKey
196+ const lookupData = cardWithLookup . $lookup ?. [ lookupKey ]
82197
83- if ( lookupData !== undefined && lookupData !== null ) {
198+ const resolveItem = async ( v : any , index : number ) : Promise < string > => {
199+ // If we have lookup data and v is an ID, find the object
200+ let item = v
201+ if ( isRef && lookupData !== undefined && typeof v === 'string' ) {
84202 const resolvedArray = Array . isArray ( lookupData ) ? lookupData : [ lookupData ]
85- const translatedValues = await Promise . all (
86- resolvedArray . map ( async ( v ) => {
87- if ( typeof v === 'object' && v !== null && 'title' in v ) {
88- const title = v . title ?? ''
89- if ( typeof title === 'string' && isIntlString ( title ) ) {
90- return await translate ( title as unknown as IntlString , { } , language )
91- }
92- return String ( title )
93- }
94- return typeof v === 'string' ? v : String ( v )
95- } )
96- )
97- return translatedValues . join ( ', ' )
203+ const found = resolvedArray . find ( ( obj ) => obj . _id === v )
204+ if ( found !== undefined ) {
205+ item = found
206+ } else if ( resolvedArray [ index ] !== undefined ) {
207+ // Fallback to index-based lookup if no _id matches (useful for tests or simplified data)
208+ item = resolvedArray [ index ]
209+ }
98210 }
211+
212+ const itemType = attrType ?. _class === core . class . ArrOf ? attrType . of : attrType
213+ return await formatSingleValue ( item , itemType , hierarchy , language , userCache , elementFormatter )
99214 }
100215
101- const translatedValues = await Promise . all (
102- value . map ( async ( v ) => {
103- if ( typeof v === 'object' && v !== null && 'title' in v ) {
104- const title = v . title ?? ''
105- if ( typeof title === 'string' && isIntlString ( title ) ) {
106- return await translate ( title as unknown as IntlString , { } , language )
107- }
108- return String ( title )
109- }
110- if ( typeof v === 'string' && isIntlString ( v ) ) {
111- return await translate ( v as unknown as IntlString , { } , language )
112- }
113- return typeof v === 'string' ? v : String ( v )
114- } )
115- )
116- return translatedValues . join ( ', ' )
216+ const formattedValues = await Promise . all ( value . map ( async ( v , i ) => await resolveItem ( v , i ) ) )
217+ return formattedValues . filter ( ( v ) => v !== '' ) . join ( ', ' )
117218}
118219
119220/**
@@ -123,6 +224,9 @@ export async function extractObjectTitleOrName (
123224 obj : Record < string , any > ,
124225 language : string | undefined
125226) : Promise < string > {
227+ if ( obj . _class === core . class . TypeMarkup || obj . _class === core . class . TypeCollaborativeDoc ) {
228+ return '' // Should be handled by markupToMarkdown
229+ }
126230 if ( 'title' in obj ) {
127231 const title = String ( obj . title ?? '' )
128232 if ( isIntlString ( title ) ) {
0 commit comments