kahuna/public/js/edits/image-editor.js (378 lines of code) (raw):

import angular from 'angular'; import template from './image-editor.html'; import './image-editor.css'; import {service} from './service'; import {imageService} from '../image/service'; import '../services/label'; import {imageAccessor} from '../services/image-accessor'; import {usageRightsEditor} from '../usage-rights/usage-rights-editor'; import { createCategoryLeases, removeCategoryLeases } from '../common/usageRightsUtils.js'; import {metadataTemplates} from "../metadata-templates/metadata-templates"; import {leases} from '../leases/leases'; import {archiver} from '../components/gr-archiver-status/gr-archiver-status'; import {collectionsApi} from '../services/api/collections-api'; import {rememberScrollTop} from '../directives/gr-remember-scroll-top'; import '../util/storage'; import {overwrite} from "../util/constants/editOptions"; export var imageEditor = angular.module('kahuna.edits.imageEditor', [ service.name, imageService.name, "kahuna.services.label", imageAccessor.name, usageRightsEditor.name, archiver.name, collectionsApi.name, rememberScrollTop.name, leases.name, metadataTemplates.name, 'util.storage' ]); imageEditor.controller('ImageEditorCtrl', [ '$rootScope', '$scope', '$timeout', 'editsService', 'editsApi', 'imageService', 'labelService', 'imageAccessor', 'collections', 'mediaApi', 'storage', function($rootScope, $scope, $timeout, editsService, editsApi, imageService, labelService, imageAccessor, collections, mediaApi, storage) { var ctrl = this; ctrl.canUndelete = false; ctrl.isDeleted = false; ctrl.displayMetadataTemplates = window._clientConfig.metadataTemplates !== undefined && window._clientConfig.metadataTemplates.length > 0; ctrl.$onInit = () => { mediaApi.getSession().then(session => { if (ctrl.image.data.softDeletedMetadata !== undefined && (session.user.permissions.canDelete || session.user.email === ctrl.image.data.uploadedBy)) { ctrl.canUndelete = true; } if (ctrl.image.data.softDeletedMetadata !== undefined) { ctrl.isDeleted = true; } }); editsService.canUserEdit(ctrl.image).then(editable => { ctrl.userCanEdit = editable; }); ctrl.batchApplyUsageRights = batchApplyUsageRights; editsApi.getUsageRightsCategories() .then(cats => ctrl.categories = cats) .finally(() => updateUsageRightsCategory()); ctrl.error = false; ctrl.saved = false; ctrl.saving = false; ctrl.showUsageRights = false; ctrl.status = ctrl.image.data.valid ? 'ready' : 'invalid'; ctrl.usageRights = imageService(ctrl.image).usageRights; ctrl.invalidReasons = ctrl.image.data.invalidReasons; ctrl.systemName = window._clientConfig.systemName; ctrl.descriptionOption = overwrite.key; ctrl.undelete = undelete; ctrl.imageAsArray = [ctrl.image]; const updateImages = (images, metadataFieldName, valueFn) => { images.map((image) => { editsService.batchUpdateMetadataField( [image], metadataFieldName, valueFn(image), ctrl.descriptionOption ); }); return Promise.resolve(ctrl.imageAsArray); }; const removeXFromImages = (metadataFieldName, accessor) => (images, removedX, fieldName) => { if (fieldName && fieldName !== metadataFieldName) { return Promise.resolve(ctrl.imageAsArray); } var removedArr = Array.isArray(removedX) ? removedX : [removedX]; return updateImages( images, metadataFieldName, (image) => accessor(image)?.filter((x) => !removedArr.includes(x)) || [] ); }; const addXToImages = (metadataFieldName, accessor) => (images, addedX, fieldName, removedElements = []) => { if (fieldName && fieldName !== metadataFieldName) { return Promise.resolve(ctrl.imageAsArray); } return updateImages( images, metadataFieldName, (image) => { const currentXInImage = accessor(image); let tempElements = currentXInImage ? [...currentXInImage, ...addedX] : [...addedX]; tempElements = tempElements.filter(e => !removedElements.includes(e)); return tempElements; } ); }; //-labels- function addLabelToImagesFn(images, addedX, fieldName, removedElements = []) { if (fieldName && fieldName !== "labels") { return Promise.resolve(ctrl.imageAsArray); } if (removedElements.length > 0) { removedElements.forEach(element => labelService.batchRemove(images, element)); } return labelService.batchAdd(images, addedX); } function removeLabelFromImagesFn(images, removedX, fieldName) { if (fieldName && fieldName !== "labels") { return Promise.resolve(ctrl.imageAsArray); } return labelService.batchRemove(images, removedX); } ctrl.addLabelToImages = addLabelToImagesFn; ctrl.removeLabelFromImages = removeLabelFromImagesFn; ctrl.labelAccessor = (image) => imageAccessor.readLabels(image).map(label => label.data); //-keywords- ctrl.keywordAccessor = (image) => imageAccessor.readMetadata(image).keywords; ctrl.addKeywordToImages = addXToImages('keywords', ctrl.keywordAccessor); ctrl.removeKeywordFromImages = removeXFromImages('keywords', ctrl.keywordAccessor); //TODO put collections in their own directive ctrl.addCollection = false; ctrl.addToCollection = addToCollection; ctrl.batchApplyCollections = batchApplyCollections; ctrl.collectionError = false; ctrl.confirmDelete = false; ctrl.getCollectionStyle = getCollectionStyle; ctrl.openCollectionTree = openCollectionTree; ctrl.removeImageFromCollection = removeImageFromCollection; ctrl.selectionMode = true; // TODO: Find a way to broadcast more selectively const batchApplyUsageRightsEvent = 'events:batch-apply:usage-rights'; const batchApplyCollectionsEvent = 'events:batch-apply:collections'; const batchRemoveCollectionsEvent = 'events:batch-remove:collections'; const metadata = ctrl.image.data.userMetadata.data.metadata; const usageRights = ctrl.image.data.userMetadata.data.usageRights; const offMetadataUpdateStart = editsService.on(metadata, 'update-start', () => ctrl.saving = true); const offMetadataUpdateEnd = editsService.on(metadata, 'update-end', onSave); const offMetadataUpdateError = editsService.on(metadata, 'update-error', onError); const offUsageRightsUpdateStart = editsService.on(usageRights, 'update-start', () => ctrl.saving = true); const offUsageRightsUpdateEnd = editsService.on(usageRights, 'update-end', onSave); const offUsageRightsUpdateError = editsService.on(usageRights, 'update-error', onError); $scope.$on('$destroy', () => { offMetadataUpdateStart(); offMetadataUpdateEnd(); offMetadataUpdateError(); offUsageRightsUpdateStart(); offUsageRightsUpdateEnd(); offUsageRightsUpdateError(); }); ctrl.onMetadataTemplateApplying = (leases) => { if (angular.isDefined(leases)) { ctrl.leasesUpdatingByTemplate = true; } }; ctrl.onMetadataTemplateApplied = () => { $scope.$broadcast('events:metadata-template:template-applied', {}); ctrl.collectionUpdatedByTemplate = false; ctrl.leasesUpdatedByTemplate = false; ctrl.leasesUpdatingByTemplate = false; ctrl.showUsageRights = false; ctrl.usageRightsUpdatedByTemplate = false; }; ctrl.onMetadataTemplateCancelled = (metadata, usageRights) => { $scope.$broadcast('events:metadata-template:template-cancelled', { metadata }); ctrl.collectionUpdatedByTemplate = false; ctrl.leasesUpdatedByTemplate = false; ctrl.usageRights.data = usageRights; ctrl.showUsageRights = false; ctrl.usageRightsUpdatedByTemplate = false; }; ctrl.onMetadataTemplateSelected = (metadata, usageRights, collection, leasesWithConfig) => { $scope.$broadcast('events:metadata-template:template-selected', { metadata }); ctrl.collectionUpdatedByTemplate = false; ctrl.leasesUpdatedByTemplate = false; ctrl.showUsageRights = false; ctrl.usageRightsUpdatedByTemplate = false; ctrl.usageRights.data = usageRights; if (angular.isDefined(leasesWithConfig)) { const leasesFromTemplate = leasesWithConfig.leases.map(lease => { return {...lease, fromTemplate: true}; }); ctrl.updatedLeases = [...leasesFromTemplate, ...(leasesWithConfig.replace ? [] : ctrl.image.data.leases.data.leases)]; ctrl.leasesUpdatedByTemplate = true; } if (angular.isDefined(collection)) { if (ctrl.image.data.collections.filter(r => r.data.path.toString() === collection.data.fullPath.toString()).length === 0) { ctrl.updatedCollections = [ {description: collection.data.data.description, fromTemplate: true}, ...ctrl.image.data.collections.map(resource => resource.data) ]; ctrl.collectionUpdatedByTemplate = true; } } if (ctrl.image.data.usageRights === undefined || ctrl.image.data.usageRights.category !== usageRights.category) { ctrl.showUsageRights = true; } const originalUsageRights = ctrl.image.data.usageRights ? ctrl.image.data.usageRights : {}; if (angular.equals(usageRights, originalUsageRights) === false) { ctrl.usageRightsUpdatedByTemplate = true; } }; if (Boolean(ctrl.withBatch)) { $scope.$on(batchApplyUsageRightsEvent, (e, { data }) => { const image = ctrl.image; const resource = image.data.userMetadata.data.usageRights; editsService.update(resource, data, image); batchSetLeasesFromUsageRights(image, data.category); }); } if (Boolean(ctrl.withBatch)) { $scope.$on(batchApplyCollectionsEvent, (e, { collections }) => { collections.forEach( ctrl.addToCollection ); }); $scope.$on(batchRemoveCollectionsEvent, () => { const collectionsOnImage = ctrl.image.data.collections; collectionsOnImage.forEach( ctrl.removeImageFromCollection ); }); } function onSave() { return ctrl.image.get().then(newImage => { ctrl.image = newImage; ctrl.imageAsArray = [newImage]; ctrl.usageRights = imageService(ctrl.image).usageRights; updateUsageRightsCategory(); ctrl.status = ctrl.image.data.valid ? 'ready' : 'invalid'; ctrl.saving = false; ctrl.error = false; ctrl.saved = true; $timeout(() => ctrl.saved = false, 1000); }). // TODO: we could retry here again, but re-saving does that, and given // it's auto-save, it should be fine. then(() => onError); } function onError() { ctrl.saving = false; ctrl.error = true; } function updateUsageRightsCategory() { let category = ctrl.categories.find(cat => cat.value === ctrl.usageRights.data.category); ctrl.usageRightsCategory = category && category.name; ctrl.showUsageRights = ctrl.usageRightsCategory === undefined; } function batchSetLeasesFromUsageRights(image, rightsCat) { const category = ctrl.categories.find(cat => cat.value === rightsCat); if (!category || image.data.usageRights.category === rightsCat) { return; } if (category.leases.length === 0) { // possibility of removal only if (!image.data.usageRights.category) { return; } const removeLeases = removeCategoryLeases(ctrl.categories, image, image.data.usageRights.category); if (removeLeases.length > 0) { $rootScope.$broadcast('events:rights-category:delete-leases', { catLeases: removeLeases, batch: false }); } return; } const catLeases = createCategoryLeases(category.leases, image); if (catLeases.length === 0) { // possibility of remove only of leases due to missing date info on image if (!image.data.usageRights.category) { return; } const removeLeases = removeCategoryLeases(ctrl.categories, image, image.data.usageRights.category); if (removeLeases.length > 0) { $rootScope.$broadcast('events:rights-category:delete-leases', { catLeases: removeLeases, batch: false }); } return; } $rootScope.$broadcast('events:rights-category:add-leases', { catLeases: catLeases, batch: false }); } function batchApplyUsageRights() { $rootScope.$broadcast(batchApplyUsageRightsEvent, { data: ctrl.usageRights.data }); //-rights category derived leases- const mtchingRightsCats = ctrl.categories.filter(c => c.value == ctrl.usageRights.data.category); if (mtchingRightsCats.length > 0) { const rightsCat = mtchingRightsCats[0]; if (rightsCat.leases.length > 0) { const catLeases = createCategoryLeases(rightsCat.leases, ctrl.image); if (catLeases.length > 0) { $rootScope.$broadcast('events:rights-category:add-leases', { catLeases: catLeases, batch: true }); } } } } function openCollectionTree() { ctrl.addCollection = true; collections.getCollections().then(collections => { ctrl.collections = collections.data.children; // this will trigger the remember-scroll-top directive to return // users to their previous position on the collections panel // once the tree has been rendered $timeout(() => { $scope.$broadcast('gr:remember-scroll-top:apply'); }); }, () => { // TODO: More informative error handling // TODO: Stop error propagating to global error handler ctrl.error = true; }).catch(() => ctrl.collectionError = true); } function addToCollection(collection) { collections.addCollectionToImage(ctrl.image, collection); //this isn't needed when called from batch apply ctrl.addCollection = false; } function removeImageFromCollection(collection) { collections.removeImageFromCollection(collection, ctrl.image); } function batchApplyCollections() { const collectionsOnImage = ctrl.image.data.collections.map(collection => collection.data.path); if (collectionsOnImage.length > 0) { $rootScope.$broadcast(batchApplyCollectionsEvent, { collections: collectionsOnImage } ); } else { ctrl.confirmDelete = true; $timeout(() => { ctrl.confirmDelete = false; }, 5000); } ctrl.batchRemoveCollections = () => { $rootScope.$broadcast(batchRemoveCollectionsEvent); }; } function getCollectionStyle(collection) { return collection.data.cssColour && `background-color: ${collection.data.cssColour}`; } function undelete() { const imageId = ctrl.image.data.id; mediaApi.undelete(imageId).then( ctrl.canUndelete = ctrl.isDeleted = false ); } ctrl.srefNonfree = () => storage.getJs("isNonFree", true) ? true : undefined; }; }]); imageEditor.directive('uiImageEditor', [function() { return { restrict: 'E', controller: 'ImageEditorCtrl', controllerAs: 'ctrl', bindToController: true, template: template, transclude: true, scope: { image: '=', // FIXME: we only need these to pass them through to `required-metadata-editor` withBatch: '=' } }; }]);