superdesk/superdesk-client-core

View on GitHub
scripts/apps/vocabularies/controllers/VocabularyEditController.tsx

Summary

Maintainability
D
1 day
Test Coverage
import ReactDOM from 'react-dom';
import React from 'react';
import _ from 'lodash';
import {IVocabularySelectionTypes, getVocabularySelectionTypes, getMediaTypeKeys, getMediaTypes} from '../constants';
import {gettext} from 'core/utils';
import {getFields} from 'apps/fields';
import {IVocabulary} from 'superdesk-api';
import {IScope as IScopeConfigController} from './VocabularyConfigController';
import {VocabularyItemsViewEdit} from '../components/VocabularyItemsViewEdit';
import {defaultAllowedWorkflows} from 'apps/relations/services/RelationsService';

VocabularyEditController.$inject = [
    '$scope',
    'notify',
    'api',
    'metadata',
    'cvSchema',
    'relationsService',
    '$timeout',
    '$element',
];

interface IScope extends IScopeConfigController {
    setFormDirty: () => void;
    newItemTemplate: any;
    idRegex: string;
    vocabulary: IVocabulary;
    selectionTypes: IVocabularySelectionTypes;
    closeVocabulary: () => void;
    updateVocabulary: (result: any) => void;
    issues: Array<any> | null;
    _errorUniqueness: boolean;
    errorMessage: string;
    save: () => void;
    requireAllowedTypesSelection: () => void;
    addItem: () => void;
    cancel: () => void;
    model: any;
    schema: any;
    schemaFields: Array<any>;
    itemsValidation: { valid: boolean };
    customFieldTypes: Array<{id: string, label: string}>;
    setCustomFieldConfig: (config: any) => void;
    editForm: any;
    tab: 'general' | 'items';
    setTab: (tab: IScope['tab']) => void;
}

const idRegex = '^[a-zA-Z0-9-_]+$';

export function VocabularyEditController(
    $scope: IScope, notify, api, metadata, cvSchema, relationsService, $timeout,
) {
    let componentRef: VocabularyItemsViewEdit = null;

    var origVocabulary = _.cloneDeep($scope.vocabulary);

    $scope.tab = 'general';

    $scope.setTab = function(tab: IScope['tab']) {
        $scope.tab = tab;
    };

    $scope.idRegex = idRegex;
    $scope.selectionTypes = getVocabularySelectionTypes();

    if ($scope.matchFieldTypeToTab('related-content-fields', $scope.vocabulary.field_type)) {
        // Insert default allowed workflows
        if ($scope.vocabulary.field_options == null) {
            $scope.vocabulary.field_options = {allowed_workflows: defaultAllowedWorkflows};
        } else if ($scope.vocabulary.field_options.allowed_workflows == null) {
            $scope.vocabulary.field_options.allowed_workflows = defaultAllowedWorkflows;
        }
    }

    function onSuccess(result) {
        notify.success(gettext('Vocabulary saved successfully'));

        $scope.closeVocabulary();
        $scope.updateVocabulary(result);
        $scope.issues = null;
    }

    function onError(response) {
        if (angular.isDefined(response.data._issues)) {
            if (angular.isDefined(response.data._issues['validator exception'])) {
                notify.error(gettext('Error: ' +
                                     response.data._issues['validator exception']));
            } else if (angular.isDefined(response.data._issues.error) &&
                       response.data._issues.error.required_field) {
                let params = response.data._issues.params;

                notify.error(gettext(
                    'Required {{field}} in item {{item}}', {field: params.field, item: params.item}));
            } else {
                $scope.issues = response.data._issues;
                notify.error(gettext('Error. Vocabulary not saved.'));
            }
        }
    }

    function checkForUniqueValues() {
        const uniqueField = $scope.vocabulary.unique_field || 'qcode';
        const list = $scope.vocabulary.items || [];

        if (list.find((item) => uniqueField in item)) {
            const uniqueList = _.uniqBy(list, (item) => item[uniqueField]);

            return list.length === uniqueList.length;
        }
        return true;
    }

    /**
     * Save current edit modal contents on backend.
     */
    $scope.save = function() {
        $scope.vocabulary.items = componentRef.getItemsForSaving();
        $scope._errorUniqueness = false;
        $scope.errorMessage = null;
        delete $scope.vocabulary['_deleted'];

        if ($scope.vocabulary._id === 'crop_sizes') {
            var activeItems = _.filter($scope.vocabulary.items, (o) => o.is_active);

            activeItems.forEach(({width, height, name}: any) => {
                if (parseInt(height, 10) < 200 || parseInt(width, 10) < 200) {
                    $scope.errorMessage = gettext('Minimum height and width should be greater than or equal to 200');
                }

                if (!name || name.match(idRegex) === null) {
                    $scope.errorMessage =
                        gettext('Name field should only have alphanumeric characters, dashes and underscores');
                }
            });
        }

        if (!checkForUniqueValues()) {
            const uniqueField = $scope.vocabulary.unique_field || 'qcode';

            $scope.errorMessage = gettext('The values should be unique for {{uniqueField}}', {uniqueField});
        }

        if ($scope.vocabulary.field_type === getMediaTypes().GALLERY.id) {
            const allowedTypes = $scope.vocabulary.field_options.allowed_types;

            Object.keys(allowedTypes).forEach((key) => {
                if (!['picture', 'video', 'audio'].includes(key)) {
                    allowedTypes[key] = false;
                }
            });
        }

        if (_.isNil($scope.errorMessage)) {
            api.save(
                'vocabularies',
                $scope.vocabulary,
                undefined,
                undefined,
                undefined,
                {skipPostProcessing: true},
            ).then(onSuccess, onError);
        }

        // discard metadata cache:
        metadata.loaded = null;
        metadata.initialize();
    };

    /**
     * Return true if at least one content type should be selected
     */
    $scope.requireAllowedTypesSelection = function() {
        if (!getMediaTypeKeys().includes($scope.vocabulary.field_type)) {
            return false;
        }

        if ($scope.vocabulary.field_options == null || $scope.vocabulary.field_options.allowed_types == null) {
            return true;
        }

        const allowedTypes = $scope.vocabulary.field_options.allowed_types;
        const selectedKeys = Object.keys(allowedTypes).filter((key) => allowedTypes[key] === true);

        return selectedKeys.length === 0;
    };

    /**
     * Discard changes and close modal.
     */
    $scope.cancel = function() {
        const editing = origVocabulary?._id != null;

        if (editing) {
            $scope.updateVocabulary(origVocabulary);
        }

        $scope.closeVocabulary();
    };

    // try to reproduce data model of vocabulary:
    var model = _.mapValues(_.keyBy(
        _.uniq(_.flatten(
            _.map($scope.vocabulary.items, (o) => _.keys(o)),
        )),
    ), () => null);

    $scope.model = model;
    $scope.schema = $scope.vocabulary.schema || cvSchema[$scope.vocabulary._id] || null;

    if ($scope.schema) {
        $scope.schemaFields = Object.keys($scope.schema)
            .sort()
            .map((key) => angular.extend(
                {key: key},
                $scope.schema[key],
            ));
    }

    $scope.schemaFields = $scope.schemaFields
        || Object.keys($scope.model)
            .filter((key) => key !== 'is_active')
            .map((key) => ({key: key, label: key, type: key}));

    $scope.itemsValidation = {valid: true};

    const fields = getFields();

    $scope.customFieldTypes = Object.keys(fields).filter((id) => fields[id].private !== true).map((id) => ({
        id: id,
        label: fields[id].label,
    }));

    $scope.setCustomFieldConfig = (config) => {
        $scope.vocabulary.custom_field_config = config;
        $scope.editForm.$setDirty();
        $scope.$applyAsync();
    };

    let placeholderElement = null;

    // wait for the template to render to the placeholder element is available
    $timeout(() => {
        placeholderElement = document.querySelector('#vocabulary-items-view-edit-placeholder');

        ReactDOM.render((
            <VocabularyItemsViewEdit
                ref={(ref) => {
                    componentRef = ref;
                }}
                items={$scope.vocabulary.items}
                schemaFields={$scope.schemaFields}
                newItemTemplate={{...$scope.model, is_active: true}}
                setDirty={() => {
                    $scope.editForm.$setDirty();
                    $scope.$apply();
                }}
                setItemsValid={(valid) => {
                    $scope.itemsValidation.valid = valid;
                    $scope.$apply();
                }}
            />
        ), placeholderElement);
    });

    $scope.$watch('errorMessage', (errorMessage: string) => {
        componentRef?.setErrorMessage(errorMessage ?? null);
    });

    $scope.$on('$destroy', () => {
        if (placeholderElement != null) {
            ReactDOM.unmountComponentAtNode(placeholderElement);
        }
    });
}