import _ from 'lodash'; import { Model, ObjectModel, ImageModel } from '@stackbit/sdk'; import { omitByNil } from '@stackbit/utils'; import { isLocalizedField, getLocalizedFieldForLocale, isDocumentFieldOneOfFieldTypes } from '@stackbit/types'; import type * as StackbitTypes from '@stackbit/types'; import type * as ContentStoreTypes from '../types'; import { ASSET_MODEL, IMAGE_MODEL } from '../common/common-schema'; import { BackCompatContentSourceInterface } from './backward-compatibility'; import { getImageFieldsFromSourceData } from './asset-sources-utils'; export function mapCSIAssetsToStoreAssets({ csiAssets, contentSourceInstance, defaultLocaleCode }: { csiAssets: StackbitTypes.Asset[]; contentSourceInstance: BackCompatContentSourceInterface; defaultLocaleCode?: string; }): ContentStoreTypes.Asset[] { const extra = getMetadataFromContentStore({ contentSourceInstance }); return csiAssets.map((csiAsset) => sourceAssetToStoreAsset({ csiAsset, defaultLocaleCode, extra }) ); } function sourceAssetToStoreAsset({ csiAsset, defaultLocaleCode, extra }: { csiAsset: StackbitTypes.Asset; defaultLocaleCode?: string; extra: { srcType: string; srcProjectId: string; srcProjectUrl: string; srcEnvironment: string }; }): ContentStoreTypes.Asset { return omitByNil({ type: 'asset', ...extra, srcObjectId: csiAsset.id, srcObjectUrl: csiAsset.manageUrl, srcObjectLabel: getAssetLabel(csiAsset, defaultLocaleCode), srcModelName: ASSET_MODEL.name, srcModelLabel: ASSET_MODEL.label ?? '', isChanged: csiAsset.status === 'added' || csiAsset.status === 'modified', status: csiAsset.status, createdAt: csiAsset.createdAt, createdBy: csiAsset.createdBy, updatedAt: csiAsset.updatedAt, updatedBy: csiAsset.updatedBy, locale: csiAsset.locale, hidden: csiAsset.hidden, fields: mapCSIAssetsFieldsToStoreFields({ csiAssetFields: csiAsset.fields, modelFields: ASSET_MODEL.fields ?? [] }) }); } function mapCSIAssetsFieldsToStoreFields({ csiAssetFields, modelFields }: { csiAssetFields: Record; modelFields: ContentStoreTypes.AssetModelField[]; }): ContentStoreTypes.AssetFields { return modelFields?.reduce((result: Record, modelField) => { const csiAssetField = csiAssetFields[modelField.name]; const docField = mapCSIAssetFieldToStoreField({ csiAssetField, modelField, localized: modelField.localized }); if ('label' in docField) { docField.label = modelField.label ?? docField.label; } result[modelField.name] = docField; return result; }, {}) as ContentStoreTypes.AssetFields; } function mapCSIAssetFieldToStoreField({ csiAssetField, modelField, localized }: { csiAssetField: StackbitTypes.AssetField | undefined; modelField: ContentStoreTypes.AssetModelField; localized?: boolean; }): ContentStoreTypes.AssetField { switch (modelField.type) { case 'string': case 'text': if (!csiAssetField) { if (localized) { return { type: modelField.type, localized: true, locales: {} }; } return { type: modelField.type, localized: false }; } return { label: _.upperFirst(modelField.name), ...csiAssetField, type: modelField.type } as ContentStoreTypes.DocumentStringLikeFieldForType<'string' | 'text'>; case 'assetFile': if (!csiAssetField) { return { type: modelField.type, ...(localized ? { localized: true, locales: {} } : { url: '' }) } as ContentStoreTypes.AssetFileField; } return { ...csiAssetField, type: modelField.type } as ContentStoreTypes.AssetFileField; default: { const _exhaustiveCheck: never = modelField; return _exhaustiveCheck; } } } /** * CSI documents do not specify unset fields. For example, if the "title" field * is not set in CMS, the CSI document will not have the title field in its * "fields" map. * * On the other hand, and for historical reasons, Stackbit client requires all * fields to be defined on the document, even the fields that are not set to any * value. Therefore, until this issue is solved and Stackbit client will be able * to infer unset document field from the model, every content store document * need to be extended and list all the fields from the matching model. * * Empty primitive fields like "string" are regarded empty when they have no * "value" property. Other more complex fields like objects and references have * the special "isUnset" property. * * @param csiDocuments * @param contentSourceInstance * @param modelMap * @param defaultLocaleCode * @param assetSources * @param createConfigDelegate */ export function mapCSIDocumentsToStoreDocuments({ csiDocuments, contentSourceInstance, modelMap, defaultLocaleCode, assetSources, createConfigDelegate, logger }: { csiDocuments: StackbitTypes.Document[]; contentSourceInstance: BackCompatContentSourceInterface; modelMap: Record; defaultLocaleCode?: string; assetSources: StackbitTypes.AssetSource[]; createConfigDelegate: () => StackbitTypes.ConfigDelegate; logger: StackbitTypes.Logger; }): ContentStoreTypes.Document[] { const meta = getMetadataFromContentStore({ contentSourceInstance }); return csiDocuments.map((csiDocument) => mapCSIDocumentToStoreDocument({ csiDocument, model: modelMap[csiDocument.modelName]!, modelMap, defaultLocaleCode, meta, assetSources, createConfigDelegate, logger }) ); } function mapCSIDocumentToStoreDocument({ csiDocument, model, modelMap, defaultLocaleCode, meta, assetSources, createConfigDelegate, logger }: { csiDocument: StackbitTypes.Document; model: Model; modelMap: Record; defaultLocaleCode?: string; meta: { srcType: string; srcProjectId: string; srcProjectUrl: string; srcEnvironment: string }; assetSources: StackbitTypes.AssetSource[]; createConfigDelegate: () => StackbitTypes.ConfigDelegate; logger: StackbitTypes.Logger; }): ContentStoreTypes.Document { return omitByNil({ type: 'document', ...meta, srcObjectId: csiDocument.id, srcObjectUrl: csiDocument.manageUrl, getPreview: ({ delegate, locale }: { delegate?: StackbitTypes.ConfigDelegate; locale?: string }) => getDocumentPreview({ csiDocument, model, srcType: meta.srcType, srcProjectId: meta.srcProjectId, delegate: delegate ?? createConfigDelegate(), locale }), srcModelLabel: model.label ?? _.startCase(csiDocument.modelName), srcModelName: csiDocument.modelName, isChanged: csiDocument.status === 'added' || csiDocument.status === 'modified', status: csiDocument.status, createdAt: csiDocument.createdAt, createdBy: csiDocument.createdBy, updatedAt: csiDocument.updatedAt, updatedBy: csiDocument.updatedBy, hidden: typeof csiDocument.hidden === 'boolean' ? csiDocument.hidden : model.hidden, locale: getDocumentLocale({ csiDocument, model }), fields: mapCSIFieldsToStoreFields({ csiDocumentFields: csiDocument.fields, modelFields: model.fields ?? [], context: { srcType: meta.srcType, srcProjectId: meta.srcProjectId, parentDocument: csiDocument, modelMap, defaultLocaleCode, assetSources, createConfigDelegate, fieldPath: [], logger } }), permissions: csiDocument.permissions }); } type MapContext = { srcType: string; srcProjectId: string; parentDocument: StackbitTypes.Document; modelMap: Record; defaultLocaleCode?: string; assetSources: StackbitTypes.AssetSource[]; createConfigDelegate: () => StackbitTypes.ConfigDelegate; fieldPath: (string | number)[]; logger: StackbitTypes.Logger; }; function mapCSIFieldsToStoreFields({ csiDocumentFields, modelFields, context }: { csiDocumentFields: Record; modelFields: StackbitTypes.Field[]; context: MapContext; }): Record { return modelFields.reduce((result: Record, modelField) => { const csiDocumentField = csiDocumentFields[modelField.name]; const fieldPath = context.fieldPath.concat(modelField.name); const docField = mapCSIFieldToStoreField({ csiDocumentField, modelField, localized: modelField.localized, context: { ...context, fieldPath: fieldPath } }); docField.label = modelField.label; result[modelField.name] = docField; return result; }, {}); } function mapCSIFieldToStoreField({ csiDocumentField, modelField, localized, context }: { csiDocumentField: StackbitTypes.DocumentField | undefined; modelField: StackbitTypes.Field | StackbitTypes.FieldSpecificProps; localized?: boolean; context: MapContext; }): ContentStoreTypes.DocumentField { if (!csiDocumentField) { const shouldUseIsUnset = ['markdown', 'richText', 'image', 'file', 'json', 'object', 'model', 'reference', 'cross-reference'].includes(modelField.type); return { type: modelField.type, ...(localized ? { localized, locales: {} } : { ...(shouldUseIsUnset ? { isUnset: true } : null), ...(modelField.type === 'list' ? { items: [] } : null) }) } as ContentStoreTypes.DocumentField; } switch (modelField.type) { case 'string': case 'text': case 'html': case 'url': case 'boolean': case 'number': case 'date': case 'datetime': case 'enum': case 'json': // TODO: 'json' and 'style' fields can be remapped from 'string' and 'text', in this case parse the JSON object case 'style': case 'color': case 'slug': // Override document field types with model field types. // Developer can remap content-source model fields to different field using stackbit config. // For example, a 'string' field in a content-source can be mapped to 'color' field in stackbit config. return { ...csiDocumentField, type: modelField.type } as ContentStoreTypes.DocumentField; case 'image': // The 'image' model field can be a 'reference' document field in CMSes like Sanity and Contentful. if (csiDocumentField.type === 'reference') { return csiDocumentField; } return mapImageField(csiDocumentField, modelField, context.assetSources, context.logger); // Don't override types of the following document fields. // Rest of the fields must have the same type across document and model fields. case 'file': case 'reference': return csiDocumentField as ContentStoreTypes.DocumentField; case 'cross-reference': return mapCrossReferenceField(csiDocumentField); case 'object': return mapObjectField(csiDocumentField as StackbitTypes.DocumentObjectField, modelField, context); case 'model': return mapModelField(csiDocumentField as StackbitTypes.DocumentModelField, modelField, context); case 'list': // list can not be in list, so modelField must be FieldList return mapListField(csiDocumentField as StackbitTypes.DocumentListField, modelField as StackbitTypes.FieldList, context); case 'richText': return mapRichTextField(csiDocumentField as StackbitTypes.DocumentRichTextField); case 'markdown': return mapMarkdownField(csiDocumentField as StackbitTypes.DocumentStringLikeField); default: { const _exhaustiveCheck: never = modelField; return _exhaustiveCheck; } } } function mapImageField( csiDocumentField: StackbitTypes.DocumentField, imageModelField: StackbitTypes.FieldImageProps, assetSources: StackbitTypes.AssetSource[], logger: StackbitTypes.Logger ): ContentStoreTypes.DocumentImageField { // the image can be remapped from 'string', 'text' or 'json' fields if (isDocumentFieldOneOfFieldTypes(csiDocumentField, ['string', 'text', 'json'])) { try { if (!isLocalizedField(csiDocumentField)) { if (imageModelField.source) { return omitByNil({ type: 'image', source: imageModelField.source, sourceData: csiDocumentField.value, fields: getImageFieldsFromSourceData({ sourceData: csiDocumentField.value, imageModelField: imageModelField, assetSources, logger }) }); } } else { return omitByNil({ type: 'image', source: imageModelField.source, localized: true, locales: _.mapValues(csiDocumentField.locales, (locale) => { return { locale: locale.locale, sourceData: locale.value, fields: getImageFieldsFromSourceData({ sourceData: locale.value, imageModelField: imageModelField, assetSources, logger }) }; }) }); } } catch (e) { return { type: 'image', ...(isLocalizedField(csiDocumentField) ? { localized: true, locales: {} } : { isUnset: true }) }; } } if (csiDocumentField.type !== 'image') { return { type: 'image', ...(isLocalizedField(csiDocumentField) ? { localized: true, locales: {} } : { isUnset: true }) }; } if (!isLocalizedField(csiDocumentField)) { return omitByNil({ type: 'image', source: csiDocumentField.source, sourceData: csiDocumentField.sourceData, fields: csiDocumentField.fields ?? getImageFieldsFromSourceData({ sourceData: csiDocumentField.sourceData, imageModelField: imageModelField, assetSources, logger }) }); } return omitByNil({ type: 'image', source: csiDocumentField.source, localized: true, locales: _.mapValues(csiDocumentField.locales, (locale) => { return { locale: locale.locale, sourceData: locale.sourceData, fields: // for backward compatibility use, fields if provided locale.fields ?? getImageFieldsFromSourceData({ sourceData: locale.sourceData, imageModelField: imageModelField, assetSources, logger }) }; }) }); } function mapCrossReferenceField(csiDocumentField: StackbitTypes.DocumentField): ContentStoreTypes.DocumentCrossReferenceField { const unlocalizedUnset = { type: 'cross-reference', refType: 'document', isUnset: true } as const; if (!isDocumentFieldOneOfFieldTypes(csiDocumentField, ['string', 'text', 'json', 'cross-reference'])) { if (isLocalizedField(csiDocumentField)) { return { type: 'cross-reference', refType: 'document', localized: true, locales: {} }; } return unlocalizedUnset; } if (csiDocumentField.type === 'cross-reference') { if (isLocalizedField(csiDocumentField)) { return { type: 'cross-reference', refType: 'document', localized: true, locales: _.reduce( csiDocumentField.locales, (accum: Record, locale, localeKey) => { // the documentField.type is 'cross-reference', so it is already in the correct format accum[localeKey] = locale; return accum; }, {} ) }; } else { return csiDocumentField; } } const parseRefObject = (value: any): { refId: string; refSrcType: string; refProjectId: string } | null => { if (typeof value === 'string') { try { value = JSON.parse(value); } catch (error) { return null; } } if (_.isPlainObject(value) && 'refId' in value && 'refSrcType' in value && 'refProjectId' in value) { return { refId: value.refId, refSrcType: value.refSrcType, refProjectId: value.refProjectId }; } return null; }; if (isLocalizedField(csiDocumentField)) { return { type: 'cross-reference', refType: 'document', localized: true, locales: _.reduce( csiDocumentField.locales, (accum: Record, locale, localeKey) => { const refObject = parseRefObject(locale.value); if (refObject) { accum[localeKey] = { locale: locale.locale, ...refObject }; } return accum; }, {} ) }; } if (!('value' in csiDocumentField)) { return unlocalizedUnset; } const refObject = parseRefObject(csiDocumentField.value); if (!refObject) { return unlocalizedUnset; } return { type: 'cross-reference', refType: 'document', ...refObject }; } function mapObjectField( csiDocumentField: StackbitTypes.DocumentObjectField, modelField: StackbitTypes.FieldObjectProps, context: MapContext ): ContentStoreTypes.DocumentObjectField { const _getObjectPreview = ({ delegate, locale }: { delegate?: StackbitTypes.ConfigDelegate; locale?: string }) => { return getObjectPreview({ parentDocument: context.parentDocument, documentField: csiDocumentField, objectModelOrObjectField: modelField, srcType: context.srcType, srcProjectId: context.srcProjectId, delegate: delegate ?? context.createConfigDelegate(), locale }) as ContentStoreTypes.DocumentObjectFieldPreview; }; if (!isLocalizedField(csiDocumentField)) { return { type: csiDocumentField.type, getPreview: _getObjectPreview, fields: mapCSIFieldsToStoreFields({ csiDocumentFields: csiDocumentField.fields ?? {}, modelFields: modelField.fields ?? [], context }) }; } return { type: csiDocumentField.type, localized: true, locales: _.mapValues(csiDocumentField.locales, (locale) => { const fieldPath = context.fieldPath.concat(locale.locale); return { locale: locale.locale, getPreview: _getObjectPreview, fields: mapCSIFieldsToStoreFields({ csiDocumentFields: locale.fields ?? {}, modelFields: modelField.fields ?? [], context: { ...context, fieldPath: fieldPath } }) }; }) }; } function mapModelField( csiDocumentField: StackbitTypes.DocumentModelField, modelField: StackbitTypes.FieldModelProps, context: MapContext ): ContentStoreTypes.DocumentModelField { const _getObjectPreview = (model: ObjectModel) => ({ delegate, locale }: { delegate?: StackbitTypes.ConfigDelegate; locale?: string }) => { return getObjectPreview({ parentDocument: context.parentDocument, documentField: csiDocumentField, objectModelOrObjectField: model, srcType: context.srcType, srcProjectId: context.srcProjectId, delegate: delegate ?? context.createConfigDelegate(), locale }) as ContentStoreTypes.DocumentModelFieldPreview; }; if (!isLocalizedField(csiDocumentField)) { const model = context.modelMap[csiDocumentField.modelName]! as ObjectModel; return { type: csiDocumentField.type, getPreview: _getObjectPreview(model), srcModelName: csiDocumentField.modelName, srcModelLabel: model.label ?? _.startCase(model.name), fields: mapCSIFieldsToStoreFields({ csiDocumentFields: csiDocumentField.fields ?? {}, modelFields: model.fields ?? [], context }) }; } return { type: csiDocumentField.type, localized: true, locales: _.mapValues(csiDocumentField.locales, (locale) => { const model = context.modelMap[locale.modelName]! as ObjectModel; const fieldPath = context.fieldPath.concat(locale.locale); return { locale: locale.locale, getPreview: _getObjectPreview(model), srcModelName: locale.modelName, srcModelLabel: model.label ?? _.startCase(model.name), fields: mapCSIFieldsToStoreFields({ csiDocumentFields: locale.fields ?? {}, modelFields: model.fields ?? [], context: { ...context, fieldPath } }) }; }) }; } function mapListField( csiDocumentField: StackbitTypes.DocumentListField, modelField: StackbitTypes.FieldList, context: MapContext ): ContentStoreTypes.DocumentListField { if (!isLocalizedField(csiDocumentField)) { return { type: csiDocumentField.type, items: csiDocumentField.items.map( (item, index) => mapCSIFieldToStoreField({ csiDocumentField: item, modelField: modelField.items ?? { type: 'string' }, // list items can not be localized, only the list itself can be localized localized: false, context: { ...context, fieldPath: context.fieldPath.concat(index) } }) as ContentStoreTypes.DocumentListFieldItems ) }; } return { type: csiDocumentField.type, localized: true, locales: _.mapValues(csiDocumentField.locales, (locale) => { return { locale: locale.locale, items: (locale.items ?? []).map( (item, index) => mapCSIFieldToStoreField({ csiDocumentField: item, modelField: modelField.items ?? { type: 'string' }, // list items can not be localized, only the list itself can be localized localized: false, context: { ...context, fieldPath: context.fieldPath.concat([locale.locale, index]) } }) as ContentStoreTypes.DocumentListFieldItems ) }; }) }; } function mapRichTextField(csiDocumentField: StackbitTypes.DocumentRichTextField): ContentStoreTypes.DocumentRichTextField { if (!isLocalizedField(csiDocumentField)) { return { ...csiDocumentField, multiElement: true }; } return { type: csiDocumentField.type, localized: true, locales: _.mapValues(csiDocumentField.locales, (locale) => { return { ...locale, multiElement: true }; }) }; } function mapMarkdownField(csiDocumentField: StackbitTypes.DocumentStringLikeField): ContentStoreTypes.DocumentMarkdownField { if (!isLocalizedField(csiDocumentField)) { return { type: 'markdown', value: csiDocumentField.value, multiElement: true }; } return { type: 'markdown', localized: true, locales: _.mapValues(csiDocumentField.locales, (locale) => { return { ...locale, multiElement: true }; }) }; } function getMetadataFromContentStore({ contentSourceInstance }: { contentSourceInstance: BackCompatContentSourceInterface }): { srcType: string; srcProjectId: string; srcProjectUrl: string; srcEnvironment: string; } { return { srcType: contentSourceInstance.getContentSourceType(), srcProjectId: contentSourceInstance.getProjectId(), srcProjectUrl: contentSourceInstance.getProjectManageUrl(), srcEnvironment: contentSourceInstance.getProjectEnvironment() }; } export function getDocumentLocale({ csiDocument, model }: { csiDocument: StackbitTypes.Document; model: Model }): string | undefined { if (csiDocument.locale) { return csiDocument.locale; } if ((model.type === 'page' || model.type === 'data') && model.localized && typeof model.locale === 'function') { return model.locale({ document: csiDocument, model }); } } export function getDocumentPreview({ csiDocument, model, srcType, srcProjectId, delegate, locale }: { csiDocument: StackbitTypes.Document; model: Model; srcType: string; srcProjectId: string; delegate: StackbitTypes.ConfigDelegate; locale?: string; }): ContentStoreTypes.DocumentPreview { let previewTitle: string | undefined; if ('preview' in model && model.preview && (model.type === 'page' || model.type === 'data')) { if (typeof model.preview === 'function') { const previewResult = model.preview({ document: { ...csiDocument, srcType, srcProjectId }, currentLocale: locale, ...delegate }); previewTitle = previewResult.title; } else if (model.preview.title) { // do not pass locale when resolving automatically to use default locale previewTitle = resolveDocumentLabelForFieldPath({ document: { ...csiDocument, srcType, srcProjectId }, fieldPath: model.preview.title, delegate }); } } // if previewTitle was not resolved, resolve using model, at the worst case the model name or label will be used if (!previewTitle) { previewTitle = getObjectTitleFromLabelField({ document: { ...csiDocument, srcType, srcProjectId }, modelOrObjectField: model, delegate, locale }); } if (!previewTitle) { previewTitle = getObjectTitleFromModel(model); } return { previewTitle: previewTitle?.toString?.() }; } function getObjectPreview({ parentDocument, documentField, objectModelOrObjectField, srcType, srcProjectId, delegate, locale }: { parentDocument: StackbitTypes.Document; documentField: Type; objectModelOrObjectField: Type extends StackbitTypes.DocumentModelField ? ObjectModel : StackbitTypes.FieldObjectProps; srcType: string; srcProjectId: string; delegate: StackbitTypes.ConfigDelegate; locale?: string; }): ContentStoreTypes.DocumentObjectFieldPreview { let previewTitle: string | undefined; if ('preview' in objectModelOrObjectField && objectModelOrObjectField.preview) { if (typeof objectModelOrObjectField.preview === 'function') { const previewResult = objectModelOrObjectField.preview({ parentDocument: { ...parentDocument, srcType, srcProjectId }, documentField: documentField, currentLocale: locale, ...delegate }); previewTitle = previewResult.title; } else if (objectModelOrObjectField.preview.title) { // do not pass locale when resolving automatically to use default locale previewTitle = resolveDocumentLabelForFieldPath({ document: { ...parentDocument, srcType, srcProjectId }, fromField: documentField, fieldPath: objectModelOrObjectField.preview.title, delegate }); } } if (!previewTitle) { previewTitle = getObjectTitleFromLabelField({ document: { ...parentDocument, srcType, srcProjectId }, documentField, modelOrObjectField: objectModelOrObjectField, delegate, locale }); } if (!previewTitle && 'name' in objectModelOrObjectField) { previewTitle = getObjectTitleFromModel(objectModelOrObjectField); } return { previewTitle: previewTitle?.toString?.() }; } function resolveDocumentLabelForFieldPath({ document, fromField, fieldPath, delegate }: { document: StackbitTypes.DocumentWithSource; fromField?: StackbitTypes.DocumentModelField | StackbitTypes.DocumentObjectField; fieldPath: string; delegate: StackbitTypes.ConfigDelegate; }): string | undefined { const documentField = delegate.getDocumentFieldForFieldPath({ document: document, fromField: fromField, fieldPath: fieldPath }); if (documentField && 'value' in documentField) { return documentField.value?.toString?.(); } return undefined; } function getObjectTitleFromLabelField({ document, documentField, modelOrObjectField, delegate }: { document: StackbitTypes.DocumentWithSource; documentField?: StackbitTypes.DocumentModelField | StackbitTypes.DocumentObjectField; modelOrObjectField: Model | StackbitTypes.FieldObjectProps | ImageModel; delegate: StackbitTypes.ConfigDelegate; locale?: string; }): string | undefined { const labelField = modelOrObjectField.labelField; if (!labelField) { return; } return resolveDocumentLabelForFieldPath({ document: document, fromField: documentField, fieldPath: labelField, delegate }); } function getAssetLabel(csiAsset: StackbitTypes.Asset, locale?: string): string { const imageModel = IMAGE_MODEL; let label = getAssetLabelFromLabelField(csiAsset, imageModel, locale); if (!label) { label = getObjectTitleFromModel(imageModel); } return label; } function getAssetLabelFromLabelField(csiAsset: StackbitTypes.Asset, imageModel: ImageModel, locale?: string): string | undefined { const labelField = imageModel.labelField; if (!labelField) { return; } const field = _.get(csiAsset.fields, labelField); if (!field) { return; } const localizedField = getLocalizedFieldForLocale(field as StackbitTypes.DocumentField, locale); if (localizedField && 'value' in localizedField && localizedField.value) { return localizedField.value; } } function getObjectTitleFromModel(model: Model | ImageModel): string { return model.label ? model.label : _.startCase(model.name); }