superdesk/superdesk-client-core

View on GitHub
scripts/apps/search/MultiImageEdit.ts

Summary

Maintainability
F
3 days
Test Coverage
import _ from 'lodash';
import {get} from 'lodash';
import {uniq, pickBy, isEmpty, forEach} from 'lodash';
import {validateMediaFieldsThrows} from 'apps/authoring/authoring/controllers/ChangeImageController';
import {logger} from 'core/services/logger';
import {gettext} from 'core/utils';
import {getLabelNameResolver} from 'apps/workspace/helpers/getLabelForFieldId';

interface IScope extends ng.IScope {
    validator: any;
    images: any;
    imagesOriginal: any;
    placeholder: any;
    isDirty: any;
    selectImage: any;
    onChange: any;
    metadata: any;
    save: any;
    close: any;
    getSelectedImages(): Array<any>;
    saveHandler(images): Promise<void>;
    successHandler?(): void;
    cancelHandler?(): void;
    getSelectedItemsLength(): number;
    selectAll: () => void;
    unselectAll: () => void;
    areAllSelected: () => boolean;
    areSomeSelected: () => boolean;
}

MultiImageEditController.$inject = [
    '$scope',
    'modal',
    'notify',
    'lock',
    'session',
    'content',
];

export function MultiImageEditController(
    $scope: IScope,
    modal,
    notify,
    lock,
    session,
    content,
) {
    const saveHandler = $scope.saveHandler;
    let unsavedChangesExist = false;

    $scope.images = [];

    $scope.$watch('imagesOriginal', (imagesOriginal: Array<any>) => {
        // add and remove images without losing metadata of the ones which stay
        const updatedImages = imagesOriginal.map((image) => {
            const existingImage = $scope.images.find(({_id}) => _id === image._id);

            return existingImage != null
                ? existingImage
                : {...image, ...pickBy($scope.metadata, (value) => !isEmpty(value))};
        });

        $scope.images = angular.copy(updatedImages);
    });

    $scope.placeholder = {};

    $scope.isDirty = () => unsavedChangesExist;

    $scope.selectAll = () => {
        if (Array.isArray($scope.images)) {
            $scope.images.forEach((image) => {
                image.selected = true;
            });
        }
    };

    $scope.unselectAll = () => {
        if (Array.isArray($scope.images)) {
            $scope.images.forEach((image) => {
                image.selected = false;
            });
        }
    };

    $scope.areAllSelected = () =>
        Array.isArray($scope.images) && $scope.images.every((image) => image.selected === true);

    $scope.areSomeSelected = () =>
        Array.isArray($scope.images) && $scope.images.some((image) => image.selected === true);

    $scope.selectImage = (image) => {
        if ($scope.images.length === 1) {
            $scope.images[0].selected = true;
        } else {
            image.selected = !image.selected;
        }

        // refresh metadata visible in the editor according to selected images
        updateMetadata();
    };

    // wait for images for initial load
    $scope.$watch('images', (images: Array<any>) => {
        if (images != null && images.length) {
            $scope.selectAll();
            updateMetadata();
        }
    });

    $scope.onChange = (field) => {
        try {
            if (field == null) {
                throw new Error('field required');
            }
        } catch (e) {
            logger.error(e);
            return;
        }

        unsavedChangesExist = true;
        $scope.placeholder[field] = '';

        $scope.images.forEach((item) => {
            if (item.selected) {
                item[field] = $scope.metadata[field];
            }
        });
    };

    let getLabelForFieldId = (id) => id;

    getLabelNameResolver().then((_getLabelForFieldId) => {
        getLabelForFieldId = _getLabelForFieldId;
    });

    $scope.save = (close) => {
        const imagesForSaving = angular.copy($scope.images);

        imagesForSaving.forEach((image) => {
            delete image.selected;
        });

        try {
            imagesForSaving.forEach((metadata) => {
                validateMediaFieldsThrows(
                    $scope.validator,
                    metadata,
                    content.schema({}, 'picture'),
                    getLabelForFieldId);
            });
        } catch (e) {
            notify.error(e);
            return;
        }

        saveHandler(imagesForSaving)
            .then((res: any) => {
                if (res != null) {
                    $scope.images = angular.copy(Array.isArray(res) && res.length > 0 ? res : [res]);
                }
                unsavedChangesExist = false;

                if (close && typeof $scope.successHandler === 'function') {
                    unlockAndCloseModal($scope.successHandler);
                }
            });
    };

    $scope.close = () => {
        if ($scope.isDirty()) {
            modal.confirm(
                gettext('You have unsaved changes, do you want to continue?'),
                gettext('Confirm'),
            )
                .then(() => {
                    if (typeof $scope.cancelHandler === 'function') {
                        unlockAndCloseModal($scope.cancelHandler);
                    }
                });
        } else if (typeof $scope.cancelHandler === 'function') {
            unlockAndCloseModal($scope.cancelHandler);
        }
    };

    $scope.$on('item:lock', (_e, data) => {
        const {imagesOriginal} = $scope;

        if (data.lock_session === session.sessionId) {
            return; // ignore locking in the session (from this modal)
        }

        // while editing metadata if any selected item is unlocked by another user remove that item from selected items
        if (Array.isArray(imagesOriginal) && data != null && data.item != null) {
            const unlockedItem = imagesOriginal.find((image) => image._id === data.item);

            notify.error(
                gettext(
                    'Item {{headline}} unlocked by another user.',
                    {headline: unlockedItem.headline || unlockedItem.slugline},
                ),
            );
            $scope.imagesOriginal = angular.copy(imagesOriginal.filter((image) => image._id !== data.item));
            $scope.metadata = {};
        }
    });

    $scope.getSelectedImages = () => ($scope.images || []).filter((item) => item.selected);

    $scope.getSelectedItemsLength = () => $scope.getSelectedImages().length || 0;

    function unlockAndCloseModal(callback) {
        // Before closing the modal unlock all the selected items
        const unlockItems = $scope.images.map((item) => {
            // In case of new upload there will be no lock on item
            // so make sure to unlock only those items which are locked
            if (item._locked === true) {
                return lock.unlock(item);
            }
            return item;
        });

        Promise.all(unlockItems).then(callback);
    }

    function updateMetadata() {
        $scope.metadata = {
            // subject is required to "usage terms" and other custom fields are editable
            subject: compare('subject'),
            headline: compare('headline'),
            slugline: compare('slugline'),
            description_text: compare('description_text'),
            archive_description: compare('archive_description'),
            alt_text: compare('alt_text'),
            byline: compare('byline'),
            copyrightholder: compare('copyrightholder'),
            usageterms: compare('usageterms'),
            copyrightnotice: compare('copyrightnotice'),
            extra: compareExtra(),
            language: compare('language'),
            creditline: compare('creditline'),
            source: compare('source'),
            ednote: compare('ednote'),
            sign_off: compare('sign_off'),
            authors: compare('authors'),
            keywords: compare('keywords'),
        };
    }

    function getUniqueValues(field: string) {
        const uniqueValues = {};

        $scope.getSelectedImages()
            .map((item) => get(item, field))
            .filter((value) => value != null && value !== '')
            .map((value) => JSON.stringify(value))
            .forEach((value) => uniqueValues[value] = 1);
        return Object.keys(uniqueValues);
    }

    /**
     * Populate .extra metadata for editing.
     *
     * Works like compare() but for custom fields stored in .extra.
     */
    function compareExtra() {
        // get unique values for each extra field
        const extra = {};
        const values = {};

        $scope.getSelectedImages().forEach((item) => {
            if (item.extra != null) {
                for (const field in item.extra) {
                    if (!values.hasOwnProperty(field)) {
                        setExtra(field, values, extra);
                    }
                }
            }
        });

        return extra;
    }

    function setExtra(field, values, extra) {
        values[field] = getUniqueValues('extra.' + field);

        if (values[field].length === 1) {
            extra[field] = getMetaValue(field, values[field], null);
        } else if (values[field].length > 1) {
            $scope.placeholder[field] = gettext('(multiple values)');
        }
    }

    function getMetaValue(field: string, uniqueValues: Array<string>, defaultValue = null) {
        $scope.placeholder[field] = '';

        if (uniqueValues.length === 1) {
            return JSON.parse(uniqueValues[0]);
        }

        return defaultValue || '';
    }

    function compare(fieldName) {
        const uniqueValues = getUniqueValues(fieldName);
        const defaultValues = {subject: []};

        if (uniqueValues.length === 1) {
            return getMetaValue(fieldName, uniqueValues, defaultValues[fieldName]);
        } else if (uniqueValues.length > 1) {
            $scope.placeholder[fieldName] = gettext('(multiple values)');
        } else {
            $scope.placeholder[fieldName] = '';
        }
    }
}

MultiImageEditDirective.$inject = ['asset', '$sce'];
export function MultiImageEditDirective(asset, $sce) {
    return {
        scope: {
            imagesOriginal: '=',
            isUpload: '=',
            saveHandler: '=',
            cancelHandler: '=',
            successHandler: '=',
            hideEditPane: '=',
            getThumbnailHtml: '=',
            getIconForItemType: '=',
            getProgress: '=',
            onRemoveItem: '=',
            uploadInProgress: '=',
            validator: '=',
        },
        transclude: {
            'additional-content': '?sdMultiEditAdditionalContent',
            'select-desk': '?sdMultiEditSelectDesk',
        },
        controller: MultiImageEditController,
        templateUrl: asset.templateUrl('apps/search/views/multi-image-edit.html'),
        link: function(scope) {
            scope.trustAsHtml = $sce.trustAsHtml;
            scope.metadataDirty = false;

            scope.handleItemClick = function(event, image) {
                if (event.target != null && event.target.classList.contains('icon-close-small')) {
                    scope.onRemoveItem(image);
                } else {
                    scope.selectImage(image);
                }
            };

            scope.setMetadataDirty = (value) => {
                scope.metadataDirty = value;
                forEach(scope.metadata, (metadata, key) => {
                    scope.onChange(key);
                });
            };
        },
    };
}