import { Injectable } from '@angular/core'; import { SequenceItem } from '@core/components/sequence-modal/sequence-modal.component'; import { TranslationService } from '@core/services/translation.service'; import { ReferenceFieldAPI } from '@core/typings/api/reference-fields.typing'; import { AdHocReportingUI } from '@core/typings/ui/ad-hoc-reporting.typing'; import { ReferenceFieldsUI, STANDARD_FIELDS_CATEGORY_ID } from '@core/typings/ui/reference-fields.typing'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { FormDefinitionForUi } from '@features/configure-forms/form.typing'; import { ComponentHelperService } from '@features/formio/services/component-helper/component-helper.service'; import { UserService } from '@features/users/user.service'; import { ArrayHelpersService, BaseValidatorExtras, composeVoid, createTopLevelValidator, createValidator, CSVBooleanFactory, Description, FileService, IsDynamicType, IsNumber, IsString, Required, RequiredIfOtherHasValue, SimpleStringMap, Transform, TypeaheadSelectOption, Unique, ValidatorErrorReturn } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { NotifierService } from '@yourcause/common/notifier'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import { uniq, uniqBy } from 'lodash'; import * as parse from 'papaparse'; import { AddEditDataTableOptionModalResponse } from './add-edit-custom-option-modal/add-edit-custom-option-modal.component'; import { CustomDataTablesResources } from './custom-data-tables.resources'; import { CustomDataTablesState } from './custom-data-tables.state'; import { ConflictResolutionInfo, CustomDataTable, CustomDataTableDetailViewOption, CustomDataTableExternalContext, CustomDataTableOption, DataTableOptionTranslations, KeyValue, KeyValueFromAPI, MergeOptionsPayload, MergePicklistsPayload, PicklistConflictForUi, PicklistDataType, PicklistOptionDependentPicklist, UpdatePicklistPayload, UpdateSortOrderPayload } from './custom-data-tables.typing'; @AttachYCState(CustomDataTablesState) @Injectable({ providedIn: 'root' }) export class CustomDataTablesService extends BaseYCService { constructor ( private logger: LogService, private customDataTableResources: CustomDataTablesResources, private fileService: FileService, private notifierService: NotifierService, private i18n: I18nService, private arrayHelper: ArrayHelpersService, private translationService: TranslationService, private userService: UserService, private clientSettingsService: ClientSettingsService, private componentHelper: ComponentHelperService ) { super(); } get customDataTables () { return this.get('customDataTables'); } get pickListTypeaheadOptions (): TypeaheadSelectOption[] { const isRootZone = this.clientSettingsService.clientSettings.isRootClient; return this.customDataTables.filter((cdt) => { return isRootZone ? cdt.hasOptions : !cdt.isSystem && cdt.hasOptions; }).map((cdt) => { return { label: cdt.name, value: cdt }; }); } get pickListIdTypeaheadOptions (): TypeaheadSelectOption[] { return this.pickListTypeaheadOptions .map((cdt) => { return { label: cdt.label, value: cdt.value.id }; }); } get customDataTableMap () { return this.get('customDataTableMap'); } get allCustomDataTableMap () { return this.get('allCustomDataTableMap'); } get customDataTableOptionsMap () { return this.get('customDataTableOptionsMap'); } get guidToNameMap () { return this.get('guidToNameMap'); } getDataTypeOptions () { return [{ label: this.i18n.translate('common:textText', {}, 'Text'), value: PicklistDataType.Text }, { label: this.i18n.translate('common:textNumeric', {}, 'Numeric'), value: PicklistDataType.Numeric }]; } resetCustomDataTableOptionsMap () { this.set('customDataTableOptionsMap', {}); } setCustomDataTableOnState (data: CustomDataTable[]) { this.set('customDataTables', data); } getCDTIdFromGuid (guid: string) { const found = this.customDataTables.find((table) => { return table.guid === guid; }); return found?.id ?? null; } getCDTFromGuid (guid: string) { const found = this.customDataTables.find((table) => { return table.guid === guid; }); return found ?? null; } getCDTFromId (id: number) { const found = this.customDataTables.find((table) => { return +table.id === +id; }); return found ?? null; } getParentKeysFromOption (option: CustomDataTableOption) { const parentKeys = option.picklistOptionDependentPicklists.map((dependency: PicklistOptionDependentPicklist) => { return dependency.dependentPicklistOptionId === option.id ? dependency.parentPicklistOptionKey : null; }); return parentKeys; } getParentCDT (parentCDTId: number) { // use parentCDTId to get info for display const parentCDT = this.customDataTables.find((cdt) => { if (parentCDTId) { return cdt.id === parentCDTId; } else { return false; } }); return parentCDT; } async getAllCdtOptionsPerForm ( guids: string[], returnInactive: boolean, languageId: string, formIds: number[], clientId?: number ) { guids = uniq(guids); // check if guids all have options on map const anyGuidIsMissingOptions = guids.some((guid) => { return !this.customDataTableOptionsMap[guid]; }); if (anyGuidIsMissingOptions) { const dataTables: KeyValueFromAPI[] = await this.getBulkCustomDataTables( formIds, languageId, returnInactive, clientId ); guids.map((guid) => { const options = dataTables ? dataTables.filter((dataTable) => { return dataTable.picklistGuid === guid; }) : []; return this.setCustomDataTableOptionsMap( guid, options ); }); } } setCustomDataTableOptionsMap ( guid: string, options: KeyValueFromAPI[] ) { if (guid && !this.customDataTableOptionsMap[guid]) { this.set('customDataTableOptionsMap', { ...this.customDataTableOptionsMap, [guid]: this.arrayHelper.sortByAttributes( options, 'sortOrder', 'value' ) }); } return this.customDataTableOptionsMap[guid]; } async resetCustomDataTables () { this.setCustomDataTableOnState(undefined); await this.setCustomDataTables(); } async resetCustomDataTableMap (id: number) { // for in use (active) options this.set('customDataTableMap', { ...this.customDataTableMap, [id]: undefined }); // for both in use and not in use this.set('allCustomDataTableMap', { ...this.allCustomDataTableMap, [id]: undefined }); // these will set and reset both await this.setCustomDataTableOptions(id); const found = this.getCDTFromId(id); if (found) { this.resetCustomDataTableOptions(found.guid); } } resetCustomDataTableOptions (guid: string) { // reset in use this.set('customDataTableOptionsMap', { ...this.customDataTableOptionsMap, [guid]: undefined }); // reset in use and not in use this.set('allCustomDataTableMap', { ...this.allCustomDataTableMap, [guid]: undefined }); } async setCustomDataTables () { if (!this.customDataTables) { const data = await this.customDataTableResources.getCustomTableDataList(); this.setCustomDataTableOnState(this.arrayHelper.sort(data, 'name')); this.setGuidToNameMap(); } } setGuidToNameMap () { const map: SimpleStringMap = {}; this.customDataTables.forEach((table) => { map[table.guid] = table.name; }); this.set('guidToNameMap', map); } async setCustomDataTableOptions (id: number) { if (!this.customDataTableMap[id]) { const detail = await this.customDataTableResources.getCustomDataTableOptions(id); this.setCustomDataTableOptionsOnState(id, detail); this.setAllCustomDataTableOptionsOnState(id, detail); } } setCustomDataTableOptionsOnState (id: number, detail: CustomDataTableOption[]) { this.set('customDataTableMap', { ...this.customDataTableMap, [id]: detail.filter((item) => item.inUse) }); } setAllCustomDataTableOptionsOnState (id: number, detail: CustomDataTableOption[]) { this.set('allCustomDataTableMap', { ...this.allCustomDataTableMap, [id]: detail }); } getOptionsForCustomDataTableDetail (id: number): CustomDataTableDetailViewOption[] { const cdt = this.getCDTFromId(id); return this.allCustomDataTableMap[id].map((option) => { const found = option.values.find((opt: DataTableOptionTranslations) => { return opt.languageId === cdt.defaultLanguageId; }) || option.values[0]; const parentKeys = this.getParentKeysFromOption(option); return { id: option.id, key: option.key, value: found.text, inUse: option.inUse, createdDate: option.createdDate, sortOrder: option.sortOrder, updatedDate: option.updatedDate, parentKeys }; }); } async setCustomDataTableOptionsForPdf ( formDefinition: FormDefinitionForUi[], languageId: string, allReferenceFields: ReferenceFieldAPI.ReferenceFieldDisplayModel[], formIds: number[], clientId?: number ) { let dataTableGuids: string[] = []; formDefinition.forEach((tab) => { this.componentHelper.eachComponent(tab.components, (comp) => { if (comp.selectedCustomDataTable) { dataTableGuids.push(comp.selectedCustomDataTable); } else { if (comp.type.startsWith('referenceFields-')) { const key = comp.type.split('-')[1]; const field = allReferenceFields.find((f) => { return f.key === key; }); if (field.customDataTableGuid) { dataTableGuids.push(field.customDataTableGuid); } } } }); }); dataTableGuids = uniq(dataTableGuids); await this.getAllCdtOptionsPerForm( dataTableGuids, true, languageId, formIds, clientId ); } getTemplateForDownload ( id: number, includeParentKeysColumn = false ) { const csvOptions = this.allCustomDataTableMap[id] ? this.getExportData( id, this.userService.getCurrentUserCulture(), includeParentKeysColumn ) : []; if (csvOptions.length > 0) { const csv = parse.unparse(csvOptions); this.fileService.downloadCSV(csv); } else { // Download blank template const input = includeParentKeysColumn ? 'key,value,sortOrder,parentKeys,inactive' : 'key,value,sortOrder,inactive'; return this.fileService.downloadString( input, 'text/csv', 'template.csv' ); } } isFileValid (parsed: KeyValue[]) { parsed = this.removeEmptyRows(parsed); if (parsed && parsed.length > 0) { const uniqVals = uniqBy(parsed, 'key'); const duplicates = uniqVals.length !== parsed.length; const errors = parsed.filter((result) => { if (!result.value || !result.key) { return this.i18n.translate( 'FORMS:textExtraHeaders', {}, 'Missing key or value' ); } return null; }); return errors.length === 0 && !duplicates; } return false; } removeEmptyRows (parsed: KeyValue[]) { return parsed.filter((item) => { return !!item.key || !!item.value; }); } async createTable ( name: string, defaultLanguageId: string, dataType: PicklistDataType, skipSuccessNotifier = false, parentPicklistId?: number ) { try { const id = await this.customDataTableResources.addCustomTable( name, defaultLanguageId, parentPicklistId, dataType ); if (!skipSuccessNotifier) { this.notifierService.success(this.i18n.translate( 'FORMS:textSuccessfullyCreatedCustomDataTable', {}, 'Successfully created the custom data table' )); } return id; } catch (e) { this.logger.error(e); this.notifierService.error(this.i18n.translate( 'FORMS:textErrorCreatingCustomDataTable', {}, 'There was an error creating the custom data table' )); } return null; } async importData (id: number, file: Blob, importOnly = false) { try { await this.customDataTableResources.uploadCsvList(id, file); await this.resetCustomDataTables(); await this.resetCustomDataTableMap(id); if (importOnly) { this.notifierService.success(this.i18n.translate( 'common:textSuccessfullyImportedSelectedFile2', {}, 'Successfully imported the selected file' )); } else { this.notifierService.success(this.i18n.translate( 'FORMS:textSuccessfullyCreatedCustomDataTableAndImported', {}, 'Successfully created the table and imported the selected file' )); } return id; } catch (e) { this.logger.error(e); this.notifierService.error(this.i18n.translate( 'common:textErrorImportingSelectedFile', {}, 'There was an error importing the selected file' )); return null; } } async createAndImportData ( name: string, file: Blob, defaultLanguageId: string, dataType: PicklistDataType, parentPicklistId?: number ) { const id = await this.createTable( name, defaultLanguageId, dataType, true, parentPicklistId ); if (id) { return this.importData( id, file ); } else { return null; } } async updatePicklist ( payload: UpdatePicklistPayload, skipSuccessNotifier = false ) { try { await this.customDataTableResources.updatePicklist(payload); await this.resetCustomDataTables(); await this.resetCustomDataTableMap(payload.id); if (!skipSuccessNotifier) { this.notifierService.success(this.i18n.translate( 'FORMS:textSuccessfullyUpdatedPicklist', {}, 'Successfully updated picklist' )); } } catch (e) { this.logger.error(e); this.notifierService.error( this.i18n.translate( 'FORMS:textErrorUpdatingPicklist', {}, 'There was an error updating the picklist' ) ); } } getExportData ( id: number, languageId: string, hasParent: boolean ) { const options = this.allCustomDataTableMap[id]; return options.map((opt) => { const value = opt.values.find((val) => { return val.languageId === languageId; }).text; const returnVal = { key: opt.key, value, sortOrder: opt.sortOrder, inactive: !opt.inUse }; if (hasParent) { return { ...returnVal, parentKeys: this.getParentKeysFromOption(opt) }; } return returnVal; }); } async exportData ( id: number, languageId: string, hasParent: boolean ) { await this.setCustomDataTableOptions(id); const csvOptions = this.getExportData( id, languageId, hasParent ); const csv = parse.unparse(csvOptions); this.fileService.downloadCSV(csv); } async getDataTableOptions (): Promise { if (!this.customDataTables) { await this.setCustomDataTables(); } return this.arrayHelper.sort(this.customDataTables.map((table) => { return { label: table.name, value: table.guid }; }), 'label'); } getMostCommonDefaultLangFromArray ( guids: string[] ) { return this.translationService.getMostCommonDefaultLangFromArray( this.customDataTables.map((table) => { return { defaultLanguageId: table.defaultLanguageId, id: table.guid }; }), guids ); } async returnExternalContextForCDTValidator ( parentDataTableGuid: string, defaultLang: string, requiresParentListValidation: boolean, picklistId?: number // if exists ): Promise { let parentListKeys = null; if (requiresParentListValidation) { const parentCDT = this.customDataTableOptionsMap[parentDataTableGuid]; if (!parentCDT) { await this.setCustomDataTableOptionsFromGuid(parentDataTableGuid, true, defaultLang); } parentListKeys = this.customDataTableOptionsMap[parentDataTableGuid] .map((option) => { return option.key; }); } const dataTable = this.customDataTables?.find((table) => { return table.id === picklistId; }); const dynamicType = dataTable?.dataType === PicklistDataType.Numeric ? 'number' : 'string'; return { requiresParentListValidation, parentListKeys, dynamicType, picklistId }; } async setOptionsListFromCategoryMap ( categoryMap: SimpleStringMap, recordIds: number[], rootObject: AdHocReportingUI.RootObject ) { const guids: string[] = []; Object.keys(categoryMap).forEach((key) => { categoryMap[key].forEach((field) => { if ( field.customDataTableGuid && !guids.includes(field.customDataTableGuid) && key !== STANDARD_FIELDS_CATEGORY_ID ) { guids.push(field.customDataTableGuid); } }); }); if (rootObject.property === 'table') { return this.setAllCdtOptionsOnForm( guids, true, this.userService.getCurrentUserCulture() ); } else { return this.getAllCdtOptionsPerForm( guids, true, this.userService.getCurrentUserCulture(), recordIds ); } } async setAllCdtOptionsOnForm ( guids: string[], returnInactive = false, languageId: string ) { guids = uniq(guids); return Promise.all(guids.map((guid) => { return this.setCustomDataTableOptionsFromGuid( guid, returnInactive, languageId ); })); } async setCustomDataTableOptionsFromGuid ( guid: string, returnInactive: boolean, languageId: string ) { if (guid && !this.customDataTableOptionsMap[guid]) { const options = await this.customDataTableResources.getKeyValuesByGuid( guid, returnInactive, languageId ); this.set('customDataTableOptionsMap', { ...this.customDataTableOptionsMap, [guid]: this.arrayHelper.sortByAttributes( options, 'sortOrder', 'value' ) }); } return this.customDataTableOptionsMap[guid]; } async prepParentCDTData ( picklistId: number, parentDataTableId: number ): Promise { let parentDataTable: CustomDataTable; if (picklistId && parentDataTableId) { await this.setCustomDataTableOptions(picklistId); parentDataTable = this.getParentCDT(parentDataTableId); } return parentDataTable; } async handleDeleteDataTable ( picklistId: number ): Promise { try { const response = await this.customDataTableResources.handleDeleteDataTable(picklistId); await this.resetCustomDataTables(); this.notifierService.success( this.i18n.translate( 'common:textSuccessfullyDeletedDataTable', {}, 'Successfully deleted data table' ) ); return response; } catch (e) { this.logger.error(e); this.notifierService.error( this.i18n.translate( 'common:textErrorDeletingDataTable', {}, 'There was an error deleting the data table' ) ); } } async handleUpdateDataTableName ( id: number, name: string, defaultLanguageId: string, parentPicklistId: number ) { try { await this.customDataTableResources.updateCustomTable( id, name, defaultLanguageId, parentPicklistId ); await this.resetCustomDataTables(); await this.resetCustomDataTableMap(id); this.notifierService.success(this.i18n.translate( 'GLOBAL:textSucessfullyUpdatedCustomDataTableName', {}, 'Successfully updated the custom data table name' )); } catch (e) { this.logger.error(e); this.notifierService.error(this.i18n.translate( 'GLOBAL:textErrorUpdatingCustomDataTableName', {}, 'There was an error updating the custom data table name' )); } } handleSortOrder (options: PicklistOptionImport[]) { const hasAtLeastOneSortOrder = options.some(option => typeof(option.sortOrder) === 'number'); if (!hasAtLeastOneSortOrder) { return options.map((option, index) => { return { ...option, sortOrder: index + 1 }; }); } return options; } /** * Saves the modal response after adding or editing a CDT option * * @param picklistId: CDT ID * @param sortOrderBeforeSubmit: the order before submitting * @param existingSortOrders: existing sort orders array * @param modalResponse: response from the modal AddEditCustomOptionModalComponent * @param option: if edit, this is the option they are editing * @param defaultLanguageId: default lang of the picklist */ async handleAddOrEditOption ( picklistId: number, sortOrderBeforeSubmit: number, existingSortOrders: number[], modalResponse: AddEditDataTableOptionModalResponse, option: CustomDataTableDetailViewOption, defaultLanguageId: string ) { try { const needsAdjusted = existingSortOrders.includes(modalResponse.sortOrder); let optionId: number; if (!!option) { optionId = option.id; await this.customDataTableResources.updateOptionValue( picklistId, option.id, modalResponse.value, defaultLanguageId, needsAdjusted ? sortOrderBeforeSubmit : modalResponse.sortOrder, modalResponse.parentKeys ); } else { const result = await this.customDataTableResources.addDataTableOption( picklistId, { ...modalResponse, sortOrder: needsAdjusted ? sortOrderBeforeSubmit : modalResponse.sortOrder } ); optionId = result; } await this.resetCustomDataTableMap(picklistId); if (needsAdjusted) { await this.handleAdjustSortOrder(picklistId, optionId, modalResponse.sortOrder); await this.resetCustomDataTableMap(picklistId); } this.notifierService.success(this.i18n.translate( !option ? 'GLOBAL:textSuccessAddingCDTOption' : 'FORMS:textSuccessfullyUpdatedValue', {}, !option ? 'Successfully added the data table option' : 'Successfully updated value' )); } catch (e) { this.logger.error(e); this.notifierService.error(this.i18n.translate( !option ? 'GLOBAL:textErrorAddingCDTOption' : 'FORMS:textErrorUpdatingValue', {}, !option ? 'There was an error adding the data table option' : 'There was an error updating the value' )); } } /** * Adjusts the sort order and saves * * @param picklistId: the CDT ID * @param optionId: the Picklist Option ID where sort order is updated * @param sortOrder: the new sort order */ async handleAdjustSortOrder ( picklistId: number, optionId: number, sortOrder: number ) { const options = this.getOptionsForCustomDataTableDetail(picklistId); const updatedOption = options.find((opt) => { return opt.id === optionId; }); updatedOption.sortOrder = sortOrder; const listWithoutUpdatedItem = options.filter((option) => { return option.id !== optionId; }); listWithoutUpdatedItem.splice(updatedOption.sortOrder - 1, 0, updatedOption); const updatedList = listWithoutUpdatedItem.map((item, index) => { return { ...item, sortOrder: index + 1 }; }); const payload: UpdateSortOrderPayload = { picklistId, picklistOptionsWithSortOrder: updatedList.map((item) => { return { picklistOptionId: item.id, sortOrder: item.sortOrder }; }) }; await this.customDataTableResources.updateSortOrder(payload); } async deactivatePicklistOption (id: number) { try { await this.customDataTableResources.deactivatePicklistOption(id); this.notifierService.success(this.i18n.translate( 'GLOBAL:textSuccessDeactivatingPicklistOption', {}, 'Successfully deactivated picklist option' )); } catch (e) { this.logger.error(e); this.notifierService.error(this.i18n.translate( 'GLOVAL:textErrorDeactivatingPicklistOption', {}, 'There was an error deactivating the picklist option' )); } } async activatePicklistOption (id: number) { try { await this.customDataTableResources.activatePicklistOption(id); this.notifierService.success(this.i18n.translate( 'GLOBAL:textSuccessActivatingPicklistOption', {}, 'Successfully activated picklist option' )); } catch (e) { this.logger.error(e); this.notifierService.error(this.i18n.translate( 'GLOVAL:textErrorActivatingPicklistOption', {}, 'There was an error activating the picklist option' )); } } /** * * @param tableColumns Table columns to use in setting CDTs * @param editingRow The particular row we are getting cdt items map for * @param returnAllItems to skip filtering and get a full list of options, set to true */ getCdtItemsMapForRow ( tableColumns: (ReferenceFieldsUI.TableFieldForUi|ReferenceFieldsUI.DataPointForUI)[], editingRow?: ReferenceFieldsUI.TableResponseRowForUiMapped|ReferenceFieldsUI.TableResponseRowForUi, returnAllItems = false ) { const cdtItemsMap: Record = {}; const cdtFields: { referenceFieldId: number; guid: string; }[] = []; tableColumns.forEach((column) => { const guid = column.referenceField?.customDataTableGuid; if (guid) { cdtFields.push({ referenceFieldId: column.referenceFieldId, guid }); } }); uniq(cdtFields).forEach((field) => { const thisCol = editingRow?.columns.find((col) => { return col.referenceFieldId === field.referenceFieldId; }); cdtItemsMap[ field.referenceFieldId ] = this.getTypeaheadOptionsForCdt( field.guid, (thisCol?.value ?? '') as string, false, undefined, returnAllItems ); }); return cdtItemsMap; } async getBulkCustomDataTables ( formIds: number[], languageId: string, returnInactive: boolean, clientId?: number ) { try { const response = await this.customDataTableResources.getBulkCustomDataTables( formIds, languageId, returnInactive, clientId ); return response; } catch (e) { console.error(e); this.notifierService.error( this.i18n.translate( 'GLOBAL:textErrorFetchingPicklistOptions', {}, 'There was an error fetching picklist options' ) ); return null; } } /** * * @param guid guid of the cdt * @param currentVal current val of the input * @param supportsMultiple does the field support multiple? * @param parentMapVal the value from reference fields parentPicklistValueMap * @param returnAllItems to skip filtering and get a full list of options, set to true */ getTypeaheadOptionsForCdt ( guid: string, currentVal: string|string[], supportsMultiple: boolean, parentMapVal?: string|string[], returnAllItems = false ): TypeaheadSelectOption[] { const options = this.customDataTableOptionsMap[guid]; const filteredOptions = returnAllItems ? options : this.getFilteredOptions( options, currentVal, supportsMultiple, parentMapVal ); if (options) { const validOptions = options.filter((option) => { return filteredOptions ? filteredOptions.some((fo) => fo.key === option.key) : true; }); return this.arrayHelper.sortByAttributes( validOptions, 'sortOrder', 'value' ).map((option) => { return { label: option.value, display: option.value, value: option.key }; }); } return []; } getFilteredOptions ( allOptions: KeyValue[], currentVal: string|string[], supportsMultiple: boolean, parentMapVal?: string|string[] ) { return (allOptions || []).filter((option) => { // Check if the picklist option is no longer in use // We keep it if they answered it previously if (!option.inUse) { const hasInactiveAnswer = supportsMultiple ? currentVal?.includes(option.key) : currentVal === option.key; if (!hasInactiveAnswer) { return false; } } const isDependencyLengthZero = option?.parentKeys.length === 0; if (parentMapVal) { let isValidChildOption = false; if (parentMapVal instanceof Array) { if (parentMapVal.length === 0) { isValidChildOption = false; } else { isValidChildOption = parentMapVal.some((val) => { return option.parentKeys.includes(val); }); } } else { isValidChildOption = option.parentKeys.includes(parentMapVal); } return isValidChildOption || isDependencyLengthZero; } return isDependencyLengthZero; }); } async handleCopyTable ( picklistId: number, newName: string ) { try { const id = await this.customDataTableResources.copyCustomDataTable( picklistId, newName ); await this.resetCustomDataTables(); this.notifierService.success(this.i18n.translate( 'common:textSuccessCopyCdt', {}, 'Successfully copied the custom data table' )); return id; } catch (e) { this.logger.error(e); this.notifierService.error(this.i18n.translate( 'common:textErrorCopyCdt', {}, 'There was an error copying the custom data table' )); return null; } } async checkForDuplicateOptionValues ( picklistId: number ): Promise { const cdt = this.getCDTFromId(picklistId); const options = await this.setCustomDataTableOptionsFromGuid( cdt.guid, true, this.userService.getCurrentUserCulture() ); const values: string[] = []; let hasDuplicates = false; options.forEach((option) => { if (values.includes(option.value)) { hasDuplicates = true; } else { values.push(option.value); } }); return hasDuplicates; } async getMergeConflictsForPicklists ( cdt1: number, cdt2: number ): Promise<{ numberOfConflicts: number; results: PicklistConflictForUi[]; }> { try { const result = await this.customDataTableResources.getMergeConflictsForPicklists( cdt1, cdt2, this.userService.getCurrentUserCulture() ); return { numberOfConflicts: result.conflictResolutionRequiredCount, results: Object.keys(result.conflictInfo).map((key) => { return { id: key, ...result.conflictInfo[key] }; }) }; } catch (e) { this.logger.error(e); this.notifierService.error(this.i18n.translate( 'common:textErrorPreparingMergeInfo', {}, 'There was an error preparing the merge information' )); return null; } } getOptionsForKeyOrValueToResolveMerge ( result: PicklistConflictForUi ): TypeaheadSelectOption[] { /* If key is the same, they must pick the value to use, and vice versa */ if (result.picklistOptionWithSameKey) { return [{ label: result.picklistOption.value, value: result.picklistOption.id }, { label: result.picklistOptionWithSameKey.value, value: result.picklistOptionWithSameKey.id }]; } else if (result.picklistOptionWithSameValue) { return [{ label: result.picklistOption.key, value: result.picklistOption.id }, { label: result.picklistOptionWithSameValue.key, value: result.picklistOptionWithSameValue.id }]; } return []; } adaptModalInfoForMergePayload ( newPicklistName: string, picklistToKeepId: number, picklistToMergeId: number, conflictResolutionsMap: Record ): MergePicklistsPayload { const conflictResolutions: Record = {}; Object.keys(conflictResolutionsMap).forEach((key) => { conflictResolutions[key] = conflictResolutionsMap[key].picklistOptionId; }); return { newPicklistName, picklistToKeepId, picklistToMergeId, conflictResolutions }; } async mergePicklists ( payload: MergePicklistsPayload ) { try { await this.customDataTableResources.mergePicklists( payload ); await this.resetCustomDataTables(); await this.resetCustomDataTableMap(payload.picklistToKeepId); this.notifierService.success(this.i18n.translate( 'common:textSuccessMergePicklists', {}, 'Successfully merged the picklists' )); } catch (e) { this.logger.error(e); this.notifierService.error(this.i18n.translate( 'common:textErrorMergingPicklists', {}, 'There was an error merging the picklists' )); } } async mergeOptions ( payload: MergeOptionsPayload ) { try { await this.customDataTableResources.mergeOptions( payload ); this.notifierService.success(this.i18n.translate( 'common:textSuccessMergeOptions', {}, 'Successfully merged the options' )); } catch (e) { this.logger.error(e); this.notifierService.error(this.i18n.translate( 'common:textErrorMergingOptions', {}, 'There was an error merging the options' )); } } async updateSortOrder ( picklistId: number, items: SequenceItem[] ) { try { const payload: UpdateSortOrderPayload = { picklistId, picklistOptionsWithSortOrder: items.map((item) => { return { picklistOptionId: item.id, sortOrder: item.sequence }; }) }; await this.customDataTableResources.updateSortOrder(payload); this.notifierService.success(this.i18n.translate( 'common:textSuccessUpdateSortOrder', {}, 'Successfully updated the sort order' )); } catch (e) { this.logger.error(e); this.notifierService.error(this.i18n.translate( 'common:textErrorUpdatingSortOrder', {}, 'There was an error updating the sort order' )); } } getRequiredPicklistOptionsMap (id: number) { const options = this.getOptionsForCustomDataTableDetail( id ); const requiredPicklistOptionsMap: Record = {}; options.forEach((option) => { requiredPicklistOptionsMap[option.key] = option.value; }); return requiredPicklistOptionsMap; } async validateRequiredRows ( id: number, contents: PicklistOptionImport[]|DependentPicklistOptionImport[] ): Promise { if (id) { await this.setCustomDataTableOptions(id); const mappedOptions: Record = {}; contents.forEach((option) => { mappedOptions[option.key] = option; }); const missingKeys: string[] = []; const requiredPicklistOptionsMap = this.getRequiredPicklistOptionsMap(id); Object.keys(requiredPicklistOptionsMap).forEach((key) => { if (!mappedOptions[key]) { missingKeys.push(key); } }); if (missingKeys.length > 0) { let errorMessage = this.i18n.translate( 'common:textAllExistingOptionsMustBeImportedAlert2', {}, 'All existing options must be included in the import. The following key(s) are missing. If you no longer wish for these options to be active, just set inactive to "true".' ) + `
    `; missingKeys.forEach((key) => { errorMessage = errorMessage + '
  • ' + key + '
  • '; }); errorMessage = errorMessage + '
'; return [{ i18nKey: '', defaultValue: errorMessage }]; } } return []; } } export const MatchesExistingParentKey = createValidator( () => (keys: string, { externalContext }) => { // adapt values here to validate, filtering out empty entried which are valid by default const keysArray = (keys || '').split(',').filter((_key) => !!_key).map((a: string) => a.trim()); // cast typing here since externalContext is any const exContext = externalContext as CustomDataTableExternalContext; // ignore validator if not requiring parent keysArray const notUsingParentList = !exContext.requiresParentListValidation; const valid = notUsingParentList ? true : keysArray.every((key) => { return exContext.parentListKeys.includes(key); }); return valid ? [] : { i18nKey: 'GLOBAL:textParentPicklistKeyMustExist', defaultValue: 'Parent picklist key must exist' }; }, { ruleText: { i18nKey: 'common:textMustMatchExistingParentKeyOnParentDataTable', defaultValue: 'Must match existing parent key on selected parent custom data table' } } ); const CannotContainCommas = createValidator( () => (key: string) => { return key.includes(',') ? { i18nKey: 'common:textKeyCannotContainComma', defaultValue: 'The key cannot contain a comma' } : []; }, { ruleText: { i18nKey: 'common:textCannotContainCommas', defaultValue: 'Cannot contain commas' } } ); const KeyValidator = composeVoid([ CannotContainCommas(), Required(), Unique(true), IsDynamicType({ evaluateAsString: true }, { ruleText: { i18nKey: 'common:textMustBeAString', defaultValue: 'Must be a string' } }), Transform(val => '' + val) ]); export class DependentPicklistOptionImport implements PicklistOptionImport { @KeyValidator() @Description({ i18nKey: 'common:textUniqueIdentifierForValue', defaultValue: 'Unique identifier for your value' }) 'key': string; @Required() @IsString() @Transform((val) => '' + val) @Description({ i18nKey: 'common:textSelectableDataDisplayed', defaultValue: 'Selectable data displayed to users' }) 'value': string; @MatchesExistingParentKey() @Transform((val) => (val || '').split(',').map((a: string) => a.trim()).join(',')) @Description({ i18nKey: 'common:textParentKeysDescription', defaultValue: 'Only available if a parent data table was selected. If this parent key\'s value is selected then the child value is available.' }) 'parentKeys': string; @IsNumber({ min: 1 }) @Unique(true) @RequiredIfOtherHasValue() @Description({ i18nKey: 'common:textSortOrderDescription', defaultValue: 'Position of the value when displayed to a user' }) 'sortOrder': number; @CSVBooleanFactory(false)() @Description({ i18nKey: 'common:textInactiveDescription', defaultValue: 'Determines if the value will be available to users for selection' }) 'inactive': boolean; } const ValidateRequiredRows = createTopLevelValidator((_) => (records: PicklistOptionImport[], extras: BaseValidatorExtras) => { const { injector, externalContext } = extras; const customDataTablesService = injector.get(CustomDataTablesService); return customDataTablesService.validateRequiredRows( externalContext.picklistId, records ); }); @ValidateRequiredRows() export class PicklistOptionImport { @KeyValidator() @Description({ i18nKey: 'common:textUniqueIdentifierForValue', defaultValue: 'Unique identifier for your value' }) 'key': string; @Required() @IsString() @Transform((val) => '' + val) @Description({ i18nKey: 'common:textSelectableDataDisplayed', defaultValue: 'Selectable data displayed to users' }) 'value': string; @IsNumber({ min: 1 }) @Unique(true) @RequiredIfOtherHasValue() @Description({ i18nKey: 'common:textSortOrderDescription', defaultValue: 'Position of the value when displayed to a user' }) 'sortOrder': number; @CSVBooleanFactory(false)() @Description({ i18nKey: 'common:textInactiveDescription', defaultValue: 'Determines if the value will be available to users for selection' }) 'inactive': boolean; }