diff --git a/src/app/_samples/mediaco/components/banner/banner.component.scss b/src/app/_samples/mediaco/components/banner/banner.component.scss index 39ee2ebc..8ec6e20a 100644 --- a/src/app/_samples/mediaco/components/banner/banner.component.scss +++ b/src/app/_samples/mediaco/components/banner/banner.component.scss @@ -34,11 +34,11 @@ } .background-image-style { - height: calc(20rem); position: sticky; top: var(--mat-toolbar-standard-height, 64px); background: var(--mat-toolbar-container-background-color, var(--mat-sys-surface)); z-index: 10; + padding-bottom: 2rem; } .background-style { diff --git a/src/app/_samples/mediaco/components/list-view/DefaultViewMeta.ts b/src/app/_samples/mediaco/components/list-view/DefaultViewMeta.ts new file mode 100644 index 00000000..8970a4f5 --- /dev/null +++ b/src/app/_samples/mediaco/components/list-view/DefaultViewMeta.ts @@ -0,0 +1,220 @@ +const LOCALIZATON_ANNOTATION = '@L '; +const PROPERTY_ANNOTATION = '@P .'; +const USER_ANNOTATION = '@USER .'; +const ASSOCIATED_ANNOTATION = '@ASSOCIATED .'; +const ASSOCIATION_ANNOTATION = '@CA '; + +const getDefaultConfig = (fieldMeta, classID, show) => { + const { + name, + label, + fieldID, + fieldType, + dataType, + type, + classID: fieldMetaClassID, + displayAs, + displayAsLink, + category, + associationClassID, + associationID + } = fieldMeta; + return { + value: (associationClassID ? ASSOCIATION_ANNOTATION : PROPERTY_ANNOTATION).concat(fieldID), + label: LOCALIZATON_ANNOTATION.concat(name || label), + fieldType, + propertyType: dataType || type, + classID: classID || fieldMetaClassID, + displayAs, + displayAsLink, + category, + show, + ...(associationClassID ? { associationLabel: LOCALIZATON_ANNOTATION.concat(category) } : {}), + associationID + }; +}; + +export function getDefaultViewMeta(fieldMeta, classID, showField) { + const { type, name, displayAs, fieldID, isUserReference, associationID, datasource, label, fieldType } = fieldMeta; + const mapperKey = type && displayAs ? type.concat(':').concat(displayAs) : type; + const defaultConfig = getDefaultConfig(fieldMeta, classID, showField); + let viewMeta; + switch (mapperKey) { + case 'True-False:pxCheckbox': + viewMeta = { + type: 'Checkbox', + config: { + ...defaultConfig, + trueLabel: '@L Yes', + falseLabel: '@L No', + caption: LOCALIZATON_ANNOTATION.concat(name || label), + label: undefined + } + }; + break; + case 'Decimal:pxCurrency': + viewMeta = { + type: 'Currency', + config: defaultConfig + }; + break; + case 'Date Time:pxDateTime': + case 'Date & time:pxDateTime': + viewMeta = { + type: 'DateTime', + config: defaultConfig + }; + break; + case 'Date:pxDateTime': + case 'Date only:pxDateTime': + viewMeta = { + type: 'Date', + config: defaultConfig + }; + break; + case 'Decimal:pxNumber': + viewMeta = { + type: 'Decimal', + config: defaultConfig + }; + break; + case 'Text:pxEmail': + viewMeta = { + type: 'Email', + config: defaultConfig + }; + break; + case 'Integer:pxInteger': + viewMeta = { + type: 'Integer', + config: defaultConfig + }; + break; + case 'Decimal:pxPercentage': + viewMeta = { + type: 'Percentage', + config: defaultConfig + }; + break; + case 'Text:pxPhone': + viewMeta = { + type: 'Phone', + config: { + ...defaultConfig, + datasource: { + source: '@DATASOURCE D_pyCountryCallingCodeList.pxResults', + fields: { + value: '@P .pyCallingCode' + } + } + } + }; + break; + case 'TimeOfDay:pxDateTime': + viewMeta = { + type: 'Time', + config: defaultConfig + }; + break; + case 'Text:pxURL': + case 'Text:pxUrl': + viewMeta = { + type: 'URL', + config: defaultConfig + }; + break; + case 'Text:pxTextArea': + viewMeta = { + type: 'TextArea', + config: defaultConfig + }; + break; + case 'Text:pxRichTextEditor': + viewMeta = { + type: 'RichText', + config: defaultConfig + }; + break; + case 'Text:pxAutoComplete': + if (isUserReference || fieldType === 'User reference') { + viewMeta = { + type: 'UserReference', + config: { + ...defaultConfig, + value: USER_ANNOTATION.concat(fieldID), + placeholder: 'Select...', + displayAs: 'Search box', + associationID, + associationLabel: undefined + } + }; + } else { + const { tableType = '' } = datasource || {}; + viewMeta = { + type: 'AutoComplete', + config: { + ...defaultConfig, + placeholder: 'Select...', + listType: 'associated', + datasource: ASSOCIATED_ANNOTATION.concat(fieldID), + deferDatasource: tableType === 'DataPage' + } + }; + } + break; + case 'Text:pxDropdown': + if (isUserReference || fieldType === 'User reference') { + viewMeta = { + type: 'UserReference', + config: { + ...defaultConfig, + value: USER_ANNOTATION.concat(fieldID), + placeholder: 'Select...', + displayAs: 'Drop-down list', + associationID, + associationLabel: undefined + } + }; + } else { + const { tableType = '' } = datasource || {}; + viewMeta = { + type: 'Dropdown', + config: { + ...defaultConfig, + placeholder: 'Select...', + listType: 'associated', + datasource: ASSOCIATED_ANNOTATION.concat(fieldID), + deferDatasource: tableType === 'DataPage' + } + }; + } + break; + case 'Text:pxRadioButtons': + { + const { tableType = '' } = datasource || {}; + viewMeta = { + type: 'RadioButtons', + config: { + ...defaultConfig, + placeholder: 'Select...', + listType: 'associated', + datasource: ASSOCIATED_ANNOTATION.concat(fieldID), + deferDatasource: tableType === 'DataPage' + } + }; + } + break; + case 'Text:pxTextInput': + viewMeta = { + type: 'TextInput', + config: defaultConfig + }; + break; + default: + viewMeta = { + type, + config: defaultConfig + }; + } + return viewMeta; +} diff --git a/src/app/_samples/mediaco/components/list-view/list-view.component.ts b/src/app/_samples/mediaco/components/list-view/list-view.component.ts index 489bdc40..346c95ef 100644 --- a/src/app/_samples/mediaco/components/list-view/list-view.component.ts +++ b/src/app/_samples/mediaco/components/list-view/list-view.component.ts @@ -7,6 +7,8 @@ import { MatDialog } from '@angular/material/dialog'; import { MatIconButton } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; +import { init } from './listViewHelpers'; + interface ListViewProps { inheritedProps: any; title: string | undefined; @@ -46,6 +48,14 @@ export class ListViewComponent implements OnInit { referenceDataPage: string; caseTypeToActivityMap: any; title: string; + payload: any; + listContext: any = {}; + ref: any = {}; + showDynamicFields: boolean | undefined; + cosmosTableRef: any; + selectionMode: string | undefined; + fieldDefs: any; + columns: any[]; constructor( public utils: Utils, @@ -65,11 +75,35 @@ export class ListViewComponent implements OnInit { this.configProps$ = this.pConn$.getConfigProps() as ListViewProps; this.template = this.configProps$.presets[0]?.template; this.title = this.configProps$.title || ''; - this.getListData(); + this.showDynamicFields = this.configProps$?.showDynamicFields; + this.selectionMode = this.configProps$.selectionMode; + + if (this.configProps$) { + if (!this.payload) { + this.payload = { referenceList: this.configProps$.referenceList }; + } + init({ + pConn$: this.pConn$, + bInForm$: this.bInForm$, + ...this.payload, + listContext: this.listContext, + ref: this.ref, + showDynamicFields: this.showDynamicFields, + cosmosTableRef: this.cosmosTableRef, + selectionMode: this.selectionMode + }).then(response => { + this.listContext = response; + this.getListData(); + }); + } } getListData() { + this.fieldDefs = this.listContext.meta.fieldDefs; this.referenceDataPage = this.configProps$.referenceList; + const componentConfig = this.pConn$.getComponentConfig(); + const columnFields = componentConfig.presets[0].children[0].children; + this.columns = this.getHeaderCells(columnFields, this.fieldDefs); PCore.getDataPageUtils() .getDataAsync(this.referenceDataPage, this.pConn$.getContextName()) .then(({ data }) => { @@ -83,9 +117,9 @@ export class ListViewComponent implements OnInit { const caseType = this.caseTypeToActivityMap[item.ActivityType]; return { icon: this.utils.getImageSrc(this.getIcon(caseType), this.utils.getSDKStaticContentUrl()), - title: item.ActivityType, - title_subtext: this.timeSince(new Date(item.pxUpdateDateTime || item.pxCreateDateTime)), - description: item.Description + title: this.columns[0] ? item[this.columns[0]?.id] : undefined, + title_subtext: this.columns[2] ? this.timeSince(new Date(item[this.columns[2]?.id] || item.pxCreateDateTime)) : undefined, + description: this.columns[1] ? item[this.columns[1]?.id] : undefined }; }); return; @@ -94,10 +128,10 @@ export class ListViewComponent implements OnInit { this.sourceList = data.map((item, index) => { return { number: index + 1, - title: item.Name, - description: item.Genere, - description_subtext: item.Views + ' views', - rating: item.Rating + title: this.columns[0] ? item[this.columns[0]?.id] : undefined, + description: this.columns[1] ? item[this.columns[1]?.id] : undefined, + description_subtext: this.columns[2] ? item[this.columns[2]?.id] + ' views' : undefined, + rating: item[this.columns[3]?.id] }; }); return; @@ -174,4 +208,33 @@ export class ListViewComponent implements OnInit { } }); } + + private getHeaderCells(colFields, fields) { + const AssignDashObjects = ['Assign-Worklist', 'Assign-WorkBasket']; + return colFields.map((field: any, index) => { + let theField = field.config.value.substring(field.config.value.indexOf(' ') + 1); + if (theField.indexOf('.') === 0) { + theField = theField.substring(1); + } + const colIndex = fields.findIndex(ele => ele.name === theField); + const displayAsLink = field.config.displayAsLink; + const headerRow: any = {}; + headerRow.id = fields[index].id; + headerRow.type = field.type; + headerRow.displayAsLink = displayAsLink; + headerRow.numeric = field.type === 'Decimal' || field.type === 'Integer' || field.type === 'Percentage' || field.type === 'Currency' || false; + headerRow.disablePadding = false; + headerRow.label = fields[index].label; + if (colIndex > -1) { + headerRow.classID = fields[colIndex].classID; + } + if (displayAsLink) { + headerRow.isAssignmentLink = AssignDashObjects.includes(headerRow.classID); + if (field.config.value?.startsWith('@CA')) { + headerRow.isAssociation = true; + } + } + return headerRow; + }); + } } diff --git a/src/app/_samples/mediaco/components/list-view/listViewHelpers.ts b/src/app/_samples/mediaco/components/list-view/listViewHelpers.ts new file mode 100644 index 00000000..aab719ea --- /dev/null +++ b/src/app/_samples/mediaco/components/list-view/listViewHelpers.ts @@ -0,0 +1,80 @@ +import { getContext, readContextResponse } from './utils'; + +export function init(props) { + const { + referenceList, + pConn$, + personalizationId, + parameters, + compositeKeys, + isSearchable, + allowBulkActions, + ref, + showDynamicFields, + isDataObject, + cosmosTableRef + } = props; + let { editing, selectionMode } = props; + + const runtimeParams = PCore.getRuntimeParamsAPI().getRuntimeParams(); + + let selectionCountThreshold; + + // promise to fetch metadata + const metaDataPromise = PCore.getAnalyticsUtils().getDataViewMetadata(referenceList, showDynamicFields, null); + + const promisesArray: any = [metaDataPromise]; + + // promise to fetch report configured columns + const reportColumnsPromise = PCore.getAnalyticsUtils() + .getFieldsForDataSource(referenceList, false, pConn$.getContextName()) + .catch(() => { + return Promise.resolve({ + data: { data: [] } + }); + }); + promisesArray.push(reportColumnsPromise); + + const fetchEditDetails = async metadata => { + const { + data: { isQueryable } + } = metadata; + if (!isDataObject) { + if (!isQueryable) { + editing = false; /* Force editing to false if DP is non queryable */ + } + + const { MULTI_ON_HOVER, MULTI } = PCore.getConstants().LIST_SELECTION_MODE; + if (allowBulkActions && isQueryable) { + /** enable bulk actions only if DP is queryable */ + selectionMode = MULTI_ON_HOVER; + } + if ([MULTI_ON_HOVER, MULTI].includes(selectionMode)) { + selectionCountThreshold = 250; // Results count should not be greater than threshold to display SelectAll checkbox. + } + } + return Promise.resolve(); + }; + + const editPromise = metaDataPromise.then(metadata => fetchEditDetails(metadata)); + promisesArray.push(editPromise); + return getContext({ + tableSource: referenceList, + ListId: personalizationId, + runtimeParams: parameters ?? runtimeParams, + promisesArray, + pConn$, + compositeKeys, + isSearchable, + isCacheable: true + }).then(async context => { + return readContextResponse(context, { + ...props, + editing, + selectionCountThreshold, + ref, + selectionMode, + cosmosTableRef + }); + }); +} diff --git a/src/app/_samples/mediaco/components/list-view/utils.ts b/src/app/_samples/mediaco/components/list-view/utils.ts new file mode 100644 index 00000000..d88523c2 --- /dev/null +++ b/src/app/_samples/mediaco/components/list-view/utils.ts @@ -0,0 +1,767 @@ +import { getDefaultViewMeta } from './DefaultViewMeta'; + +const USER_REFERENCE = 'UserReference'; +const PAGE = '!P!'; +const PAGELIST = '!PL!'; + +export const formatConstants = { + WorkStatus: 'WorkStatus', + Integer: 'Integer', + WorkLink: 'WorkLink' +}; + +class DataApi { + mappedPropertyToOriginalProperty: any; + originalPropertyToMappedProperty: any; + constructor() { + this.originalPropertyToMappedProperty = {}; + this.mappedPropertyToOriginalProperty = {}; + this.setPropertyMaps = this.setPropertyMaps.bind(this); + this.getMappedProperty = this.getMappedProperty.bind(this); + this.getOriginalProperty = this.getOriginalProperty.bind(this); + } + + setPropertyMaps(originalToMappedPropertyObj = {}, mappedToOriginalPropertyObj = {}) { + this.originalPropertyToMappedProperty = { + ...this.originalPropertyToMappedProperty, + ...originalToMappedPropertyObj + }; + this.mappedPropertyToOriginalProperty = { + ...this.mappedPropertyToOriginalProperty, + ...mappedToOriginalPropertyObj + }; + } + + getMappedProperty(propertyName) { + return this.originalPropertyToMappedProperty[propertyName] ?? propertyName; + } + + getOriginalProperty(propertyName) { + return this.mappedPropertyToOriginalProperty[propertyName] ?? propertyName; + } +} + +export async function getContext(componentConfig) { + const { + promisesArray = [] // array of promises which can be invoked paralelly, + } = componentConfig; + const promisesResponseArray = await Promise.all(promisesArray); + const dataApi = new DataApi(); + return { + promisesResponseArray, + setPropertyMaps: dataApi.setPropertyMaps, + getMappedProperty: dataApi.getMappedProperty, + getOriginalProperty: dataApi.getOriginalProperty + }; +} + +/** + * [getFieldNameFromEmbeddedFieldName] + * Description - converting embeddedField name starting with !P! or !PL! to normal field + * @ignore + * @param {string} propertyName EmbeddedField name starting with !P! or !PL! + * @returns {string} returns converted string without !P! or !PL! and : + * + * @example Example for getFieldNameFromEmbeddedFieldName + * getFieldNameFromEmbeddedFieldName('!P!Organisation:Name') return 'Organisation.Name' + * getFieldNameFromEmbeddedFieldName('!PL!Employees:Name') return 'Employees.Name' + */ +export function getFieldNameFromEmbeddedFieldName(propertyName) { + let value = propertyName; + if (value.startsWith(PAGE) || value.startsWith(PAGELIST)) { + value = value.substring(value.lastIndexOf('!') + 1); + value = value.replace(/:/g, '.'); + } + return value; +} + +/** + * [updateMetaEmbeddedFieldID] + * Description - If the fieldID in meta starts with '!P!' or '!PL!' and contains ':' then replace them with .(dot) + * @ignore + * @param {Array} metaFields Fields metadata Array. Contains metadata of all the fields. + */ +export function updateMetaEmbeddedFieldID(metaFields) { + return metaFields.forEach(metaField => { + if (metaField.fieldID?.startsWith(PAGE) || metaField.fieldID?.startsWith(PAGELIST)) { + metaField.fieldID = getFieldNameFromEmbeddedFieldName(metaField.fieldID); + } + }); +} + +export const isEmbeddedField = field => { + if (field?.startsWith('@')) { + field = field.substring(field.indexOf(' ') + 1); + if (field[0] === '.') field = field.substring(1); + } + return field?.indexOf('.') > 0; +}; + +/** + * [isPageListProperty] + * Description - checking if propertyName is pageList or not + * @ignore + * @param {string} propertyName PropertyName + * @returns {boolean} true if property is pageList else false + * + * @example Example for isPageListProperty + * isPageListProperty('!PL!Employees.Name') return true + * isPageListProperty('!P!Employees.Name') return false + * isPageListProperty('Name') return false + */ +export function isPageListProperty(propertyName) { + return propertyName.startsWith(PAGELIST); +} + +export const isPageListInPath = (propertyName, currentClassID) => { + if (!propertyName.includes('.')) { + return false; + } + const [first, ...rest] = propertyName.split('.'); + const metadata: any = PCore.getMetadataUtils().getPropertyMetadata(first, currentClassID); + if (metadata?.type === 'Page List') { + return true; + } + return isPageListInPath(rest.join('.'), metadata?.pageClass); +}; + +/** + * [getEmbeddedFieldName] + * Description - converting normal field name to embedded field starting with !P! or !PL! + * @ignore + * @param {string} propertyName Field name + * @param {string} classID classID of datapage + * @returns {string} returns converted string with !P! or !PL! and : + * + * @example Example for getEmbeddedFieldName + * For page property, getEmbeddedFieldName('Organisation.Name') return '!P!Organisation:Name' + * For pageList property, getEmbeddedFieldName('Employees.Name') return '!PL!Employees:Name' + */ + +export function getEmbeddedFieldName(propertyName, classID) { + let value = propertyName; + if (isPageListInPath(value, classID)) { + value = `!PL!${value.replace(/\./g, ':')}`; + } else { + value = `!P!${value.replace(/\./g, ':')}`; + } + return value; +} + +/** + * [preparePropertyMaps] + * Description - preparing maps for property names and set it in dataApi context + * @ignore + * @param {Array} fields fields array + * @param {string} classID classID of datapage + * @param {string} context dataApi context + * @returns {boolean} true if pageListProperty is present + */ +export function preparePropertyMaps(fields, classID, context) { + const { setPropertyMaps } = context; + const maps = fields.reduce( + (acc, field) => { + let { value } = field.config; + if (value.startsWith('@')) { + value = value.substring(value.indexOf(' ') + 1); + if (value[0] === '.') value = value.substring(1); + } + let name = value; + // Preparing name for embedded property + if (isEmbeddedField(name)) { + name = getEmbeddedFieldName(name, classID); + } + if (isPageListProperty(name) && !acc[2]) { + acc[2] = true; + } + acc[0][value] = name; + acc[1][name] = value; + + return acc; + }, + [{}, {}, false] + ); + setPropertyMaps(maps[0], maps[1]); + return maps[2]; +} + +/** + * [getConfigEmbeddedFieldsMeta] + * Description - Get the metadata for configured embedded fields + * @ignore + * @param {Set} configFields Set of config fields + * @param {string} classID clasID of datapage + * @returns {Array} Metadata of configured embedded fields + */ +export function getConfigEmbeddedFieldsMeta(configFields, classID) { + const configEmbeddedFieldsMeta: any[] = []; + configFields.forEach(field => { + let value = field; + if (isEmbeddedField(value)) { + // conversion Page.PageList[].property => Page.PageList.property + if (value.includes('[')) { + value = value.substring(0, value.indexOf('[')) + value.substring(value.indexOf(']') + 1); + } + const meta: any = PCore.getMetadataUtils().getEmbeddedPropertyMetadata(value, classID); + meta.fieldID = field; + configEmbeddedFieldsMeta.push(meta); + } + }); + return configEmbeddedFieldsMeta; +} + +/** + * [mergeConfigEmbeddedFieldsMeta] + * Description - Get the metadata for configured embedded fields + * @ignore + * @param {Array} configEmbeddedFieldsMeta config fields metadata. + * @param {Array} metaFields Fields metadata Array. Contains metadata of all the fields + */ +export function mergeConfigEmbeddedFieldsMeta(configEmbeddedFieldsMeta, metaFields) { + const mergedMetaFields = [...metaFields]; + configEmbeddedFieldsMeta.forEach(configFieldMeta => { + const fieldMeta = metaFields.find(metaField => metaField.fieldID === configFieldMeta.fieldID); + if (!fieldMeta) mergedMetaFields.push(configFieldMeta); + }); + return mergedMetaFields; +} + +const oldToNewFieldTypeMapping = { + 'Date Time': 'Date & time', + Date: 'Date only' +}; + +/** + * [updateFieldType] + * Description - Updates the field type if its changed in the new implementation. Such mapping is maintained in oldToNewFieldTypeMapping. + * @ignore + * @param {Array} metaFields Fields metadata Array. Contains metadata of all the fields. + */ +function updateFieldType(metaFields) { + metaFields.forEach(metaField => { + if (metaField.type) metaField.type = oldToNewFieldTypeMapping[metaField.type] || metaField.type; + }); +} + +function getPresetMetaAttribute(attribute) { + const { + type, + config: { label, value } + } = attribute; + return { + type, + name: value.startsWith('@') ? value.substring(4) : value, + label: label.startsWith('@') ? label.substring(3) : label + }; +} + +/** + * [generateViewMetaData] + * Description - Returns a list of metaobjects of the fields provided. + * @ignore + * @param {Array} rawFields List of fields to update meta for + * @param {string} classID Class ID from the response + * @returns {Array} List of fields with updated meta objects. + */ +function generateViewMetaData(rawFields, classID, showField) { + return rawFields.map(item => getDefaultViewMeta(item, classID, showField)); +} + +/** + * [getConfigFields] + * Description - Returns list of config fields with primary fields meta updated. + * @ignore + * @param {Array} configFields List of Authored fields + * @param {Array} primaryFields List of Primary Fields + * @param {Array} metaFields Metadata of all fields + * @param {string} classID Class ID from the response + * @returns {Array} List of all fields with their meta updated. + */ +function getConfigFields(configFields, primaryFields, metaFields, classID) { + const presetConfigFields = configFields; + const primaryFieldsViewIndex = presetConfigFields.findIndex(field => field.config.value === 'pyPrimaryFields'); + if (!primaryFields || !primaryFields.length) { + if (primaryFieldsViewIndex < 0) return presetConfigFields; + + presetConfigFields.splice(primaryFieldsViewIndex, 1); + + return presetConfigFields; + } + + if (primaryFieldsViewIndex > -1) { + // list of uncommon fields - non overlap of primary fields grouped view and independent entity columns of primary type + const uncommonFieldsList = primaryFields.filter( + primaryField => !presetConfigFields.some(presetConfigField => presetConfigField.config.value.split('.')[1] === primaryField) + ); + const uncommonFieldsRawMeta: any[] = []; + uncommonFieldsList.forEach(uncommonField => { + const uncommonFieldMeta = metaFields.find(metaField => metaField.fieldID === uncommonField); + if (uncommonFieldMeta) uncommonFieldsRawMeta.push(uncommonFieldMeta); + }); + const uncommonFieldsConfigMeta = generateViewMetaData(uncommonFieldsRawMeta, classID, true); + + presetConfigFields.splice(primaryFieldsViewIndex, 1, ...uncommonFieldsConfigMeta); + } + return presetConfigFields; +} + +/** + * [getTableConfigFromPresetMeta] + * Description - Get the table config from the presets meta. + * @ignore + * @param {object} presetMeta Presets meta + * @param {boolean} isMetaWithPresets true if meta has presets else false + * @param {Function} getPConnect Callback to get the PConnect object + * @param {string} classID Class ID from the response + * @param {Array} primaryFields List of Primary Fields + * @param {Array} metaFields List of all metafields + * @returns {object} Table config object + */ +export function getTableConfigFromPresetMeta(presetMeta, isMetaWithPresets, pConn$, classID, primaryFields, metaFields) { + let presetId; + let presetName; + let cardHeader; + let secondaryText; + let timelineDate; + let timelineTitle; + let timelineStatus; + let timelineIcon; + let { filterExpression } = pConn$.getRawMetadata().config; + let fieldsMeta; + let configFields; + if (isMetaWithPresets) { + presetId = presetMeta.id; + presetName = presetMeta.label; + cardHeader = presetMeta.cardHeader && getPresetMetaAttribute(presetMeta.cardHeader); + secondaryText = presetMeta.secondaryText && getPresetMetaAttribute(presetMeta.secondaryText); + timelineDate = presetMeta.timelineDate && getPresetMetaAttribute(presetMeta.timelineDate); + timelineTitle = presetMeta.timelineTitle && getPresetMetaAttribute(presetMeta.timelineTitle); + timelineStatus = presetMeta.timelineStatus && getPresetMetaAttribute(presetMeta.timelineStatus); + timelineIcon = presetMeta.timelineIcon && getPresetMetaAttribute(presetMeta.timelineIcon); + filterExpression = presetMeta.config.filterExpression; + [fieldsMeta] = presetMeta.children; + if ( + presetMeta.timelineTitle && + !fieldsMeta.children.find(fieldMeta => { + return fieldMeta?.config?.value === presetMeta.timelineTitle?.config?.value; + }) + ) { + const { type, config } = presetMeta.timelineTitle; + fieldsMeta.children.push({ type, config: { ...config, show: false } }); + } + if ( + presetMeta.timelineDate && + !fieldsMeta.children.find(fieldMeta => { + return fieldMeta?.config?.value === presetMeta.timelineDate?.config?.value; + }) + ) { + const { type, config } = presetMeta.timelineDate; + fieldsMeta.children.push({ type, config: { ...config, show: false } }); + } + configFields = getConfigFields(fieldsMeta.children, primaryFields, metaFields, classID); + } else { + fieldsMeta = presetMeta.props; + configFields = getConfigFields( + fieldsMeta + .getPConnect() + .getChildren() + ?.map(child => { + return child.getPConnect().getRawMetadata(); + }), + primaryFields, + metaFields, + classID + ); + } + return { + presetId, + presetName, + cardHeader, + secondaryText, + timelineDate, + timelineTitle, + timelineStatus, + timelineIcon, + filterExpression, + fieldsMeta, + configFields + }; +} + +/** + * [getReportColumns] + * Description - Returns a set of columns from the report response. + * @ignore + * @param {object} response - + * @returns {Set} Set of columns from the report response + */ +function getReportColumns(response) { + const { + data: { data: reportColumns } + } = response; + const reportColumnsSet = new Set(); + reportColumns?.forEach(item => { + let val = item.pyFieldName; + // Remove '.' from index 0 only, if '.' is present + if (val[0] === '.') { + val = val.substring(1); + } + reportColumnsSet.add(val); + }); + return reportColumnsSet; +} + +/** + * [getConfigFieldValue] + * Descritpion - Returns a valid value for a configuration field. Remove any annotations and also "." + * @ignore + * @param {object} config + * config.value - Raw value + * @returns {string} value - Value with out any annotations or "." + */ +function getConfigFieldValue(config) { + let { value } = config; + if (value.startsWith('@')) { + value = value.substring(value.indexOf(' ') + 1); + if (value[0] === '.') value = value.substring(1); + } + return value; +} + +/** + * [prepareConfigFields] + * Description - Prepares a set of configuration fields and pushes each config type to a set using the callback parameter. + * @ignore + * @param {object} configFields List of Authored fields + * @param {Function} pushToComponentsList Callback to push the field type to a set. + * @returns {Set} configFieldSet + */ +function prepareConfigFields(configFields, pushToComponentsList) { + const configFieldSet = new Set(); + configFields.forEach(item => { + pushToComponentsList(item.type); + const val = getConfigFieldValue(item.config); + configFieldSet.add(val); + }); + return configFieldSet; +} + +/** + * [findAuthoredField] + * Description - Finds an authored field from yhe list of config fields. + * @ignore + * @param {Array} configFields List of Authored fields + * @param {string} fieldID Filter + * @returns {object} config with its field value equal to fieldID, which means an authored field + */ +function findAuthoredField(configFields, fieldID) { + return configFields.find(configField => { + const val = getConfigFieldValue(configField.config); + return val === fieldID; + }); +} + +/** + * [findAndUpdateAuthoredFieldConfig] + * Description - Find the authored field, and update its config. + * @ignore + * @param {Array} configFields List of Authored fields + * @param {object} item Field item to copy displayAs and category information + * @param {string} classId classID from the response + */ +function findAndUpdateAuthoredFieldConfig(configFields, item, classId) { + const authoredField = findAuthoredField(configFields, item.fieldID); + if (authoredField?.config) { + if (item.displayAs) { + authoredField.config.displayAs = item.displayAs; + } + authoredField.config.classID = item.classID ?? classId; + + if (authoredField.type === USER_REFERENCE) { + authoredField.config.associationID = item.associationID || item.fieldID; + } + authoredField.config.category = item.category; + // FieldType identifies whether the field is configured as pickList + authoredField.config.fieldType = item.fieldType; + // type defined on the property rule, + // used for the picklist field to assign the appropriate type + authoredField.config.propertyType = item.dataType || item.type; + } +} + +/** + * [isAnExtraField] + * Description - Returns true if the field is an extra field. Extra field is the one which is not authored but part of the report. + * @ignore + * @param {Array} configFields List of Authored fields + * @param {Set} configFieldSet Set if Authored filed values + * @param {Set} reportColumnsSet Set of columns from the report + * @param {object} item Config field item + * @param {string} classId Class ID from the response + * @param {boolean} showDynamicFields Flag indicating whether fields are fetched dynamically at runtime + * @returns {boolean} true If the field is an extra field else false. + */ +function isAnExtraField(configFields, configFieldSet, reportColumnsSet, item, classId, showDynamicFields) { + // Is the field already present in authoring metadata? + // Mutates config fields to copy displayAs and category information + if (configFieldSet.has(item.fieldID)) { + findAndUpdateAuthoredFieldConfig(configFields, item, classId); + return false; + } + + // If field is not authored and not part of report columns then discard it + return showDynamicFields || !!reportColumnsSet.has(item.fieldID); +} + +/** + * [prepareExtraFields] + * Description - Returns a list of extra fields with their meta updated. + * @ignore + * @param {Array} metaFields List of fields + * @param {Array} configFields List of Authored fields + * @param {Set} configFieldSet Set if Authored filed values + * @param {Set} reportColumnsSet Set of columns from the report + * @param {string} classID Class ID from the response + * @param {boolean} showDynamicFields Flag indicating whether fields are fetched dynamically at runtime + * @returns {Array} List of extra fields with their meta updated. + */ +function prepareExtraFields(metaFields, configFields, configFieldSet, reportColumnsSet, classID, showDynamicFields) { + // Filter all the extra fields + const extraFileds = metaFields.filter(item => { + return isAnExtraField(configFields, configFieldSet, reportColumnsSet, item, classID, showDynamicFields); + }); + return generateViewMetaData(extraFileds, classID, false); + // Update the meta object of each of the extra fields. +} + +const AssignDashObjects = ['Assign-Worklist', 'Assign-WorkBasket']; + +function populateRenderingOptions(name, config, field) { + const shouldDisplayAsSemanticLink = 'displayAsLink' in field.config && field.config.displayAsLink; + if (shouldDisplayAsSemanticLink) { + config.customObject.isAssignmentLink = AssignDashObjects.includes(field.config.classID); + if (field.config.value.startsWith('@CA')) { + config.customObject.isAssociation = true; + } + config.cellRenderer = formatConstants.WorkLink; + } else if (name === 'pyStatusWork' || name === 'pyAssignmentStatus') { + config.cellRenderer = formatConstants.WorkStatus; + } else if (name === 'pxUrgencyWork') { + config.cellRenderer = formatConstants.Integer; + } +} +function isFLProperty(label) { + return label?.startsWith('@FL'); +} + +function getFieldLabel(fieldConfig) { + const { label, classID, caption } = fieldConfig; + let fieldLabel = (label ?? caption)?.substring(4); + const labelSplit = fieldLabel?.split('.'); + const propertyName = labelSplit?.pop(); + const fieldMetaData: any = PCore.getMetadataUtils().getPropertyMetadata(propertyName, classID) ?? {}; + fieldLabel = fieldMetaData.label ?? fieldMetaData.caption ?? propertyName; + + const definedOnClassID = fieldMetaData.definedOnClassID; + const localeValue = PCore.getLocaleUtils().getLocaleValue( + fieldLabel, + `${definedOnClassID ?? fieldMetaData.classID ?? classID}.${propertyName}`, + PCore.getLocaleUtils().FIELD_LABELS_BUNDLE_KEY, + null + ); + return localeValue || fieldLabel; +} +export function initializeColumns(fields: any[] = [], getMappedProperty: any = null) { + return fields.map((field, originalColIndex) => { + let name = field.config.value; + + if (name.startsWith('@')) { + name = name.substring(name.indexOf(' ') + 1); + if (name[0] === '.') name = name.substring(1); + } + name = getMappedProperty?.(name) ?? name; + + let label = field.config.label || field.config.caption; + const { show = true, displayAs } = field.config; + if (isFLProperty(label)) { + label = getFieldLabel(field.config); + } else if (label.startsWith('@')) { + label = label.substring(3); + } + + const config = { + name, + label, + show, + classID: field.config.classID, + id: field.id || name || originalColIndex, + displayAs, + associationID: field.config.associationID, + ...(field.config.classID && { category: field.config.category }), + customObject: {}, + fieldType: field.config.fieldType, + meta: { + ...field + } + }; + + populateRenderingOptions(name, config, field); + + return config; + }); +} + +export const getItemKey = fields => { + let itemKey; + if (fields.findIndex(field => field.id === 'pyGUID') > -1) { + itemKey = 'pyGUID'; + } else { + itemKey = 'pzInsKey'; + } + return itemKey; +}; + +export function preparePatchQueryFields(fields, isDataObject = false, classID = '') { + const queryFields: any[] = []; + fields.forEach(field => { + const patchFields: any[] = []; + if (field.cellRenderer === 'WorkLink') { + if (field.customObject && field.customObject.isAssignmentLink) { + const associationName = field.name.includes(':') ? `${field.name.split(':')[0]}:` : ''; + patchFields.push(`${associationName}pzInsKey`); + patchFields.push(`${associationName}pxRefObjectClass`); + } else if (field.customObject && field.customObject.isAssociation) { + const associationCategory = field.name.split(':')[0]; + patchFields.push(`${associationCategory}:pyID`); + patchFields.push(`${associationCategory}:pzInsKey`); + patchFields.push(`${associationCategory}:pxObjClass`); + } else if (isDataObject) { + const dataViewName = PCore.getDataTypeUtils().getSavableDataPage(classID); + const dataPageKeys = PCore.getDataTypeUtils().getDataPageKeys(dataViewName); + dataPageKeys?.forEach(item => (item.isAlternateKeyStorage ? patchFields.push(item.linkedField) : patchFields.push(item.keyName))); + } else { + patchFields.push('pyID'); + patchFields.push('pzInsKey'); + patchFields.push('pxObjClass'); + } + } + patchFields.forEach(k => { + if (!queryFields.find(q => q === k)) { + queryFields.push(k); + } + }); + }); + + return queryFields; +} + +/** + * Update the renderer type for the properties of type Page. + */ +export function updatePageFieldsConfig(configFields, parentClassID) { + return configFields.forEach(item => { + const { + type, + config: { value } + } = item; + const propertyName = PCore.getAnnotationUtils().getPropertyName(value); + if (isEmbeddedField(value) && !isPageListInPath(propertyName, parentClassID)) { + item.config.componentType = type; + item.type = 'PagePropertyRenderer'; + } + }); +} + +export const readContextResponse = async (context, params) => { + const { pConn$, apiContext, children, showDynamicFields, referenceList, isDataObject } = params; + // let { listContext } = params; + const { promisesResponseArray, apiContext: otherContext } = context; + + const listOfComponents: any[] = []; + const { + data: { fields: metaFields, classID, isQueryable } + } = promisesResponseArray[0]; + let { + data: { primaryFields } + } = promisesResponseArray[0]; + // When list is configured with Include all class fields configuration, provide support for Primary fields column + if (showDynamicFields) { + const sourceMetadata = PCore.getMetadataUtils().getDataPageMetadata(referenceList); + if (sourceMetadata?.primaryFields) { + primaryFields = sourceMetadata.primaryFields; + } + // updating metaData fieldID to normal property if it has fieldID starts with !P! or !PL! + updateMetaEmbeddedFieldID(metaFields); + } + updateFieldType(metaFields); + + if (isDataObject) { + const compositeKeys: any[] = []; + const dataViewName = PCore.getDataTypeUtils().getSavableDataPage(classID); + const dataPageKeys = PCore.getDataTypeUtils().getDataPageKeys(dataViewName); + dataPageKeys?.forEach(item => (item.isAlternateKeyStorage ? compositeKeys.push(item.linkedField) : compositeKeys.push(item.keyName))); + if (compositeKeys.length && otherContext) { + otherContext.setCompositeKeys(compositeKeys); + } + if (otherContext) { + otherContext.fetchRowActionDetails = null; + } + } + + const presetArray = []; + const rawMetadata = pConn$.getRawMetadata().config; + const rawPresets = rawMetadata.presets; + const isMetaWithPresets = rawPresets && rawPresets.length !== 0; + const childrenIterator = isMetaWithPresets ? rawPresets : children; + const resolvedPresets = pConn$.getConfigProps().presets; + let fields; + let tableConfig; + childrenIterator?.forEach((presetMeta, index) => { + const { configFields } = getTableConfigFromPresetMeta( + { ...presetMeta, label: resolvedPresets[index].label }, + isMetaWithPresets, + pConn$, + classID, + primaryFields, + metaFields + ); + const pushToComponentsList = fieldType => { + listOfComponents.push(fieldType); + }; + // read report columns response - in case of nonqueryable ignore the response and rely only on the fields configured at authoing time in presets + const reportColumnsSet = isQueryable ? getReportColumns(promisesResponseArray[1]) : new Set(); + + const configFieldSet = prepareConfigFields(configFields, pushToComponentsList); + + // FIXME #EmbeddedPropertyPatch + // TODO: Remove this merge logic when the metadata response includes all optimized embedded page and pagelists + // merging configured embedded properties with metadata so that while preparing extra fields, config fields can get other properties from meta(eg: category, displayAsLink) + // get configured embedded properties metadata and get new mergedMetaFields + const configEmbeddedFieldsMeta = getConfigEmbeddedFieldsMeta(configFieldSet, classID); + const mergedMetaFields = mergeConfigEmbeddedFieldsMeta(configEmbeddedFieldsMeta, metaFields); + + const extraFields = prepareExtraFields( + mergedMetaFields, // passing new merged meta fields which has meta of configured embedded fields of current preset + configFields, + configFieldSet, + reportColumnsSet, + classID, + showDynamicFields + ); + + if (isQueryable) { + updatePageFieldsConfig(configFields, classID); + preparePropertyMaps([...configFields, ...extraFields], classID, context); + } + + const { getMappedProperty } = context; + + fields = initializeColumns([...configFields, ...extraFields], getMappedProperty); + const patchQueryFields = preparePatchQueryFields(fields, isDataObject, classID); + const itemKey = getItemKey(fields); + tableConfig = { fieldDefs: fields, patchQueryFields, itemKey, isQueryable }; + }); + const meta = tableConfig; + params.listContext = { meta, presets: presetArray, apiContext: { ...apiContext } }; + return params.listContext; +}; diff --git a/src/app/_samples/mediaco/mediaCoStyles.scss b/src/app/_samples/mediaco/mediaCoStyles.scss index 5d3dee83..a5ebffa5 100644 --- a/src/app/_samples/mediaco/mediaCoStyles.scss +++ b/src/app/_samples/mediaco/mediaCoStyles.scss @@ -2,7 +2,8 @@ // Action Buttons styles for MediaCo sample app - start .button-bar { gap: 1rem; - margin-top: 1.2rem; + margin-top: 2rem; + margin-bottom: 0.5rem; } .right-group { @@ -18,10 +19,19 @@ // Self Service Case View styles for MediaCo sample app - start .psdk-flow-container-top { background-color: #fff !important; - padding: 0.5rem 0.625rem !important; + padding: 2rem !important; border-radius: 0 !important; border-top-left-radius: 2rem !important; border-bottom: gray 0.0625rem solid !important; + + h2 { + color: #46185a; + font-weight: 700; + font-size: 36px; + padding-left: 1rem; + padding-top: 0.75rem; + margin: 0; + } } // Self Service Case View styles for MediaCo sample app - end }