public/components/stub-modal/stub-modal.js (465 lines of code) (raw):
import angular from 'angular';
import 'angular-bootstrap-temporary';
import _ from 'lodash';
import moment from 'moment';
import 'components/date-time-picker/date-time-picker';
import 'lib/composer-service';
import 'lib/content-service';
import 'lib/article-format-service';
import 'lib/legal-states-service';
import 'lib/picture-desk-states-service';
import 'lib/filters-service';
import 'lib/prodoffice-service';
import 'lib/telemetry-service';
import { punters } from 'components/punters/punters';
import { generateErrorMessages, doesContentTypeRequireCommissionedLength, useNativeFormFeedback } from '../../lib/stub-form-validation.ts';
import { setDisplayHintForFormat } from 'lib/model/special-formats.ts';
import { getArticleFormatLabel, isFormatLabel } from 'lib/model/format-helpers.ts';
const wfStubModal = angular.module('wfStubModal', [
'ui.bootstrap', 'articleFormatService', 'legalStatesService', 'pictureDeskStatesService', 'wfComposerService', 'wfContentService', 'wfDateTimePicker', 'wfProdOfficeService', 'wfFiltersService', 'wfCapiAtomService', 'wfTelemetryService'])
.directive('punters', ['$rootScope', punters]);
function StubModalInstanceCtrl($rootScope, $scope, $modalInstance, $window, config, stub, mode,
sections, statusLabels, articleFormatService, legalStatesService, pictureDeskStatesService, wfComposerService, wfProdOfficeService, wfContentService,
wfPreferencesService, wfFiltersService, sectionsInDesks, wfCapiAtomService, wfTelemetryService) {
wfContentService.getTypes().then( (types) => {
$scope.contentName =
(wfContentService.getAtomTypes())[stub.contentType] ?
"Atom" : (types[stub.contentType] || "News item");
$scope.stubFormat = getArticleFormatLabel(stub.contentType);
$scope.$watch('stub.articleFormat', (newValue) => {
$scope.stubFormat = newValue;
})
wfPreferencesService.getPreference('featureSwitches').then((data) => { $scope.showFormatDropdown = data;})
$scope.modalTitle = ({
'create': `Create ${$scope.contentName}`,
'edit': `Edit ${$scope.contentName}`,
'import': 'Import Existing Content'
})[mode];
});
$scope.stubFormatIsCorrectlyPopulated = function() {
return isFormatLabel($scope.stubFormat)
}
$scope.loadingTemplates = true;
wfComposerService.loadTemplates().then(templates => {
const sortedTemplates = _.sortBy(templates, 'title');
$scope.templates = sortedTemplates.map(({ title, dateCreated }) => {
// TODO MRB: Ideally Composer would give us back an opaque ID.
// It's like this for now so we can roll Composer and Workflow
// forward and back independently.
return {
id: `${title}_${dateCreated}`,
display: `${title} - ${moment(dateCreated).format("Do MMMM YYYY")}`
}
});
}).finally(() => {
$scope.loadingTemplates = false;
});
function getAtomDisplayName(type) {
switch (type) {
case 'media':
return 'Media';
case 'chart':
return 'Chart'
default:
return type;
}
}
function getAtomDropdownData() {
return _wfConfig.atomTypes.map(type => {
return { value: type, displayName: getAtomDisplayName(type) };
});
}
$scope.mode = mode;
$scope.formData = {};
$scope.disabled = !!stub.composerId;
$scope.sections = getSectionsList(sections);
$scope.templates = [];
$scope.statuses = statusLabels;
$scope.cdesks = _wfConfig.commissioningDesks;
$scope.atomTypes = getAtomDropdownData();
if(mode==='import') {
$scope.statuses = statusLabels;
}
$scope.stub = stub;
if ($scope.stub.section) {
/**
* To ensure that a modal loaded without a preference for section does not validate,
* only set the section if a preference was found
*/
$scope.stub.section = (function findSelectedSectionInAvailableSections (sect) {
const filteredSections = $scope.sections ? $scope.sections.filter((el) => (el ? el.name === sect.name : false)) : [];
if (filteredSections.length > 0) {
return filteredSections[0];
}
return sect;
})($scope.stub.section);
}
$scope.stub.status = 'Writers';
/**
* If the user currently has a desk selected then only
* show the sections that are part of that desk in the dropdown
* @param sections
* @returns Filtered list of sections
*/
function getSectionsList (sections) {
const filtered = sections.filter(({ selected }) => selected);
if (filtered.length === 0) {
return sections
}
return filtered
}
$scope.articleFormats = articleFormatService.getArticleFormats();
$scope.legalStates = legalStatesService.getLegalStates();
$scope.pictureDeskStates = pictureDeskStatesService.getpictureDeskStates();
$scope.prodOffices = wfProdOfficeService.getProdOffices();
$scope.$watch('stub.section', (newValue) => {
if (newValue) {
wfPreferencesService.getPreference('preferredStub').then((data) => {
data.section = newValue.name;
wfPreferencesService.setPreference('preferredStub', data);
}, () => {
wfPreferencesService.setPreference('preferredStub', {
section: newValue.name
});
})
}
}, true);
$scope.validImport = false;
$scope.wfComposerState;
$scope.warningMessages = undefined
$scope.$watch('stub', (newStub) => {
$scope.warningMessages = generateErrorMessages(newStub)
}, true)
$scope.isCommissionedLengthRequired = () => doesContentTypeRequireCommissionedLength($scope.stub.contentType);
$scope.requiredAttrForCommissionedLength = () => doesContentTypeRequireCommissionedLength($scope.stub.contentType) && !$scope.stub.missingCommissionedLengthReason ? 'true' : null
/* when a request is made to import an item from another tool,
* e.g. composer or an atom editor, then we will check to see if
* it is already being tracked by Workflow. If, this function will
* be called with the workflow entry as it's argument.
*/
function importHandleExisting(content) {
if(content.visibleOnUi) {
$scope.wfComposerState = 'visible';
$scope.stubId = res.data.data.id;
}
else {
$scope.wfComposerState = 'invisible'
}
}
function importComposerContent() {
wfComposerService.getComposerContent($scope.formData.importUrl)
.then((response) => wfComposerService.parseComposerData(response, $scope.stub))
.then((contentItem) => {
const composerId = contentItem.composerId;
if(composerId) {
$scope.composerUrl = config.composerViewContent + '/' + composerId;
$scope.stub.title = contentItem.headline;
// slice needed because the australian prodOffice is 'AUS' in composer and 'AU' in workflow
$scope.stub.prodOffice = contentItem.composerProdOffice ? contentItem.composerProdOffice.slice(0,2) : 'UK';
wfContentService.getById(composerId).then(
(res) => importHandleExisting(res.data.data),
(err) => {
if(err.status === 404) {
$scope.validImport = true;
}
});
}
}, () => {
$scope.actionSuccess = false;
});
}
function importContentAtom(id, atomType) {
wfCapiAtomService.getCapiAtom(id, atomType).then((response) => {
if(response) {
$scope.editorUrl = config.mediaAtomMakerViewAtom + id;
const atom = wfCapiAtomService.parseCapiAtomData(response, atomType);
$scope.stub.title = atom.title;
$scope.stub.contentType = atomType.toLowerCase();
$scope.stub.editorId = id;
wfContentService.getByEditorId(id).then(
(res) => importHandleExisting(res.data.data),
(err) => {
if(err.status === 404) {
$scope.validImport = true;
}
}
);
}
});
}
/* we can import from various different tools. Which one will be
* determined by the URL. This list matches URL regexes to
* functions which can handle the import. The first one that
* matches will be applied. The default fallback is Composer,
* which will match against everything and attempt to import. If
* that import fails the whole thing has failed. */
const importUrlHandlers = [
{ name: "Media Atom Maker",
regex: "videos/([0-9a-f-]+)$",
fn: (url, matches) => importContentAtom(matches[1], "media")
},
{
name: "Atom Workshop",
regex: /atoms\/([a-z]+)\/([0-9a-f-]+)/gi,
fn: (url, matches) => importContentAtom(matches[1], matches[2])
},
{ name: "Composer",
regex: "^.*$",
fn: importComposerContent
}
];
$scope.importUrlChanged = () => {
const url = $scope.formData.importUrl;
const handler = _.find(importUrlHandlers, (handlerObj) => {
return url.search(handlerObj.regex) !== -1;
});
if(handler) {
$scope.importHandler = handler;
$scope.importHandler.fn(url, url.match(handler.regex));
}
};
$scope.resetCommissionedLength = () => {
$scope.stub.commissionedLength = null;
}
$scope.resetMissingCommissionedLengthReason = () => {
$scope.stub.missingCommissionedLengthReason = null;
}
$scope.commissionedLengthSuggestions = [
400,
650,
900,
1200,
]
$scope.sendTelemetryForSuggestion = (value, missingCommissionedLengthReason = null) => {
const commissioningDesk = $scope.cdesks.find(desk => desk.id.toString() === stub.commissioningDesks)?.externalName;
const tags = {
contentId: stub.id,
productionOffice: stub.prodOffice,
commissioningDesk
}
if (missingCommissionedLengthReason) tags['missingCommissionedLengthReason'] = missingCommissionedLengthReason;
if(wfTelemetryService !== null && wfTelemetryService !== undefined) {
wfTelemetryService.sendTelemetryEvent(
"WORKFLOW_COMMISSIONED_LENGTH_SUGGESTION_PRESSED",
tags,
value
)
}
}
$scope.sendTelemetryForImport = (contentName) => {
if(contentName === 'Atom') {
return;
}
const tags = {
contentId: stub.composerId,
productionOffice: stub.prodOffice,
commissioningDesk: stub.section?.name,
commissionedLength: stub.commissionedLength,
contentType: stub.contentType
}
if(wfTelemetryService !== null && wfTelemetryService !== undefined) {
wfTelemetryService.sendTelemetryEvent(
"WORKFLOW_CONTENT_IMPORTED_FROM_COMPOSER",
tags,
true
)
}
}
$scope.setPriorityToVeryUrgent = () => {
$scope.stub.priority = 2;
}
$scope.submit = function (form) {
if (form.$invalid) {
useNativeFormFeedback($scope.stub)
return; // Form is not ready to submit
}
if ($scope.actionSuccess) { // Form has already been submitted successfully
if ($scope.composerUrl) {
window.open($scope.composerUrl, "_blank");
}
if ($scope.editorUrl) {
window.open($scope.editorUrl, "_blank");
}
$scope.cancel()
}
else {
const addToComposer = $scope.stub.status !== 'Stub' && $scope.contentName !== 'Atom';
const addToAtomEditor = !addToComposer && $scope.contentName === 'Atom' && $scope.stub.status !== 'Stub';
$scope.ok(addToComposer, addToAtomEditor);
}
};
$scope.updateCommissionedLengthInComposer = function() {
const commissionedLength = $scope.stub.commissionedLength;
const missingCommissionedLengthReason = $scope.stub.missingCommissionedLengthReason;
[null, undefined].includes(commissionedLength)
? wfComposerService.deleteFieldInPreviewAndLive(stub.composerId, 'commissionedLength')
: wfComposerService.updateFieldInPreviewAndLive(stub.composerId, 'commissionedLength', commissionedLength);
[null, undefined].includes(missingCommissionedLengthReason)
? wfComposerService.deleteFieldInPreviewAndLive(stub.composerId, 'missingCommissionedLengthReason')
: wfComposerService.updateFieldInPreviewAndLive(stub.composerId, 'missingCommissionedLengthReason', missingCommissionedLengthReason);
}
$scope.ok = function (addToComposer, addToAtomEditor) {
const stub = setDisplayHintForFormat ($scope.stub);
function createItemPromise() {
if ($scope.contentName === 'Atom') {
stub.contentType = $scope.stub.contentType.toLowerCase();
if (addToAtomEditor) {
return wfContentService.createInAtomEditor(stub);
} else if (stub.id) {
return wfContentService.updateStub(stub);
} else {
return wfContentService.createStub(stub);
}
} else {
if (addToComposer) {
return wfContentService.createInComposer(stub);
} else if (stub.id) {
return wfContentService.updateStub(stub);
} else {
return wfContentService.createStub(stub);
}
}
}
$scope.actionInProgress = true;
createItemPromise().then(() => {
const eventName = ({
'create': {
category: 'Stub',
action: 'Created',
value: {
'Created in Composer': stub.composerId
}
},
'edit': {
category: 'Stub',
action: 'Edited'
},
'import': {
category: 'Content',
action: 'Imported'
},
}[$scope.mode]);
$rootScope.$broadcast('track:event', eventName.category, eventName.action, null, null, Object.assign(
{}, {
'Section': stub.section,
'Content type': stub.contentType
}, eventName.value ? eventName.value : {})
);
$rootScope.$broadcast('getContent');
if ($scope.contentName === 'Atom') {
if (stub.editorId && ($scope.mode !== 'import')) {
$scope.editorUrl = wfContentService.getEditorUrl(stub.editorId, stub.contentType);
} else {
$modalInstance.close({
addToEditor: addToAtomEditor,
stub: $scope.stub
});
}
} else {
if(stub.composerId && ($scope.mode !== 'import')) {
$scope.composerUrl = config.composerViewContent + '/' + stub.composerId;
} else {
$modalInstance.close({
addToComposer: addToComposer,
stub: $scope.stub
});
}
}
$scope.actionSuccess = true;
$scope.actionInProgress = false;
}, (err) => {
$scope.actionSuccess = false;
$scope.contentUpdateError = true;
if(err.status === 409) {
if(err.data.composerId) {
$scope.composerUrl = config.composerViewContent + '/' + err.data.composerId;
}
if(err.data.editorId) {
$scope.editorUrl = wfContentService.getEditorUrl(stub.editorId, stub.contentType);
}
if(err.data.stubId) {
$scope.stubId = err.data.stubId;
}
} else {
$scope.actionSuccess = false;
}
$rootScope.$apply(() => { throw new Error('Stub ' + mode + ' failed: ' + (err.message || err)); });
$scope.actionInProgress = false;
});
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.delete = function () {
wfContentService.remove($scope.stub.id)
.then(() => {
$scope.$emit('content.deleted');
$scope.$emit('track:event', 'Content', 'Deleted', null, null, {
'Section': $scope.stub.contentItem.section,
'Content type': $scope.stub.contentItem.contentType
});
$scope.$apply();
$modalInstance.dismiss('cancel');
}, function (err) {
$scope.$apply(() => { throw err; });
});
};
}
wfStubModal.run([
'$window',
'$rootScope',
'$modal',
'$log',
'wfContentService',
'wfFiltersService',
'wfProdOfficeService',
'wfPreferencesService',
'wfLocationService',
'sections',
function ($window, $rootScope, $modal, $log, wfContentService, wfFiltersService, wfProdOfficeService, wfPreferencesService, wfLocationService, sections) {
const defaultAtomType = "media"
function currentFilteredOffice() {
return wfFiltersService.get('prodOffice');
}
function guessCurrentOfficeFromTimezone() {
return wfProdOfficeService.timezoneToOffice(wfLocationService.getCurrentLocation().id);
}
/**
* Return a promise for stub data based off the users stored preferences.
* Currently only modifies section for content creation
*
* @param contentType
* @returns {Promise}
*/
function setUpPreferredStub (contentType) {
function createStubData (contentType, sectionName) {
return {
articleFormat: getArticleFormatLabel(contentType),
contentType: contentType === "atom" ? defaultAtomType : contentType,
// Only send through a section if one is found in the prefs
section: sectionName === null ? sectionName : sections.filter((section) => section.name === sectionName)[0],
priority: 0,
needsLegal: 'NA',
needsPictureDesk: 'NA',
prodOffice: currentFilteredOffice() || guessCurrentOfficeFromTimezone()
};
}
return wfPreferencesService.getPreference('preferredStub').then((data) => {
return createStubData(contentType, data.section);
}, () => {
return createStubData(contentType, null);
});
}
$rootScope.$on('stub:edit', function (event, stub) {
open(stub, 'edit');
});
$rootScope.$on('stub:create', function (event, contentType) {
setUpPreferredStub(contentType).then((stub) => {
open(stub, 'create')
});
});
$rootScope.$on('content:import', function () {
setUpPreferredStub(null).then((stub) => {
open(stub, 'import')
});
});
function open(stub, mode) {
$modal.open({
templateUrl: '/assets/components/stub-modal/stub-modal.html',
controller: StubModalInstanceCtrl,
windowClass: 'stubModal',
resolve: {
stub: () => stub,
mode: () => mode
}
});
}
}]).directive('wfFocus', ['$timeout', function($timeout){
return {
restrict: "A",
link: function (scope, element, attrs) {
if(attrs.focusMe === "true" || attrs.focusMe === undefined) {
$timeout(function() { element[0].focus(); }, 500);
}
}
};
}]).directive('stringToNumber', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
ngModel.$parsers.push(function(value) {
return '' + value;
});
ngModel.$formatters.push(function(value) {
return parseFloat(value);
});
}
};
});