<commit_before>import * as helpers from 'apps/authoring/authoring/helpers';
import _ from 'lodash';
import {merge, flatMap} from 'lodash';
import postscribe from 'postscribe';
import thunk from 'redux-thunk';
import {gettext} from 'core/utils';
import {logger} from 'core/services/logger';
import {combineReducers, createStore, applyMiddleware} from 'redux';
import {applyMiddleware as coreApplyMiddleware} from 'core/middleware';
import {getArticleSchemaMiddleware} from '..';
import {isPublished} from 'apps/archive/utils';
import {AuthoringWorkspaceService} from '../services/AuthoringWorkspaceService';
import {copyJson} from 'core/helpers/utils';
import {appConfig, extensions} from 'appConfig';
import {onPublishMiddlewareResult, IExtensionActivationResult, IArticle} from 'superdesk-api';
import {mediaIdGenerator} from '../services/MediaIdGeneratorService';
import {addInternalEventListener} from 'core/internal-events';
import {validateMediaFieldsThrows} from '../controllers/ChangeImageController';
import {getLabelNameResolver} from 'apps/workspace/helpers/getLabelForFieldId';
import {ITEM_STATE} from 'apps/archive/constants';
import {isMediaType} from 'core/helpers/item';
import {confirmQuickPublish} from '../services/quick-publish-modal';

/**
 * @ngdoc directive
 * @module superdesk.apps.authoring
 * @name sdAuthoring
 *
 * @description
 *   This directive is responsible for generating superdesk content authoring form.
 */

AuthoringDirective.$inject = [
    'superdesk',
    'authoringWorkspace',
    'notify',
    'desks',
    'authoring',
    'api',
    'session',
    'lock',
    'privileges',
    'content',
    '$location',
    'referrer',
    'macros',
    '$q',
    'modal',
    'archiveService',
    'confirm',
    'reloadService',
    '$rootScope',
    'suggest',
    'editorResolver',
    'compareVersions',
    'embedService',
    '$injector',
    'autosave',
];
export function AuthoringDirective(
    superdesk,
    authoringWorkspace: AuthoringWorkspaceService,
    notify,
    desks,
    authoring,
    api,
    session,
    lock,
    privileges,
    content,
    $location,
    referrer,
    macros,
    $q,
    modal,
    archiveService,
    confirm,
    reloadService,
    $rootScope,
    suggest,
    editorResolver,
    compareVersions,
    embedService,
    $injector,
    autosave,
) {
    return {
        link: function($scope, elem, attrs) {
            $scope.loading = false;
            $scope.tabsPinned = false;

            var _closing;
            var mediaFields = {};
            var userDesks;

            const UNIQUE_NAME_ERROR = gettext('Error: Unique Name is not unique.');
            const MEDIA_TYPES = ['video', 'picture', 'audio'];
            const isPersonalSpace = $location.path() === '/workspace/personal';

            $scope.toDeskEnabled = false; // Send an Item to a desk
            $scope.closeAndContinueEnabled = false; // Create an update of an item and Close the item.
            $scope.publishEnabled = false; // publish an item
            $scope.publishAndContinueEnabled = false; // Publish an item and Create an update.

            desks.fetchCurrentUserDesks().then((desksList) => {
                userDesks = desksList;
                $scope.itemActions = authoring.itemActions($scope.origItem, userDesks);
            });

            $scope.privileges = privileges.privileges;
            $scope.dirty = false;
            $scope.views = {send: false};
            $scope.stage = null;
            $scope._editable = !!$scope.origItem._editable;
            $scope.isMediaType = isMediaType($scope.origItem);
            $scope.action = $scope.action || ($scope._editable ? 'edit' : 'view');

            $scope.highlight = !!$scope.origItem.highlight;
            $scope.showExportButton = $scope.highlight && $scope.origItem.type === 'composite';
            $scope.openSuggestions = () => suggest.setActive();
            $scope.openCompareVersions = (item) => compareVersions.init(item);
            $scope.isValidEmbed = {};
            $scope.embedPreviews = {};
            $scope.mediaFieldVersions = {};
            $scope.refreshTrigger = 0;
            $scope.isPreview = false;
            $scope.isCorrectionInProgress = false;

            $scope.$watch('origItem', (newValue, oldValue) => {
                $scope.itemActions = null;
                if (newValue) {
                    $scope.itemActions = authoring.itemActions(newValue, userDesks);
                }
            }, true);

            $scope.$watch('item.flags', (newValue, oldValue) => {
                if (newValue !== oldValue) {
                    $scope.item.flags = _.clone($scope.origItem.flags);
                    $scope.item.flags = newValue;
                    $scope.origItem.flags = oldValue;
                    $scope.dirty = true;
                }
            }, true);

            function checkShortcutButtonAvailability(personal = false) {
                if (personal) {
                    return appConfig?.features?.publishFromPersonal && $scope.item.state !== 'draft';
                }
                return $scope.item.task && $scope.item.task.desk && $scope.item.state !== 'draft' || $scope.dirty;
            }

            $scope.$watch('item', () => {
                $scope.toDeskEnabled = appConfig?.features?.customAuthoringTopbar?.toDesk
                && !isPersonalSpace && checkShortcutButtonAvailability();

                $scope.closeAndContinueEnabled = appConfig?.features?.customAuthoringTopbar?.closeAndContinue
                && !isPersonalSpace && checkShortcutButtonAvailability();

                $scope.publishEnabled = appConfig?.features?.customAuthoringTopbar?.publish
                    && canPublishOnDesk() && checkShortcutButtonAvailability(isPersonalSpace);

                $scope.publishAndContinueEnabled = appConfig?.features?.customAuthoringTopbar?.publishAndContinue
                    && !isPersonalSpace && canPublishOnDesk() && checkShortcutButtonAvailability();
            }, true);

            $scope._isInProductionStates = !isPublished($scope.origItem);

            $scope.fullPreview = false;
            $scope.proofread = false;
            $scope.referrerUrl = referrer.getReferrerUrl();
            $scope.gettext = gettext;

            content.getTypes().then(() => {
                $scope.content_types = content.types;
            });

            /**
             * Get the Desk and Stage for the item.
             */
            function getDeskStage() {
                if ($scope.origItem.task && $scope.origItem.task.stage) {
                    if (archiveService.isLegal($scope.origItem)) {
                        $scope.deskName = $scope.origItem.task.desk;
                        $scope.stage = $scope.origItem.task.stage;
                    } else {
                        api('stages').getById($scope.origItem.task.stage)
                            .then((result) => {
                                $scope.stage = result;
                            });

                        setDesk();
                    }
                }
            }

            /**
             * Get the Current Template for the item.
            */
            function getCurrentTemplate() {
                if (typeof $scope.item?.template !== 'string') {
                    logger.error(new Error('template must be present'));
                    return;
                }
                api('content_templates').getById($scope.item.template)
                    .then((result) => {
                        $scope.currentTemplate = result;
                    });
            }

            /**
            * Get the desk name and desk type.
            */
            function setDesk() {
                if (!$scope.item.task.desk) {
                    return false;
                }
                const desk = desks.getItemDesk($scope.item);

                if (desk) {
                    $scope.deskName = desk.name;
                    $scope.deskType = desk.desk_type;
                }
            }

            /**
             * Check if it is allowed to publish on desk
             * @returns {Boolean}
             */
            function canPublishOnDesk() {
                return !($scope.deskType === 'authoring' && appConfig.features.noPublishOnAuthoringDesk) &&
                    privileges.userHasPrivileges({publish: 1});
            }

            getDeskStage();
            getCurrentTemplate();
            /**
             * `desk_stage:change` event from send and publish action.
             * If send action succeeds but publish fails then we need change item location.
             */
            $scope.$on('desk_stage:change', getDeskStage);

            /**
             * Start editing current item
             */

            $scope.edit = function edit() {
                if ($scope.origItem.state === 'unpublished') {
                    api.update('archive', $scope.origItem, {state: 'in_progress'})
                        .then((updated) => {
                            // for updating and refreshing the item everywhere,
                            // close it first and then open it.
                            $scope.close().then(() => {
                                authoringWorkspace.edit(updated);
                            });
                        });
                } else if (isPublished($scope.origItem)) {
                    authoringWorkspace.view($scope.origItem);
                } else {
                    authoringWorkspace.edit($scope.origItem);
                }
            };

            /**
             * Create a new version
             */
            $scope.save = function() {
                return authoring.save($scope.origItem, $scope.item).then((res) => {
                    $scope.dirty = false;
                    _.merge($scope.item, res);

                    if (res.highlight) {
                        _previewHighlight(res._id);
                    }

                    notify.success(gettext('Item updated.'));

                    initMedia();
                    return $scope.origItem;
                }, (response) => {
                    if (response.status === 412) {
                        notifyPreconditionFailed();
                        return;
                    }

                    if (angular.isDefined(response.data._issues)) {
                        if (angular.isDefined(response.data._issues.unique_name) &&
                            response.data._issues.unique_name.unique === 1) {
                            notify.error(UNIQUE_NAME_ERROR);
                            return;
                        } else if (angular.isDefined(response.data._issues['validator exception'])) {
                            notify.error(gettext('Error: {{message}}',
                                {message: response.data._issues['validator exception']}));
                            return;
                        }
                    }

                    notify.error(gettext('Error. Item not updated.'));
                });
            };

            $scope.openFullPreview = function($event) {
                if ($event.button === 0 && !$event.ctrlKey) {
                    $event.preventDefault();
                    $scope.fullPreview = true;
                }
            };

            $scope.closeFullPreview = function() {
                $scope.fullPreview = false;
            };

            /**
             * Export the list of highlights as a text item.
             */
            $scope.exportHighlight = function(item) {
                if ($scope.save_enabled()) {
                    modal.confirm(gettext('You have unsaved changes, do you want to continue?'))
                        .then(() => {
                            _exportHighlight(item._id);
                        },
                        );
                } else {
                    _exportHighlight(item._id);
                }
            };

            function _exportHighlight(_id) {
                api.generate_highlights.save({}, {package: _id})
                    .then(authoringWorkspace.edit, (response) => {
                        if (response.status === 403) {
                            _forceExportHighlight(_id);
                        } else {
                            notify.error(gettext('Error creating highlight.'));
                        }
                    });
            }

            function _forceExportHighlight(_id) {
                modal.confirm(gettext('There are items locked or not published. Do you want to continue?'))
                    .then(() => {
                        api.generate_highlights.save({}, {package: _id, export: true})
                            .then(authoringWorkspace.edit, (response) => {
                                notify.error(gettext('Error creating highlight.'));
                            });
                    });
            }

            function _previewHighlight(_id) {
                api.generate_highlights.save({}, {package: _id, preview: true})
                    .then((response) => {
                        $scope.highlight_preview = response.body_html;
                    }, (data) => {
                        $scope.highlight_preview = data.message;
                    });
            }

            if ($scope.origItem.highlight) {
                _previewHighlight($scope.origItem._id);
            }

            /**
             * Invoked by validatePublishScheduleAndEmbargo() to validate the date and time values.
             *
             * @return {string} if the values are invalid then returns appropriate error message.
             *         Otherwise empty string.
             */
            function validateTimestamp(datePartOfTS, timePartOfTS, timestamp, timezone, fieldName) {
                var errors = authoring.validateSchedule(
                    datePartOfTS,
                    timePartOfTS,
                    timestamp,
                    timezone,
                    fieldName,
                );

                if (!errors) {
                    return;
                }

                if (errors.date) {
                    return gettext('{{field}} date is required!', {field: fieldName});
                }

                if (errors.time) {
                    return gettext('{{field}} time is required!', {field: fieldName});
                }

                if (errors.timestamp) {
                    return gettext('{{field}} is not a valid date!', {field: fieldName});
                }

                if (errors.future && fieldName !== 'Embargo' || $scope._isInProductionStates) {
                    return gettext('{{field}} cannot be earlier than now!', {field: fieldName});
                }
            }

            /**
             * Validates the Embargo Date and Time and Publish Schedule Date and Time if they are set on the item.
             *
             * @return {boolean} true if valid, false otherwise.
             */
            function validatePublishScheduleAndEmbargo(item) {
                if (item.embargo && item.publish_schedule) {
                    notify.error(gettext('An Item can\'t have both Embargo and Publish Schedule.'));
                    return false;
                }

                var errorMessage;

                if (item.embargo_date || item.embargo_time) {
                    errorMessage = validateTimestamp(
                        item.embargo_date, item.embargo_time, item.embargo,
                        item.schedule_settings ? item.schedule_settings.time_zone : null,
                        gettext('Embargo'));
                }

                if (item.publish_schedule_date || item.publish_schedule_time) {
                    if (isPublished(item, false)) {
                        return true;
                    }

                    errorMessage = validateTimestamp(
                        item.publish_schedule_date, item.publish_schedule_time,
                        item.publish_schedule, item.schedule_settings ? item.schedule_settings.time_zone : null,
                        gettext('Publish Schedule'));
                }

                if (errorMessage) {
                    notify.error(errorMessage);
                    return false;
                }

                return true;
            }

            /**
             * Checks if associations is with rewrite_of item then open then modal to add associations.
             * The user has options to add associated media to the current item and review the media change
             * or publish the current item without media.
             * User will be prompted in following scenarios:
             * 1. Edit feature image and confirm media update is enabled.
             * 2. Once item is published then no confirmation.
             * 3. If current item is update and updated story has associations
             */
            function checkMediaAssociatedToUpdate() {
                let rewriteOf = $scope.item.rewrite_of;

                if (!(appConfig.features != null && appConfig.features.confirmMediaOnUpdate) ||
                    !(appConfig.features != null && appConfig.features.editFeaturedImage) ||
                    !rewriteOf || _.includes(['kill', 'correct', 'takedown'], $scope.action) ||
                    $scope.item.associations && $scope.item.associations.featuremedia) {
                    return $q.when(true);
                }

                return api.find('archive', rewriteOf)
                    .then((rewriteOfItem) => {
                        if (rewriteOfItem && rewriteOfItem.associations &&
                            rewriteOfItem.associations.featuremedia) {
                            return confirm.confirmFeatureMedia(rewriteOfItem);
                        }
                        return true;
                    })
                    .then((result) => {
                        if (result && result.associations) {
                            $scope.item.associations = result.associations;
                            $scope.autosave($scope.item);
                            return false;
                        }

                        return true;
                    });
            }

            function getOnPublishMiddlewares()
            : Array<IExtensionActivationResult['contributions']['entities']['article']['onPublish']> {
                return flatMap(
                    Object.values(extensions).map(({activationResult}) => activationResult),
                    (activationResult) =>
                        activationResult.contributions != null
                        && activationResult.contributions.entities != null
                        && activationResult.contributions.entities.article != null
                        && activationResult.contributions.entities.article.onPublish != null
                            ? activationResult.contributions.entities.article.onPublish
                            : [],
                );
            }

            function publishItem(orig, item) {
                autosave.stop(item);

                var action = $scope.action === 'edit' ? 'publish' : $scope.action;
                const onPublishMiddlewares = getOnPublishMiddlewares();
                let warnings: Array<{text: string}> = [];
                const initialValue: Promise<onPublishMiddlewareResult> = Promise.resolve({});

                $scope.error = {};

                return onPublishMiddlewares.reduce(
                    (current, next) => {
                        return current.then((result) => {
                            if (result && result.warnings && result.warnings.length > 0) {
                                warnings = warnings.concat(result.warnings);
                            }

                            return next(Object.assign({
                                _id: _.get(orig, '_id'),
                                type: _.get(orig, 'type'),
                            }, item));
                        });
                    },
                    initialValue,
                )
                    .then((result) => {
                        if (result && result.warnings && result.warnings.length > 0) {
                            warnings = warnings.concat(result.warnings);
                        }

                        return result;
                    })
                    .then(() => checkMediaAssociatedToUpdate())
                    .then((result) => {
                        if (result && warnings.length < 1) {
                            return authoring.publish(orig, item, action);
                        }
                        return $q.reject(false);
                    })
                    .then((response: IArticle) => {
                        notify.success(gettext('Item published.'));
                        $scope.item = response;
                        $scope.dirty = false;
                        authoringWorkspace.close(true);
                        return true;
                    }, (response) => {
                        let issues = _.get(response, 'data._issues');

                        if (issues) {
                            if (angular.isDefined(issues['validator exception'])) {
                                var errors = issues['validator exception'];
                                var modifiedErrors = errors.replace(/\[/g, '')
                                    .replace(/\]/g, '')
                                    .split(',');

                                modifiedErrors.forEach((error) => {
                                    const message = _.trim(error, '\' ');
                                    // the message format is 'Field error text' (contains ')
                                    const field = message.split(' ')[0];

                                    $scope.error[field.toLocaleLowerCase()] = true;
                                    notify.error(message);
                                });

                                if (issues.fields) {
                                    Object.assign($scope.error, issues.fields);
                                }

                                $scope.$applyAsync(); // make $scope.error changes visible

                                if (errors.indexOf('9007') >= 0 || errors.indexOf('9009') >= 0) {
                                    authoring.open(item._id, true).then((res) => {
                                        $scope.origItem = res;
                                        $scope.dirty = false;
                                        $scope.item = copyJson($scope.origItem);
                                    });
                                }
                                return $q.reject(false);
                            }

                            if (issues.unique_name && issues.unique_name.unique) {
                                notify.error(UNIQUE_NAME_ERROR);
                                return $q.reject(false);
                            }
                        } else if (response && response.status === 412) {
                            notifyPreconditionFailed();
                            return $q.reject(false);
                        } else if (warnings.length > 0) {
                            warnings.forEach(
                                (warning) => {
                                    notify.error(warning.text);
                                },
                            );
                            return $q.reject(false);
                        }
                        return $q.reject(false);
                    });
            }

            function notifyPreconditionFailed() {
                notify.error(gettext('Item has changed since it was opened. ' +
                    'Please close and reopen the item to continue. ' +
                    'Regrettably, your changes cannot be saved.'));
                $scope._editable = false;
                $scope.dirty = false;
            }

            let getLabelForFieldId = (id) => id;

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

            function validateForPublish(item) {
                var validator = appConfig.validator_media_metadata;

                if (item.type === 'picture' || item.type === 'graphic') {
                    // required media metadata fields are defined in superdesk.config.js
                    try {
                        validateMediaFieldsThrows(validator, item, $scope.schema, getLabelForFieldId);
                    } catch (e) {
                        notify.error(e);
                        return false;
                    }
                }

                return true;
            }

            $scope.hideLiveSuggestions = function() {
                return $rootScope.config.features && $rootScope.config.features.hideLiveSuggestions;
            };

            $scope.openExport = function() {
                $scope.export = true;
            };

            $scope.canExport = function() {
                return $scope.privileges.content_export ? $scope.item.lock_user === session.identity._id &&
                    $scope.itemActions.export : false;
            };

            $scope.closeExport = function() {
                $scope.export = false;
            };

            $scope.useTansaProofing = function() {
                return $rootScope.config.features && $rootScope.config.features.useTansaProofing;
            };

            var deregisterTansa = $rootScope.$on('tansa:end', afterTansa);

            $scope.runTansa = function() {
                if (window.RunTansaProofing && appConfig.tansa != null) {
                    const _editor = editorResolver.get();

                    if (_editor && _editor.version() === '3') {
                        $('#editor3Tansa').html(_editor.getHtmlForTansa());
                    }

                    const profiles = appConfig.tansa.profiles || {};

                    if (profiles[$scope.item.language] != null) {
                        window.tansa.settings.profileId = profiles[$scope.item.language];
                    }

                    (function workAroundTansaSpellcheckerSelectionBug() {
                        /**
                         * The issue was that Tansa spell-checker would only check a single field, even if no input
                         * field was focused at the time of initializing the spell-checker.
                         *
                         * If spell-checking was cancelled, that field would receive focus.
                         *
                         * If the page was reloaded, or an article reopened - all fields were spell-checked as expected.
                         *
                         * The issue was only happening if an expanded text selection was made in input[type="text"]
                         * field at **any point** prior to initializing the spell-checker.
                         * Such a selection is made every time when focusing a
                         * non-empty input[type="text"] field using a tab key.
                         * Even if other text fields received focus after that,
                         * it would only spell-check the single field,
                         * that last had an expanded text selection performed on it.
                         *
                         * I assume Tansa spell-checker has a feature to only spell-check a selected part of the text
                         * and their JavaScript code has a bug where it can't differentiate between text that
                         * was intentionally selected for spell-checking from text that was selected long before
                         * initializing the spell-checker, even after multiple other input fields received focus.
                         */

                        Array.from(document.querySelectorAll('input[type="text"]')).forEach((el: HTMLInputElement) => {
                            // Making sure there are no expanded selections.
                            if (el !== document.activeElement) {
                                el.setSelectionRange(0, 0);
                            }
                        });
                    })();

                    window.RunTansaProofing();
                } else {
                    notify.error(gettext('Tansa is not responding. You can continue editing or publish the story.'));
                }
            };

            $scope.isRemovedField = function(fieldName) {
                return appConfig.infoRemovedFields != null && appConfig.infoRemovedFields.hasOwnProperty(fieldName);
            };

            function afterTansa(e, isCancelled) {
                const _editor = editorResolver.get();

                if (_editor && _editor.version() === '3' && !isCancelled) {
                    _editor.setHtmlFromTansa($('#editor3Tansa').html());
                }
            }

            /**
             * Depending on the item state one of the publish, correct, kill actions will be executed on the item
             * in $scope.
             */
            $scope.publish = function() {
                if (helpers.itemHasUnresolvedSuggestions($scope.item)) {
                    modal.alert({
                        headerText: gettext('Resolving suggestions'),
                        bodyText: gettext(
                            'Article cannot be published. Please accept or reject all suggestions first.',
                        ),
                    });

                    return Promise.reject();
                }

                if (helpers.itemHasUnresolvedComments($scope.item)) {
                    modal.confirm({
                        bodyText: gettext(
                            'This article contains unresolved comments.'
                            + 'Click on Cancel to go back to editing to'
                            + 'resolve those comments or OK to ignore and proceed with publishing',
                        ),
                        headerText: gettext('Resolving comments'),
                        okText: gettext('Ok'),
                        cancelText: gettext('Cancel'),
                    }).then((ok) => ok ? performPublish() : false);

                    return Promise.reject();
                }

                return performPublish();
            };

            function performPublish() {
                if (validatePublishScheduleAndEmbargo($scope.item) && validateForPublish($scope.item)) {
                    var message = 'publish';

                    if ($scope.action && $scope.action !== 'edit') {
                        message = $scope.action;
                    } else if ($scope.action === 'edit' && $scope.item.state === ITEM_STATE.CORRECTION) {
                        $scope.action = 'correct';
                    }

                    if ($scope.dirty && message === 'publish') {
                        // confirmation only required for publish
                        return authoring.publishConfirmation($scope.origItem, $scope.item, $scope.dirty, message)
                            .then((res) => {
                                if (res) {
                                    return publishItem($scope.origItem, $scope.item);
                                }
                            }, (response) => {
                                notify.error(gettext('Error. Item not published.'));
                                return $q.reject(false);
                            });
                    }

                    return publishItem($scope.origItem, $scope.item);
                }

                return false;
            }

            $scope.showCustomButtons = () => {
                return $scope.toDeskEnabled || $scope.closeAndContinueEnabled
                    || $scope.publishAndContinueEnabled || $scope.publishEnabled;
            };

            $scope.saveAndContinue = function(customButtonAction, showConfirm) {
                if ($scope.dirty) {
                    showConfirm ?
                        $scope.saveTopbar()
                            .then(() => confirmQuickPublish([$scope.item]))
                            .then(customButtonAction) :
                        $scope.saveTopbar()
                            .then(customButtonAction);
                } else {
                    showConfirm ?
                        confirmQuickPublish([$scope.item]).then(customButtonAction) :
                        customButtonAction();
                }
                initMedia();
            };

            $scope.publishAndContinue = function() {
                $scope.publish(true).then((published: boolean) => {
                    if (published) {
                        authoring.rewrite($scope.item);
                    }
                }, (err) => {
                    notify.error(gettext('Failed to publish and continue.'));
                });
                initMedia();
            };

            // Close the current article, create an update of the article and open it in the edit mode.
            $scope.closeAndContinue = function() {
                $scope.close().then(() => {
                    authoring.rewrite($scope.item);
                });
            };

            $scope.deschedule = function() {
                $scope.item.publish_schedule = null;
                return $scope.save();
            };

            /**
             * Close an item - unlock
             */
            $scope.close = function() {
                _closing = true;

                // returned promise used by superdesk-fi
                return authoring.close($scope.item, $scope.origItem, $scope.save_enabled()).then(() => {
                    authoringWorkspace.close(true);
                    $rootScope.$broadcast('item:close', $scope.origItem._id);
                });
            };

            /**
             * Cancel an action, i.e. go back to view mode.
             * This is used in the 'EDIT AND CORRECT' and 'EDIT AND KILL' actions.
             * When the user clicks on the 'CANCEL' button it takes them back to the view action on the item
             */
            $scope.cancel = function() {
                authoringWorkspace.view($scope.origItem);
            };

            /*
             * Minimize an item
             */
            $scope.minimize = function() {
                authoringWorkspace.close(true);
            };

            $scope.closeOpenNew = function(createFunction, paramValue) {
                _closing = true;
                authoring.close($scope.item, $scope.origItem, $scope.dirty, true).then(() => {
                    createFunction(paramValue);
                });
            };

            /**
             * Called by the sendItem directive before send.
             * If the $scope is dirty then upon confirmation save the item and then unlock the item.
             * If the $scope is not dirty then unlock the item.
             * @param {String} action - action to display in confirmation dialog
             * @return {Object} promise
             */
            $scope.beforeSend = function(action) {
                $scope.sending = true;
                if ($scope.dirty) {
                    return confirm.confirmSendTo(action)
                        .then(() => $scope.save().then(() => lock.unlock($scope.origItem)), () => // cancel
                            $q.reject());
                }

                return lock.unlock($scope.origItem)
                    .catch(() => $scope.origItem); // ignore failed unlock
            };

            /**
             * Preview different version of an item
             */
            $scope.preview = function(version) {
                helpers.forcedExtend($scope.item, version);
                $scope.refreshTrigger++;
                $scope.isPreview = true;
                $scope._editable = false;
            };

            /**
             * Revert item to given version
             */
            $scope.revert = function(version) {
                $scope.isPreview = false;
                helpers.forcedExtend($scope.item, version);

                /**
                 * Before restoring, a version can be previewed in read only mode.
                 * For this to work, `_editable` is set to false.
                 * It has to be set back to true so the story is editable after reverting.
                 */
                $scope._editable = true;

                $scope.refreshTrigger++;
                if ($scope.item.annotations == null) {
                    $scope.item.annotations = [];
                }
                return $scope.save();
            };

            /**
             * Close preview and start working again
             */
            $scope.closePreview = function() {
                $scope.item = copyJson($scope.origItem);
                $scope._editable = $scope.action !== 'view' && authoring.isEditable($scope.origItem);

                if ($scope.isPreview) {
                    $scope.isPreview = false;
                    $scope.refreshTrigger++;
                }

                // populate content fields so that it can undo to initial (empty) version later
                var _autosave = $scope.origItem._autosave || {};

                Object.keys(helpers.CONTENT_FIELDS_DEFAULTS).forEach((key) => {
                    var value = _autosave[key] || $scope.origItem[key] || helpers.CONTENT_FIELDS_DEFAULTS[key];

                    $scope.item[key] = angular.copy(value);
                });
            };

            /**
             * Checks if the item can be unlocked or not.
             */
            $scope.can_unlock = function() {
                return !$scope.item.sendTo && lock.can_unlock($scope.item);
            };

            $scope.save_enabled = function() {
                confirm.dirty = $scope.dirty;

                return ($scope.dirty || $scope.item._autosave != null) &&
                    _.reduce($scope.isValidEmbed, (agg, val) => agg && val, true);
            };

            $scope.previewFormattedEnabled = function() {
                return !!_.get($rootScope.config, 'features.previewFormats');
            };

            // call the function to unlock and lock the story for editing.
            $scope.unlock = function() {
                $scope.unlockClicked = true;
                lock.unlock($scope.item).then((unlockedItem) => {
                    $scope.edit(unlockedItem);
                });
            };

            $scope.openAction = function(action) {
                if (action === 'correct') {
                    if (appConfig?.corrections_workflow &&
                    [ITEM_STATE.PUBLISHED, ITEM_STATE.CORRECTED].includes($scope.item.state)) {
                        $scope.isCorrectionInProgress = true;
                        authoring.correction($scope.item, () => $scope.isCorrectionInProgress = false);
                    } else {
                        authoringWorkspace.correct($scope.item);
                    }
                } else if (action === 'kill') {
                    authoringWorkspace.kill($scope.item);
                } else if (action === 'takedown') {
                    authoringWorkspace.takedown($scope.item);
                } else if (action === 'rewrite') {
                    authoring.rewrite($scope.item);
                } else if (action === 'unpublish') {
                    authoring.unpublish($scope.item);
                }
            };

            $scope.isLocked = function() {
                return lock.isLocked($scope.item);
            };

            $scope.isLockedByMe = function() {
                return lock.isLockedByMe($scope.item);
            };

            /**
             * On changing the content profile add the new (key, default-value) to the item
             * if new content profile has some additional keys than item
             *
             * @function changeProfile
             * @param {Object} item - item being edited currently
             */
            $scope.changeProfile = function(item) {
                angular.forEach($scope.content_types, (profile) => {
                    if (item.profile === profile._id && profile.schema) {
                        angular.forEach(profile.schema, (schema, key) => {
                            if (schema && schema.default && !(key in item)) {
                                item[key] = _.cloneDeep(schema.default);
                            }
                        });
                    }
                });

                $scope.autosave(item);
            };

            $scope.firstLineConfig = appConfig?.ui?.authoring?.firstLine ?? {};

            // default to true
            $scope.firstLineConfig.wordCount = $scope.firstLineConfig.wordCount ?? true;

            $scope.autosave = function(item, timeout) {
                $scope.dirty = true;
                angular.extend($scope.item, item); // make sure all changes are available

                return authoring.autosave(
                    $scope.item,
                    $scope.origItem,
                    timeout,
                ).then(
                    () => {
                        $scope.$applyAsync(() => {
                            authoringWorkspace.addAutosave();
                            initMedia();
                            updateSchema();
                        });
                    },
                );
            };

            $scope.sendToNextStage = function() {
                var currentDeskId = desks.getCurrentDeskId();

                if (currentDeskId == null) {
                    throw new Error('currentDeskId is null');
                }

                var stageIndex, stageList = desks.deskStages[currentDeskId];
                var selectedStage, selectedDesk = desks.deskLookup[currentDeskId];

                for (var i = 0; i < stageList.length; i++) {
                    if (stageList[i]._id === $scope.stage._id) {
                        selectedStage = stageList[i];
                        stageIndex = i + 1 === stageList.length ? 0 : i + 1;
                        break;
                    }
                }

                $rootScope.$broadcast('item:nextStage', {
                    stage: stageList[stageIndex],
                    itemId: $scope.item._id,
                    selectedStage: selectedStage,
                    selectedDesk: selectedDesk,
                    item: $scope.item,
                });
                $scope.close();
            };

            // Returns true if the given text is an URL
            $scope.isURL = (text) =>
                _.startsWith(_.lowerCase(_.trim(text)), 'http');
<commit_msg>FIX: To Desk feature fails when article was opened from search or monitoring widget. [SDESK-6161] (#3980)<commit_after>import * as helpers from 'apps/authoring/authoring/helpers';
import _ from 'lodash';
import {merge, flatMap} from 'lodash';
import postscribe from 'postscribe';
import thunk from 'redux-thunk';
import {gettext} from 'core/utils';
import {logger} from 'core/services/logger';
import {combineReducers, createStore, applyMiddleware} from 'redux';
import {applyMiddleware as coreApplyMiddleware} from 'core/middleware';
import {getArticleSchemaMiddleware} from '..';
import {isPublished} from 'apps/archive/utils';
import {AuthoringWorkspaceService} from '../services/AuthoringWorkspaceService';
import {copyJson} from 'core/helpers/utils';
import {appConfig, extensions} from 'appConfig';
import {onPublishMiddlewareResult, IExtensionActivationResult, IArticle} from 'superdesk-api';
import {mediaIdGenerator} from '../services/MediaIdGeneratorService';
import {addInternalEventListener} from 'core/internal-events';
import {validateMediaFieldsThrows} from '../controllers/ChangeImageController';
import {getLabelNameResolver} from 'apps/workspace/helpers/getLabelForFieldId';
import {ITEM_STATE} from 'apps/archive/constants';
import {isMediaType} from 'core/helpers/item';
import {confirmQuickPublish} from '../services/quick-publish-modal';

/**
 * @ngdoc directive
 * @module superdesk.apps.authoring
 * @name sdAuthoring
 *
 * @description
 *   This directive is responsible for generating superdesk content authoring form.
 */

AuthoringDirective.$inject = [
    'superdesk',
    'authoringWorkspace',
    'notify',
    'desks',
    'authoring',
    'api',
    'session',
    'lock',
    'privileges',
    'content',
    '$location',
    'referrer',
    'macros',
    '$q',
    'modal',
    'archiveService',
    'confirm',
    'reloadService',
    '$rootScope',
    'suggest',
    'editorResolver',
    'compareVersions',
    'embedService',
    '$injector',
    'autosave',
];
export function AuthoringDirective(
    superdesk,
    authoringWorkspace: AuthoringWorkspaceService,
    notify,
    desks,
    authoring,
    api,
    session,
    lock,
    privileges,
    content,
    $location,
    referrer,
    macros,
    $q,
    modal,
    archiveService,
    confirm,
    reloadService,
    $rootScope,
    suggest,
    editorResolver,
    compareVersions,
    embedService,
    $injector,
    autosave,
) {
    return {
        link: function($scope, elem, attrs) {
            $scope.loading = false;
            $scope.tabsPinned = false;

            var _closing;
            var mediaFields = {};
            var userDesks;

            const UNIQUE_NAME_ERROR = gettext('Error: Unique Name is not unique.');
            const MEDIA_TYPES = ['video', 'picture', 'audio'];
            const isPersonalSpace = $location.path() === '/workspace/personal';

            $scope.toDeskEnabled = false; // Send an Item to a desk
            $scope.closeAndContinueEnabled = false; // Create an update of an item and Close the item.
            $scope.publishEnabled = false; // publish an item
            $scope.publishAndContinueEnabled = false; // Publish an item and Create an update.

            desks.fetchCurrentUserDesks().then((desksList) => {
                userDesks = desksList;
                $scope.itemActions = authoring.itemActions($scope.origItem, userDesks);
            });

            $scope.privileges = privileges.privileges;
            $scope.dirty = false;
            $scope.views = {send: false};
            $scope.stage = null;
            $scope._editable = !!$scope.origItem._editable;
            $scope.isMediaType = isMediaType($scope.origItem);
            $scope.action = $scope.action || ($scope._editable ? 'edit' : 'view');

            $scope.highlight = !!$scope.origItem.highlight;
            $scope.showExportButton = $scope.highlight && $scope.origItem.type === 'composite';
            $scope.openSuggestions = () => suggest.setActive();
            $scope.openCompareVersions = (item) => compareVersions.init(item);
            $scope.isValidEmbed = {};
            $scope.embedPreviews = {};
            $scope.mediaFieldVersions = {};
            $scope.refreshTrigger = 0;
            $scope.isPreview = false;
            $scope.isCorrectionInProgress = false;

            $scope.$watch('origItem', (newValue, oldValue) => {
                $scope.itemActions = null;
                if (newValue) {
                    $scope.itemActions = authoring.itemActions(newValue, userDesks);
                }
            }, true);

            $scope.$watch('item.flags', (newValue, oldValue) => {
                if (newValue !== oldValue) {
                    $scope.item.flags = _.clone($scope.origItem.flags);
                    $scope.item.flags = newValue;
                    $scope.origItem.flags = oldValue;
                    $scope.dirty = true;
                }
            }, true);

            function checkShortcutButtonAvailability(personal = false) {
                if (personal) {
                    return appConfig?.features?.publishFromPersonal && $scope.item.state !== 'draft';
                }
                return $scope.item.task && $scope.item.task.desk && $scope.item.state !== 'draft' || $scope.dirty;
            }

            $scope.$watch('item', () => {
                $scope.toDeskEnabled = appConfig?.features?.customAuthoringTopbar?.toDesk
                && !isPersonalSpace && checkShortcutButtonAvailability();

                $scope.closeAndContinueEnabled = appConfig?.features?.customAuthoringTopbar?.closeAndContinue
                && !isPersonalSpace && checkShortcutButtonAvailability();

                $scope.publishEnabled = appConfig?.features?.customAuthoringTopbar?.publish
                    && canPublishOnDesk() && checkShortcutButtonAvailability(isPersonalSpace);

                $scope.publishAndContinueEnabled = appConfig?.features?.customAuthoringTopbar?.publishAndContinue
                    && !isPersonalSpace && canPublishOnDesk() && checkShortcutButtonAvailability();
            }, true);

            $scope._isInProductionStates = !isPublished($scope.origItem);

            $scope.fullPreview = false;
            $scope.proofread = false;
            $scope.referrerUrl = referrer.getReferrerUrl();
            $scope.gettext = gettext;

            content.getTypes().then(() => {
                $scope.content_types = content.types;
            });

            /**
             * Get the Desk and Stage for the item.
             */
            function getDeskStage() {
                if ($scope.origItem.task && $scope.origItem.task.stage) {
                    if (archiveService.isLegal($scope.origItem)) {
                        $scope.deskName = $scope.origItem.task.desk;
                        $scope.stage = $scope.origItem.task.stage;
                    } else {
                        api('stages').getById($scope.origItem.task.stage)
                            .then((result) => {
                                $scope.stage = result;
                            });

                        setDesk();
                    }
                }
            }

            /**
             * Get the Current Template for the item.
            */
            function getCurrentTemplate() {
                if (typeof $scope.item?.template !== 'string') {
                    logger.error(new Error('template must be present'));
                    return;
                }
                api('content_templates').getById($scope.item.template)
                    .then((result) => {
                        $scope.currentTemplate = result;
                    });
            }

            /**
            * Get the desk name and desk type.
            */
            function setDesk() {
                if (!$scope.item.task.desk) {
                    return false;
                }
                const desk = desks.getItemDesk($scope.item);

                if (desk) {
                    $scope.deskName = desk.name;
                    $scope.deskType = desk.desk_type;
                }
            }

            /**
             * Check if it is allowed to publish on desk
             * @returns {Boolean}
             */
            function canPublishOnDesk() {
                return !($scope.deskType === 'authoring' && appConfig.features.noPublishOnAuthoringDesk) &&
                    privileges.userHasPrivileges({publish: 1});
            }

            getDeskStage();
            getCurrentTemplate();
            /**
             * `desk_stage:change` event from send and publish action.
             * If send action succeeds but publish fails then we need change item location.
             */
            $scope.$on('desk_stage:change', getDeskStage);

            /**
             * Start editing current item
             */

            $scope.edit = function edit() {
                if ($scope.origItem.state === 'unpublished') {
                    api.update('archive', $scope.origItem, {state: 'in_progress'})
                        .then((updated) => {
                            // for updating and refreshing the item everywhere,
                            // close it first and then open it.
                            $scope.close().then(() => {
                                authoringWorkspace.edit(updated);
                            });
                        });
                } else if (isPublished($scope.origItem)) {
                    authoringWorkspace.view($scope.origItem);
                } else {
                    authoringWorkspace.edit($scope.origItem);
                }
            };

            /**
             * Create a new version
             */
            $scope.save = function() {
                return authoring.save($scope.origItem, $scope.item).then((res) => {
                    $scope.dirty = false;
                    _.merge($scope.item, res);

                    if (res.highlight) {
                        _previewHighlight(res._id);
                    }

                    notify.success(gettext('Item updated.'));

                    initMedia();
                    return $scope.origItem;
                }, (response) => {
                    if (response.status === 412) {
                        notifyPreconditionFailed();
                        return;
                    }

                    if (angular.isDefined(response.data._issues)) {
                        if (angular.isDefined(response.data._issues.unique_name) &&
                            response.data._issues.unique_name.unique === 1) {
                            notify.error(UNIQUE_NAME_ERROR);
                            return;
                        } else if (angular.isDefined(response.data._issues['validator exception'])) {
                            notify.error(gettext('Error: {{message}}',
                                {message: response.data._issues['validator exception']}));
                            return;
                        }
                    }

                    notify.error(gettext('Error. Item not updated.'));
                });
            };

            $scope.openFullPreview = function($event) {
                if ($event.button === 0 && !$event.ctrlKey) {
                    $event.preventDefault();
                    $scope.fullPreview = true;
                }
            };

            $scope.closeFullPreview = function() {
                $scope.fullPreview = false;
            };

            /**
             * Export the list of highlights as a text item.
             */
            $scope.exportHighlight = function(item) {
                if ($scope.save_enabled()) {
                    modal.confirm(gettext('You have unsaved changes, do you want to continue?'))
                        .then(() => {
                            _exportHighlight(item._id);
                        },
                        );
                } else {
                    _exportHighlight(item._id);
                }
            };

            function _exportHighlight(_id) {
                api.generate_highlights.save({}, {package: _id})
                    .then(authoringWorkspace.edit, (response) => {
                        if (response.status === 403) {
                            _forceExportHighlight(_id);
                        } else {
                            notify.error(gettext('Error creating highlight.'));
                        }
                    });
            }

            function _forceExportHighlight(_id) {
                modal.confirm(gettext('There are items locked or not published. Do you want to continue?'))
                    .then(() => {
                        api.generate_highlights.save({}, {package: _id, export: true})
                            .then(authoringWorkspace.edit, (response) => {
                                notify.error(gettext('Error creating highlight.'));
                            });
                    });
            }

            function _previewHighlight(_id) {
                api.generate_highlights.save({}, {package: _id, preview: true})
                    .then((response) => {
                        $scope.highlight_preview = response.body_html;
                    }, (data) => {
                        $scope.highlight_preview = data.message;
                    });
            }

            if ($scope.origItem.highlight) {
                _previewHighlight($scope.origItem._id);
            }

            /**
             * Invoked by validatePublishScheduleAndEmbargo() to validate the date and time values.
             *
             * @return {string} if the values are invalid then returns appropriate error message.
             *         Otherwise empty string.
             */
            function validateTimestamp(datePartOfTS, timePartOfTS, timestamp, timezone, fieldName) {
                var errors = authoring.validateSchedule(
                    datePartOfTS,
                    timePartOfTS,
                    timestamp,
                    timezone,
                    fieldName,
                );

                if (!errors) {
                    return;
                }

                if (errors.date) {
                    return gettext('{{field}} date is required!', {field: fieldName});
                }

                if (errors.time) {
                    return gettext('{{field}} time is required!', {field: fieldName});
                }

                if (errors.timestamp) {
                    return gettext('{{field}} is not a valid date!', {field: fieldName});
                }

                if (errors.future && fieldName !== 'Embargo' || $scope._isInProductionStates) {
                    return gettext('{{field}} cannot be earlier than now!', {field: fieldName});
                }
            }

            /**
             * Validates the Embargo Date and Time and Publish Schedule Date and Time if they are set on the item.
             *
             * @return {boolean} true if valid, false otherwise.
             */
            function validatePublishScheduleAndEmbargo(item) {
                if (item.embargo && item.publish_schedule) {
                    notify.error(gettext('An Item can\'t have both Embargo and Publish Schedule.'));
                    return false;
                }

                var errorMessage;

                if (item.embargo_date || item.embargo_time) {
                    errorMessage = validateTimestamp(
                        item.embargo_date, item.embargo_time, item.embargo,
                        item.schedule_settings ? item.schedule_settings.time_zone : null,
                        gettext('Embargo'));
                }

                if (item.publish_schedule_date || item.publish_schedule_time) {
                    if (isPublished(item, false)) {
                        return true;
                    }

                    errorMessage = validateTimestamp(
                        item.publish_schedule_date, item.publish_schedule_time,
                        item.publish_schedule, item.schedule_settings ? item.schedule_settings.time_zone : null,
                        gettext('Publish Schedule'));
                }

                if (errorMessage) {
                    notify.error(errorMessage);
                    return false;
                }

                return true;
            }

            /**
             * Checks if associations is with rewrite_of item then open then modal to add associations.
             * The user has options to add associated media to the current item and review the media change
             * or publish the current item without media.
             * User will be prompted in following scenarios:
             * 1. Edit feature image and confirm media update is enabled.
             * 2. Once item is published then no confirmation.
             * 3. If current item is update and updated story has associations
             */
            function checkMediaAssociatedToUpdate() {
                let rewriteOf = $scope.item.rewrite_of;

                if (!(appConfig.features != null && appConfig.features.confirmMediaOnUpdate) ||
                    !(appConfig.features != null && appConfig.features.editFeaturedImage) ||
                    !rewriteOf || _.includes(['kill', 'correct', 'takedown'], $scope.action) ||
                    $scope.item.associations && $scope.item.associations.featuremedia) {
                    return $q.when(true);
                }

                return api.find('archive', rewriteOf)
                    .then((rewriteOfItem) => {
                        if (rewriteOfItem && rewriteOfItem.associations &&
                            rewriteOfItem.associations.featuremedia) {
                            return confirm.confirmFeatureMedia(rewriteOfItem);
                        }
                        return true;
                    })
                    .then((result) => {
                        if (result && result.associations) {
                            $scope.item.associations = result.associations;
                            $scope.autosave($scope.item);
                            return false;
                        }

                        return true;
                    });
            }

            function getOnPublishMiddlewares()
            : Array<IExtensionActivationResult['contributions']['entities']['article']['onPublish']> {
                return flatMap(
                    Object.values(extensions).map(({activationResult}) => activationResult),
                    (activationResult) =>
                        activationResult.contributions != null
                        && activationResult.contributions.entities != null
                        && activationResult.contributions.entities.article != null
                        && activationResult.contributions.entities.article.onPublish != null
                            ? activationResult.contributions.entities.article.onPublish
                            : [],
                );
            }

            function publishItem(orig, item) {
                autosave.stop(item);

                var action = $scope.action === 'edit' ? 'publish' : $scope.action;
                const onPublishMiddlewares = getOnPublishMiddlewares();
                let warnings: Array<{text: string}> = [];
                const initialValue: Promise<onPublishMiddlewareResult> = Promise.resolve({});

                $scope.error = {};

                return onPublishMiddlewares.reduce(
                    (current, next) => {
                        return current.then((result) => {
                            if (result && result.warnings && result.warnings.length > 0) {
                                warnings = warnings.concat(result.warnings);
                            }

                            return next(Object.assign({
                                _id: _.get(orig, '_id'),
                                type: _.get(orig, 'type'),
                            }, item));
                        });
                    },
                    initialValue,
                )
                    .then((result) => {
                        if (result && result.warnings && result.warnings.length > 0) {
                            warnings = warnings.concat(result.warnings);
                        }

                        return result;
                    })
                    .then(() => checkMediaAssociatedToUpdate())
                    .then((result) => {
                        if (result && warnings.length < 1) {
                            return authoring.publish(orig, item, action);
                        }
                        return $q.reject(false);
                    })
                    .then((response: IArticle) => {
                        notify.success(gettext('Item published.'));
                        $scope.item = response;
                        $scope.dirty = false;
                        authoringWorkspace.close(true);
                        return true;
                    }, (response) => {
                        let issues = _.get(response, 'data._issues');

                        if (issues) {
                            if (angular.isDefined(issues['validator exception'])) {
                                var errors = issues['validator exception'];
                                var modifiedErrors = errors.replace(/\[/g, '')
                                    .replace(/\]/g, '')
                                    .split(',');

                                modifiedErrors.forEach((error) => {
                                    const message = _.trim(error, '\' ');
                                    // the message format is 'Field error text' (contains ')
                                    const field = message.split(' ')[0];

                                    $scope.error[field.toLocaleLowerCase()] = true;
                                    notify.error(message);
                                });

                                if (issues.fields) {
                                    Object.assign($scope.error, issues.fields);
                                }

                                $scope.$applyAsync(); // make $scope.error changes visible

                                if (errors.indexOf('9007') >= 0 || errors.indexOf('9009') >= 0) {
                                    authoring.open(item._id, true).then((res) => {
                                        $scope.origItem = res;
                                        $scope.dirty = false;
                                        $scope.item = copyJson($scope.origItem);
                                    });
                                }
                                return $q.reject(false);
                            }

                            if (issues.unique_name && issues.unique_name.unique) {
                                notify.error(UNIQUE_NAME_ERROR);
                                return $q.reject(false);
                            }
                        } else if (response && response.status === 412) {
                            notifyPreconditionFailed();
                            return $q.reject(false);
                        } else if (warnings.length > 0) {
                            warnings.forEach(
                                (warning) => {
                                    notify.error(warning.text);
                                },
                            );
                            return $q.reject(false);
                        }
                        return $q.reject(false);
                    });
            }

            function notifyPreconditionFailed() {
                notify.error(gettext('Item has changed since it was opened. ' +
                    'Please close and reopen the item to continue. ' +
                    'Regrettably, your changes cannot be saved.'));
                $scope._editable = false;
                $scope.dirty = false;
            }

            let getLabelForFieldId = (id) => id;

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

            function validateForPublish(item) {
                var validator = appConfig.validator_media_metadata;

                if (item.type === 'picture' || item.type === 'graphic') {
                    // required media metadata fields are defined in superdesk.config.js
                    try {
                        validateMediaFieldsThrows(validator, item, $scope.schema, getLabelForFieldId);
                    } catch (e) {
                        notify.error(e);
                        return false;
                    }
                }

                return true;
            }

            $scope.hideLiveSuggestions = function() {
                return $rootScope.config.features && $rootScope.config.features.hideLiveSuggestions;
            };

            $scope.openExport = function() {
                $scope.export = true;
            };

            $scope.canExport = function() {
                return $scope.privileges.content_export ? $scope.item.lock_user === session.identity._id &&
                    $scope.itemActions.export : false;
            };

            $scope.closeExport = function() {
                $scope.export = false;
            };

            $scope.useTansaProofing = function() {
                return $rootScope.config.features && $rootScope.config.features.useTansaProofing;
            };

            var deregisterTansa = $rootScope.$on('tansa:end', afterTansa);

            $scope.runTansa = function() {
                if (window.RunTansaProofing && appConfig.tansa != null) {
                    const _editor = editorResolver.get();

                    if (_editor && _editor.version() === '3') {
                        $('#editor3Tansa').html(_editor.getHtmlForTansa());
                    }

                    const profiles = appConfig.tansa.profiles || {};

                    if (profiles[$scope.item.language] != null) {
                        window.tansa.settings.profileId = profiles[$scope.item.language];
                    }

                    (function workAroundTansaSpellcheckerSelectionBug() {
                        /**
                         * The issue was that Tansa spell-checker would only check a single field, even if no input
                         * field was focused at the time of initializing the spell-checker.
                         *
                         * If spell-checking was cancelled, that field would receive focus.
                         *
                         * If the page was reloaded, or an article reopened - all fields were spell-checked as expected.
                         *
                         * The issue was only happening if an expanded text selection was made in input[type="text"]
                         * field at **any point** prior to initializing the spell-checker.
                         * Such a selection is made every time when focusing a
                         * non-empty input[type="text"] field using a tab key.
                         * Even if other text fields received focus after that,
                         * it would only spell-check the single field,
                         * that last had an expanded text selection performed on it.
                         *
                         * I assume Tansa spell-checker has a feature to only spell-check a selected part of the text
                         * and their JavaScript code has a bug where it can't differentiate between text that
                         * was intentionally selected for spell-checking from text that was selected long before
                         * initializing the spell-checker, even after multiple other input fields received focus.
                         */

                        Array.from(document.querySelectorAll('input[type="text"]')).forEach((el: HTMLInputElement) => {
                            // Making sure there are no expanded selections.
                            if (el !== document.activeElement) {
                                el.setSelectionRange(0, 0);
                            }
                        });
                    })();

                    window.RunTansaProofing();
                } else {
                    notify.error(gettext('Tansa is not responding. You can continue editing or publish the story.'));
                }
            };

            $scope.isRemovedField = function(fieldName) {
                return appConfig.infoRemovedFields != null && appConfig.infoRemovedFields.hasOwnProperty(fieldName);
            };

            function afterTansa(e, isCancelled) {
                const _editor = editorResolver.get();

                if (_editor && _editor.version() === '3' && !isCancelled) {
                    _editor.setHtmlFromTansa($('#editor3Tansa').html());
                }
            }

            /**
             * Depending on the item state one of the publish, correct, kill actions will be executed on the item
             * in $scope.
             */
            $scope.publish = function() {
                if (helpers.itemHasUnresolvedSuggestions($scope.item)) {
                    modal.alert({
                        headerText: gettext('Resolving suggestions'),
                        bodyText: gettext(
                            'Article cannot be published. Please accept or reject all suggestions first.',
                        ),
                    });

                    return Promise.reject();
                }

                if (helpers.itemHasUnresolvedComments($scope.item)) {
                    modal.confirm({
                        bodyText: gettext(
                            'This article contains unresolved comments.'
                            + 'Click on Cancel to go back to editing to'
                            + 'resolve those comments or OK to ignore and proceed with publishing',
                        ),
                        headerText: gettext('Resolving comments'),
                        okText: gettext('Ok'),
                        cancelText: gettext('Cancel'),
                    }).then((ok) => ok ? performPublish() : false);

                    return Promise.reject();
                }

                return performPublish();
            };

            function performPublish() {
                if (validatePublishScheduleAndEmbargo($scope.item) && validateForPublish($scope.item)) {
                    var message = 'publish';

                    if ($scope.action && $scope.action !== 'edit') {
                        message = $scope.action;
                    } else if ($scope.action === 'edit' && $scope.item.state === ITEM_STATE.CORRECTION) {
                        $scope.action = 'correct';
                    }

                    if ($scope.dirty && message === 'publish') {
                        // confirmation only required for publish
                        return authoring.publishConfirmation($scope.origItem, $scope.item, $scope.dirty, message)
                            .then((res) => {
                                if (res) {
                                    return publishItem($scope.origItem, $scope.item);
                                }
                            }, (response) => {
                                notify.error(gettext('Error. Item not published.'));
                                return $q.reject(false);
                            });
                    }

                    return publishItem($scope.origItem, $scope.item);
                }

                return false;
            }

            $scope.showCustomButtons = () => {
                return $scope.toDeskEnabled || $scope.closeAndContinueEnabled
                    || $scope.publishAndContinueEnabled || $scope.publishEnabled;
            };

            $scope.saveAndContinue = function(customButtonAction, showConfirm) {
                if ($scope.dirty) {
                    showConfirm ?
                        $scope.saveTopbar()
                            .then(() => confirmQuickPublish([$scope.item]))
                            .then(customButtonAction) :
                        $scope.saveTopbar()
                            .then(customButtonAction);
                } else {
                    showConfirm ?
                        confirmQuickPublish([$scope.item]).then(customButtonAction) :
                        customButtonAction();
                }
                initMedia();
            };

            $scope.publishAndContinue = function() {
                $scope.publish(true).then((published: boolean) => {
                    if (published) {
                        authoring.rewrite($scope.item);
                    }
                }, (err) => {
                    notify.error(gettext('Failed to publish and continue.'));
                });
                initMedia();
            };

            // Close the current article, create an update of the article and open it in the edit mode.
            $scope.closeAndContinue = function() {
                $scope.close().then(() => {
                    authoring.rewrite($scope.item);
                });
            };

            $scope.deschedule = function() {
                $scope.item.publish_schedule = null;
                return $scope.save();
            };

            /**
             * Close an item - unlock
             */
            $scope.close = function() {
                _closing = true;

                // returned promise used by superdesk-fi
                return authoring.close($scope.item, $scope.origItem, $scope.save_enabled()).then(() => {
                    authoringWorkspace.close(true);
                    $rootScope.$broadcast('item:close', $scope.origItem._id);
                });
            };

            /**
             * Cancel an action, i.e. go back to view mode.
             * This is used in the 'EDIT AND CORRECT' and 'EDIT AND KILL' actions.
             * When the user clicks on the 'CANCEL' button it takes them back to the view action on the item
             */
            $scope.cancel = function() {
                authoringWorkspace.view($scope.origItem);
            };

            /*
             * Minimize an item
             */
            $scope.minimize = function() {
                authoringWorkspace.close(true);
            };

            $scope.closeOpenNew = function(createFunction, paramValue) {
                _closing = true;
                authoring.close($scope.item, $scope.origItem, $scope.dirty, true).then(() => {
                    createFunction(paramValue);
                });
            };

            /**
             * Called by the sendItem directive before send.
             * If the $scope is dirty then upon confirmation save the item and then unlock the item.
             * If the $scope is not dirty then unlock the item.
             * @param {String} action - action to display in confirmation dialog
             * @return {Object} promise
             */
            $scope.beforeSend = function(action) {
                $scope.sending = true;
                if ($scope.dirty) {
                    return confirm.confirmSendTo(action)
                        .then(() => $scope.save().then(() => lock.unlock($scope.origItem)), () => // cancel
                            $q.reject());
                }

                return lock.unlock($scope.origItem)
                    .catch(() => $scope.origItem); // ignore failed unlock
            };

            /**
             * Preview different version of an item
             */
            $scope.preview = function(version) {
                helpers.forcedExtend($scope.item, version);
                $scope.refreshTrigger++;
                $scope.isPreview = true;
                $scope._editable = false;
            };

            /**
             * Revert item to given version
             */
            $scope.revert = function(version) {
                $scope.isPreview = false;
                helpers.forcedExtend($scope.item, version);

                /**
                 * Before restoring, a version can be previewed in read only mode.
                 * For this to work, `_editable` is set to false.
                 * It has to be set back to true so the story is editable after reverting.
                 */
                $scope._editable = true;

                $scope.refreshTrigger++;
                if ($scope.item.annotations == null) {
                    $scope.item.annotations = [];
                }
                return $scope.save();
            };

            /**
             * Close preview and start working again
             */
            $scope.closePreview = function() {
                $scope.item = copyJson($scope.origItem);
                $scope._editable = $scope.action !== 'view' && authoring.isEditable($scope.origItem);

                if ($scope.isPreview) {
                    $scope.isPreview = false;
                    $scope.refreshTrigger++;
                }

                // populate content fields so that it can undo to initial (empty) version later
                var _autosave = $scope.origItem._autosave || {};

                Object.keys(helpers.CONTENT_FIELDS_DEFAULTS).forEach((key) => {
                    var value = _autosave[key] || $scope.origItem[key] || helpers.CONTENT_FIELDS_DEFAULTS[key];

                    $scope.item[key] = angular.copy(value);
                });
            };

            /**
             * Checks if the item can be unlocked or not.
             */
            $scope.can_unlock = function() {
                return !$scope.item.sendTo && lock.can_unlock($scope.item);
            };

            $scope.save_enabled = function() {
                confirm.dirty = $scope.dirty;

                return ($scope.dirty || $scope.item._autosave != null) &&
                    _.reduce($scope.isValidEmbed, (agg, val) => agg && val, true);
            };

            $scope.previewFormattedEnabled = function() {
                return !!_.get($rootScope.config, 'features.previewFormats');
            };

            // call the function to unlock and lock the story for editing.
            $scope.unlock = function() {
                $scope.unlockClicked = true;
                lock.unlock($scope.item).then((unlockedItem) => {
                    $scope.edit(unlockedItem);
                });
            };

            $scope.openAction = function(action) {
                if (action === 'correct') {
                    if (appConfig?.corrections_workflow &&
                    [ITEM_STATE.PUBLISHED, ITEM_STATE.CORRECTED].includes($scope.item.state)) {
                        $scope.isCorrectionInProgress = true;
                        authoring.correction($scope.item, () => $scope.isCorrectionInProgress = false);
                    } else {
                        authoringWorkspace.correct($scope.item);
                    }
                } else if (action === 'kill') {
                    authoringWorkspace.kill($scope.item);
                } else if (action === 'takedown') {
                    authoringWorkspace.takedown($scope.item);
                } else if (action === 'rewrite') {
                    authoring.rewrite($scope.item);
                } else if (action === 'unpublish') {
                    authoring.unpublish($scope.item);
                }
            };

            $scope.isLocked = function() {
                return lock.isLocked($scope.item);
            };

            $scope.isLockedByMe = function() {
                return lock.isLockedByMe($scope.item);
            };

            /**
             * On changing the content profile add the new (key, default-value) to the item
             * if new content profile has some additional keys than item
             *
             * @function changeProfile
             * @param {Object} item - item being edited currently
             */
            $scope.changeProfile = function(item) {
                angular.forEach($scope.content_types, (profile) => {
                    if (item.profile === profile._id && profile.schema) {
                        angular.forEach(profile.schema, (schema, key) => {
                            if (schema && schema.default && !(key in item)) {
                                item[key] = _.cloneDeep(schema.default);
                            }
                        });
                    }
                });

                $scope.autosave(item);
            };

            $scope.firstLineConfig = appConfig?.ui?.authoring?.firstLine ?? {};

            // default to true
            $scope.firstLineConfig.wordCount = $scope.firstLineConfig.wordCount ?? true;

            $scope.autosave = function(item, timeout) {
                $scope.dirty = true;
                angular.extend($scope.item, item); // make sure all changes are available

                return authoring.autosave(
                    $scope.item,
                    $scope.origItem,
                    timeout,
                ).then(
                    () => {
                        $scope.$applyAsync(() => {
                            authoringWorkspace.addAutosave();
                            initMedia();
                            updateSchema();
                        });
                    },
                );
            };

            $scope.sendToNextStage = function() {
                var currentDeskId = $scope.item.task.desk;

                if (currentDeskId == null) {
                    throw new Error('currentDeskId is null');
                }

                var stageIndex, stageList = desks.deskStages[currentDeskId];
                var selectedStage, selectedDesk = desks.deskLookup[currentDeskId];

                for (var i = 0; i < stageList.length; i++) {
                    if (stageList[i]._id === $scope.stage._id) {
                        selectedStage = stageList[i];
                        stageIndex = i + 1 === stageList.length ? 0 : i + 1;
                        break;
                    }
                }

                $rootScope.$broadcast('item:nextStage', {
                    stage: stageList[stageIndex],
                    itemId: $scope.item._id,
                    selectedStage: selectedStage,
                    selectedDesk: selectedDesk,
                    item: $scope.item,
                });
                $scope.close();
            };

            // Returns true if the given text is an URL
            $scope.isURL = (text) =>
                _.startsWith(_.lowerCase(_.trim(text)), 'http');

{'fraction_non_alphanumeric': 0.1033501128276341, 'fraction_numerical': 0.0010414858531504947, 'mean_word_length': 1.2205776544351938, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 137, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 44, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5563336', 'n_tokens_mistral': 20690, 'n_tokens_neox': 19254, 'n_words': 4323}