import { Component, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { SpinnerService } from '@core/services/spinner.service'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { BasicForm, BasicFormForUi, FormStates } from '@features/configure-forms/form.typing'; import { FormsService } from '@features/configure-forms/services/forms/forms.service'; import { FormBuilderService } from '@features/formio/form-builder/services/form-builder/form-builder.service'; import { FormLogicService } from '@features/formio/services/form-logic/form-logic.service'; import { StandardProductConfigurationService } from '@features/platform-admin/standard-product-configuration/standard-product-configuration.service'; import { StandardProductTypes } from '@features/platform-admin/standard-product-configuration/standard-product-configuration.typing'; import { ProgramService } from '@features/programs/program.service'; import { ALL_SKIP_FILTER, ArrayHelpersService, BulkAction, SimpleStringMap, TopLevelFilter, TopLevelFilterOption, TopLevelFilterOptionsConfig, TypeaheadSelectOption } from '@yourcause/common'; import { CachedAttr, CACHE_TYPES } from '@yourcause/common/cache'; import { I18nService } from '@yourcause/common/i18n'; import { ConfirmationModalComponent, ModalFactory } from '@yourcause/common/modals'; import { Subscription } from 'rxjs'; import { CreateEditFormModalComponent } from '../create-edit-form-modal/create-edit-form-modal.component'; import { EditConfirmModalResponse, EditFormConfirmModalComponent } from '../edit-form-confirm-modal/edit-form-confirm-modal.component'; import { FormsResolver } from '../resolvers/forms.resolver'; @Component({ templateUrl: './base-forms-page.component.html', styleUrls: ['./base-forms-page.component.scss'] }) export class BaseFormsPageComponent implements OnInit, OnDestroy { sub = new Subscription(); forms: BasicFormForUi[] = []; formTypeOptions = this.formService.getFormTypeOptions(); drilldownMap: SimpleStringMap = {}; grantProgramIdOptions: TypeaheadSelectOption[] = []; formStatusOptions: TopLevelFilterOption[] = [{ display: this.i18n.translate( 'CONFIG:textAllForms', {}, 'All forms' ), value: ALL_SKIP_FILTER }, { display: this.i18n.translate( 'common:textDraft', {}, 'Draft' ), value: FormStates.DRAFT }, { display: this.i18n.translate( 'common:textPublished', {}, 'Published' ), value: FormStates.PUBLISHED }]; grantProgramIds: string[] = []; FormStates = FormStates; bulkActions: BulkAction[] = [{ disabled: (forms: BasicFormForUi[]) => { return forms.some((form) => { return form.isStandardFormTemplate; }); }, label: this.i18n.translate( 'common:linkExportForms', {}, 'Export form(s)' ), exec: (forms: BasicFormForUi[]) => this.exportForms(forms) }, this.isRootZone ? { disabled: (forms: BasicFormForUi[]) => { return forms.some((form) => { return form.standardComponentIsPublished || form.state === FormStates.DRAFT; }); }, label: this.i18n.translate( 'GLOBAL:textPublishToUAT', {}, 'Publish to UAT' ), exec: (forms: BasicFormForUi[]) => { return this.publishOrUnpublishToUAT(forms); } } : undefined].filter((item) => !!item); topLevelFilters: TopLevelFilter[] = []; formTypeMap: { [x: string]: string; } = {}; @CachedAttr(CACHE_TYPES.LOCALSTORAGE, ALL_SKIP_FILTER) formStatusOption: string; constructor ( private modalFactory: ModalFactory, private i18n: I18nService, private formService: FormsService, private formsResolver: FormsResolver, private programService: ProgramService, private router: Router, private spinnerService: SpinnerService, private arrayHelper: ArrayHelpersService, private clientSettingsService: ClientSettingsService, private standardProductService: StandardProductConfigurationService, private formBuilderService: FormBuilderService, private formLogicService: FormLogicService ) { this.sub.add( this.formService.changesTo$('forms').subscribe(() => { this.setForms(); }) ); this.formService.formTypes.forEach((type) => { this.formTypeMap['' + type.id] = this.formService.translateFormTypes( type.id ); }); } get isRootZone () { return this.clientSettingsService.clientSettings.isRootClient; } ngOnInit () { this.topLevelFilters = this.returnTopLevelFilters(); } returnTopLevelFilters () { const grantProgramFilterConfig: TopLevelFilterOptionsConfig = this.returnGrantProgramIdOptions(); const showStandardFilter = !this.isRootZone && this.standardProductService.standardFormTemplates.length > 0; return [ new TopLevelFilter( 'text', 'name', '', this.i18n.translate('common:textSearchByName', {}, 'Search by name') ), new TopLevelFilter( 'typeaheadSingleEquals', 'state', this.formStatusOption, '', { selectOptions: [ ...this.formStatusOptions, showStandardFilter ? { display: this.i18n.translate( 'common:textStandardProductTemplates', {}, 'Standard product templates' ), value: true, customColumn: 'isStandardFormTemplate' } : undefined ].filter((item) => !!item) }, this.i18n.translate('CONFIG:lblFormStatusOptions') ), new TopLevelFilter( 'checkboxDropdown', 'programIds', this.grantProgramIds, this.i18n.translate( 'common:textSearchByProgramName', {}, 'Search by program name' ), grantProgramFilterConfig, undefined, undefined, true ) ].filter((item) => !!item); } setForms () { this.forms = this.standardProductService.getFormsAndStandardTemplates(); } onTopLevelFilterChange (filter: TopLevelFilter) { if (filter.column === 'state') { this.formStatusOption = filter.value; } } returnGrantProgramIdOptions () { const programOptions = this.programService.allPrograms.map((program) => { return { label: program.grantProgramName, value: program.grantProgramId }; }); const adaptedOptions = this.arrayHelper.sort(programOptions, 'label'); const response: TopLevelFilterOptionsConfig = { selectOptions: adaptedOptions, filterObjectName: this.i18n.translate( 'common:lblProgram', {}, 'Program' ).toLowerCase(), filterObjectNamePlural: this.i18n.translate( 'GLOBAL:textPrograms', {}, 'Programs' ).toLowerCase() }; return response; } editForm (formId: number, state: FormStates, revisionNumber: number) { const form = this.formService.getFormFromId(formId); if (state === FormStates.PUBLISHED && this.forms.some((formDraft) => formDraft.formId === formId && formDraft.state === FormStates.DRAFT)) { this.editPublishedForm(form, revisionNumber); } else { const route = `/management/program-setup/forms/${formId}${ state === FormStates.PUBLISHED ? '/published' : '/draft' }`; this.router.navigateByUrl(route); } } async editPublishedForm (form: BasicForm, revisionNumber: number) { const response = await this.modalFactory.open( EditFormConfirmModalComponent, { formName: form.name, formRevisionNumber: revisionNumber }); if (response === EditConfirmModalResponse.Edit_Exising) { const route = `/management/program-setup/forms/${form.formId}/published`; this.router.navigateByUrl(route); } else if (response === EditConfirmModalResponse.Edit_Latest) { const route = `/management/program-setup/forms/${form.formId}/draft`; this.router.navigateByUrl(route); } } async exportForms (forms: BasicFormForUi[]) { this.spinnerService.startSpinner(); const exportFormsPayload = forms.map((form) => { return { formId: form.formId, revisionId: form.revisionId }; }); // If a custom data reference field exists, we need to update picklistGuids so it gets included in export (this is to account for existing forms that did not save this data. It now saves it, but we need to make sure old forms export correctly) await this.formLogicService.ensureReferenceFieldPicklistsAreSaved(exportFormsPayload); await this.formService.exportForms(exportFormsPayload); this.spinnerService.stopSpinner(); } async copy (form: BasicFormForUi) { this.spinnerService.startSpinner(); const result = await this.formLogicService.copyForm(form); await this.reloadForms(); this.spinnerService.stopSpinner(); if (result.id) { this.editForm(result.id, FormStates.DRAFT, form.revisionVersion); } } async publish (form: BasicFormForUi) { const proceed = await this.formService.handlePublishForm(form.formId, form.revisionId, form.name); if (proceed) { this.spinnerService.startSpinner(); await this.formService.refreshForms(); this.drilldownMap = { ...this.drilldownMap, [form.revisionId]: undefined }; await this.onDrilldown({ ...form, state: FormStates.PUBLISHED }); this.spinnerService.stopSpinner(); } } async remove (form: BasicFormForUi, isRevision = false) { const proceed = await this.modalFactory.open( ConfirmationModalComponent, { modalHeader: this.i18n.translate( isRevision ? 'FORMS:hdrConfirmRemoveRevision' : 'FORMS:hdrConfirmRemoveForm', {}, isRevision ? 'Remove Revision' : 'Remove Form' ), modalSubHeader: form.name, confirmText: this.i18n.translate( isRevision ? 'FORMS:textAreYouSureRemoveThisRevision' : 'FORMS:textAreYouSureRemoveThisFormAndRevisions', {}, isRevision ? 'Are you sure you want to remove this revision? This action cannot be undone.' : `Are you sure you want to remove this form and all it's versions? This action cannot be undone.` ), confirmButtonText: this.i18n.translate( 'common:textRemove', {}, 'Remove' ) } ); if (proceed) { this.spinnerService.startSpinner(); await this.formService.handleRemoveFormOrRevision( form.formId, form.revisionId, isRevision ); this.spinnerService.stopSpinner(); } } async reloadForms () { this.formService.setLoaded(false); await this.formsResolver.resolve(); } async onDrilldown (row: BasicFormForUi) { if (!this.drilldownMap[row.revisionId]) { this.drilldownMap = { ...this.drilldownMap, [row.revisionId]: await this.formService.getProgramsRelatedToForm(row.formId) }; } } async publishOrUnpublishToUAT (rows: BasicFormForUi[]) { const toUnpublish = rows.every((row) => row.standardComponentIsPublished); const actionText = this.i18n.translate( toUnpublish ? 'GLOBAL:textUnpublish' : 'GLOBAL:textPublish', {}, toUnpublish ? 'Unpublish' : 'Publish' ); let confirmText = this.i18n.translate( toUnpublish ? 'common:textConfirmUnpublishForm' : 'common:textConfirmPublishForm', {}, toUnpublish ? 'Are you sure you want to unpublish this form?' : 'Are you sure you want to publish this form?' ); if (rows.length > 1) { confirmText = this.i18n.translate( 'common:textConfirmPublishForms2', {}, 'Are you sure you want to publish the forms?' ); } const response = await this.modalFactory.open( ConfirmationModalComponent, { modalHeader: actionText, modalSubHeader: rows.length === 1 ? rows[0].name : '', confirmText, confirmButtonText: actionText } ); if (response) { this.spinnerService.startSpinner(); if (toUnpublish) { await this.standardProductService.handleUnpublishStandardComponents( rows[0].formId, StandardProductTypes.FORMS ); } else { await this.standardProductService.handlePublishStandardComponents( rows.map((row) => row.formId), StandardProductTypes.FORMS ); } this.spinnerService.stopSpinner(); } } async createFromTemplate (row: BasicFormForUi) { const response = await this.modalFactory.open( CreateEditFormModalComponent, { startFromTemplate: { name: row.name, formId: row.formId, revisionId: row.revisionId, defaultFormLang: row.defaultLanguageId, description: row.description } } ); if (response) { this.spinnerService.startSpinner(); const route = await this.formLogicService.handleCreateForm(response.formGroup); await this.formService.refreshForms(); this.spinnerService.stopSpinner(); if (route) { if (response.openQuickAddModal) { this.formBuilderService.setOpenQuickAddModal(true); } this.router.navigate([route]); } } } ngOnDestroy () { this.sub.unsubscribe(); } }