superdesk/superdesk-client-core

View on GitHub
scripts/apps/workspace/content/controllers/ContentProfilesController.ts

Summary

Maintainability
C
1 day
Test Coverage
import {cloneDeep, get, isEqual} from 'lodash';
import {gettext} from 'core/utils';
import {IContentProfile, IRestApiResponse} from 'superdesk-api';
import {appConfig} from 'appConfig';
import {assertNever, nameof} from 'core/helpers/typescript-helpers';
import {httpRequestJsonLocal} from 'core/helpers/network';

export enum IContentProfileType {
    text = 'text',
    picture = 'picture',
    audio = 'audio',
    video = 'video',
}

const allContentProfileTypes: Array<IContentProfileType> =
    Object.keys(IContentProfileType).map((key) => IContentProfileType[key]);

interface IScope extends ng.IScope {
    showInfoBubble: boolean;
    creating: boolean;
    editing: {[key: string]: any};
    new: {[key: string]: any};
    active_only: boolean;
    contentTypeFilter: string | null;
    ngForm: any;
    contentProfileTypes: Array<{
        label: string;
        value: string;
        disabled: boolean;
        icon: string;
    }>;
    setNgForm(ngForm): void;
    patchContentProfile(patch: Partial<IContentProfile>): void;
    getContentProfileIconByProfileType(type: IContentProfile['type']): string;
    toggleContentProfileFilter(type: IContentProfile['type']): void;
}

function getContentProfileIcon(type: IContentProfileType): string {
    switch (type) {
    case IContentProfileType.text:
        return 'icon-text';
    case IContentProfileType.picture:
        return 'icon-picture';
    case IContentProfileType.audio:
        return 'icon-audio';
    case IContentProfileType.video:
        return 'icon-video';
    default:
        return 'icon-text';
    }
}

function getLabelForContentProfileType(type: IContentProfileType): string {
    switch (type) {
    case IContentProfileType.text:
        return gettext('Text');
    case IContentProfileType.picture:
        return gettext('Picture');
    case IContentProfileType.audio:
        return gettext('Audio');
    case IContentProfileType.video:
        return gettext('Video');
    default:
        return assertNever(type);
    }
}

ContentProfilesController.$inject = ['$scope', '$location', 'notify', 'content', 'modal', '$q'];
export function ContentProfilesController($scope: IScope, $location, notify, content, modal, $q) {
    var self = this;

    // info bubble
    $scope.showInfoBubble = true;

    // creating will be true while the modal for creating a new content
    // profile is visible.
    $scope.creating = false;

    // editing will hold data about the content profile being edited, as well
    // as the bind to the editing form. If no profile is being edited, it will
    // be null.
    $scope.editing = null;

    $scope.active_only = false;

    // required for being able to mark the form as dirty and enable the save button
    // after saving content profile widgets config
    $scope.setNgForm = (ngForm) => {
        $scope.ngForm = ngForm;
    };

    $scope.patchContentProfile = (patch: Partial<IContentProfile>) => {
        Object.assign($scope.editing.form, patch);

        $scope.$applyAsync(() => {
            $scope.ngForm.$dirty = true;
        });
    };

    $scope.getContentProfileIconByProfileType = (type: IContentProfile['type']) => {
        return getContentProfileIcon(IContentProfileType[type]);
    };

    $scope.contentTypeFilter = null;

    $scope.toggleContentProfileFilter = (type: IContentProfile['type']) => {
        if ($scope.contentTypeFilter === type) {
            $scope.contentTypeFilter = null;
        } else {
            $scope.contentTypeFilter = type;
        }
    };

    /**
     * @description Refreshes the list of content profiles by fetching them.
     * @returns {Promise}
     * @private
     */
    function refreshList(callEditActive) {
        return content.getTypes(null, true).then((types) => {
            self.items = types;
            if (callEditActive) {
                editActive();
            }
        });
    }

    /**
     * @description Start editing active profile
     * @private
     */
    function editActive() {
        $scope.editing = null;

        if ($location.search()._id) {
            const active = self.items.find((p) => p._id === $location.search()._id);

            if (active) {
                content.getTypeMetadata(active._id).then((type) => {
                    $scope.editing = {
                        form: cloneDeep(type),
                        original: cloneDeep(type),
                    };
                }, () => {
                    $scope.editing = {
                        form: cloneDeep(active),
                        original: active,
                    };
                });
            }
        }
    }

    function setContentProfiles() {
        $scope.contentProfileTypes = []; // loading

        httpRequestJsonLocal<IRestApiResponse<IContentProfile>>({
            method: 'GET',
            path: '/content_types',
            urlParams: {
                where: {type: {$ne: 'text'}},
            },
        }).then((res) => {
            const existingTypes = new Set(res._items.map((profile) => IContentProfileType[profile.type]));

            $scope.contentProfileTypes = allContentProfileTypes.map((type) => ({
                label: getLabelForContentProfileType(type),
                value: type,
                disabled: existingTypes.has(type),
                icon: getContentProfileIcon(type),
            }));

            $scope.$applyAsync();
        });
    }

    /**
     * @description Reports that an error has occurred.
     * @private
     */
    function reportError(resp) {
        let message = get(resp, 'data._issues["validator exception"]') || '';

        notify.error(`Operation failed ${message} (check console for response).`);
        console.error(resp);
        return $q.reject(resp);
    }

    $scope.$on('resource:updated', (event, data) => {
        if (data.resource === 'content_types' && data.fields[nameof<IContentProfile>('type')] === 1) {
            setContentProfiles();
        }
    });

    /**
     * @description Middle-ware that checks an error response to verify whether
     * it is a duplication error.
     * @param {Function} next The function to be called when error is not a
     * duplication error.
     * @private
     */
    function uniqueError(next) {
        return function(resp) {
            if (angular.isObject(resp) &&
                angular.isObject(resp.data) &&
                angular.isObject(resp.data._issues) &&
                angular.isObject(resp.data._issues.label) &&
                resp.data._issues.label.unique) {
                notify.error(self.duplicateErrorTxt);
                return $q.reject(resp);
            }
            return next(resp);
        };
    }

    this.duplicateErrorTxt = gettext('A content profile with this name already exists.');

    /**
     * @description Toggles the visibility of the creation modal.
     */
    this.toggleCreate = function() {
        $scope.new = {};
        $scope.creating = !$scope.creating;
    };

    /**
     * @description Toggles the visibility of the profile editing modal.
     * @param {Object} p the content profile being edited.
     */
    this.toggleEdit = function(p) {
        $location.search({_id: p ? p._id : null});
        $scope.$applyAsync(editActive);
    };

    /**
     * @description Creates a new content profile.
     */
    this.save = function() {
        if ($scope.new?.type == null) {
            notify.error(gettext('"{{x}}" field is required', {x: 'content type'}));
            return;
        }

        var onSuccess = function(resp) {
            refreshList(true);
            self.toggleCreate();
            return resp;
        };

        content.createProfile($scope.new)
            .then(onSuccess, uniqueError(reportError))
            .then(this.toggleEdit);
    };

    /**
     * @description Commits the changes made in the editing form for a profile
     * to the server.
     */
    this.update = function() {
        var e = $scope.editing;
        var diff = {};

        this.savingInProgress = true;
        Object.keys(e.form).forEach((k) => {
            if (!isEqual(e.form[k], e.original[k])) {
                diff[k] = e.form[k];
            }
        });

        content.updateProfile(e.original, diff)
            .then(refreshList.bind(this, false), reportError)
            .then(this.toggleEdit.bind(this, null))
            .then(() => {
                this.savingInProgress = false;
            });
    };

    /**
     * @description Queries the user for confirmation and deletes the content profile.
     */
    this.delete = function(item) {
        modal.confirm('Are you sure you want to delete this profile?').then(() => {
            content.removeProfile(item)
                .then(refreshList.bind(this, false), reportError)
                .then(this.toggleEdit.bind(this, null));
        });
    };

    refreshList(true);
    setContentProfiles();
}