Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Publish and continue #4231

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,685 changes: 832 additions & 853 deletions package-lock.json

Large diffs are not rendered by default.

206 changes: 194 additions & 12 deletions scripts/api/article.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import {IArticle, IDangerousArticlePatchingOptions, IDesk, IStage} from 'superdesk-api';
import {patchArticle} from './article-patch';
import ng from 'core/services/ng';
import {httpRequestJsonLocal} from 'core/helpers/network';
import {applicationState, openArticle} from 'core/get-superdesk-api-implementation';
import {ISendToDestinationDesk, ISendToDestination} from 'core/interactive-article-actions-panel/interfaces';
import {fetchItems, fetchItemsToCurrentDesk} from './article-fetch';
import {IPublishingDateOptions} from 'core/interactive-article-actions-panel/subcomponents/publishing-date-options';
import {sendItems} from './article-send';
import {duplicateItems} from './article-duplicate';
import {sdApi} from 'api';
import {appConfig} from 'appConfig';
import {KILLED_STATES, ITEM_STATE, PUBLISHED_STATES} from 'apps/archive/constants';
import {appConfig, extensions} from 'appConfig';
import {ITEM_STATE, KILLED_STATES, PUBLISHED_STATES} from 'apps/archive/constants';
import {applicationState, openArticle} from 'core/get-superdesk-api-implementation';
import {dataApi} from 'core/helpers/CrudManager';
import {httpRequestJsonLocal} from 'core/helpers/network';
import {assertNever} from 'core/helpers/typescript-helpers';
import {copyJson} from 'core/helpers/utils';
import {ISendToDestination, ISendToDestinationDesk} from 'core/interactive-article-actions-panel/interfaces';
import {IPublishingDateOptions} from 'core/interactive-article-actions-panel/subcomponents/publishing-date-options';
import {notify} from 'core/notify/notify';
import ng from 'core/services/ng';
import {gettext} from 'core/utils';
import {flatMap, trim} from 'lodash';
import {IArticle, IDangerousArticlePatchingOptions, IDesk, IStage, onPublishMiddlewareResult} from 'superdesk-api';
import {duplicateItems} from './article-duplicate';
import {fetchItems, fetchItemsToCurrentDesk} from './article-fetch';
import {patchArticle} from './article-patch';
import {sendItems} from './article-send';
import {authoringApiCommon} from 'apps/authoring-bridge/authoring-api-common';

const isLocked = (_article: IArticle) => _article.lock_session != null;
const isLockedInCurrentSession = (_article: IArticle) => _article.lock_session === ng.get('session').sessionId;
Expand Down Expand Up @@ -175,6 +180,171 @@ function createNewUsingDeskTemplate(): void {
});
}

/**
* 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(
item: IArticle,
action: string,
autosave: (item: IArticle) => void,
): Promise<boolean> {
if (!appConfig.features?.confirmMediaOnUpdate
|| !appConfig.features?.editFeaturedImage
|| !item.rewrite_of
|| ['kill', 'correct', 'takedown'].includes(action)
|| item.associations?.featuremedia
) {
return Promise.resolve(true);
}

return ng.get('api').find('archive', item.rewrite_of)
.then((rewriteOfItem) => {
if (rewriteOfItem?.associations?.featuremedia) {
return ng.get('confirm').confirmFeatureMedia(rewriteOfItem);
}

return true;
})
.then((result) => {
if (result?.associations) {
item.associations = result.associations;
autosave(item);
return false;
}

return true;
});
}

function notifyPreconditionFailed($scope: any) {
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;
}

interface IScope {
item?: IArticle;
error?: {};
autosave?: (item: IArticle) => void;
dirty?: boolean;
$applyAsync?: () => void;
origItem?: IArticle;
}

function publishItem(orig: IArticle, item: IArticle): Promise<boolean | IArticle> {
const scope: IScope = {};

return publishItem_legacy(orig, item, scope)
.then((published) => published ? scope.item : published);
}

function canPublishOnDesk(deskType: string): boolean {
return !(deskType === 'authoring' && appConfig.features.noPublishOnAuthoringDesk) &&
ng.get('privileges').privileges.userHasPrivileges({publish: 1});
}

function showPublishAndContinue(item: IArticle, dirty: boolean): boolean {
return appConfig.features?.customAuthoringTopbar?.publishAndContinue
&& sdApi.navigation.isPersonalSpace()
&& canPublishOnDesk(sdApi.desks.getDeskById(sdApi.desks.getCurrentDeskId()).desk_type)
&& authoringApiCommon.checkShortcutButtonAvailability(item, dirty, sdApi.navigation.isPersonalSpace());
}

function publishItem_legacy(
orig: IArticle,
item: IArticle,
scope: IScope,
action: string = 'publish',
): Promise<boolean> {
let warnings: Array<{text: string}> = [];
const initialValue: Promise<onPublishMiddlewareResult> = Promise.resolve({});

scope.error = {};

return flatMap(
Object.values(extensions).map(({activationResult}) => activationResult),
(activationResult) => activationResult.contributions?.entities?.article?.onPublish ?? [],
).reduce((current, next) => {
return current.then((result) => {
if ((result?.warnings?.length ?? 0) > 0) {
warnings = warnings.concat(result.warnings);
}

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

return result;
})
.then(() => checkMediaAssociatedToUpdate(item, action, scope.autosave))
.then((result) => (result && warnings.length < 1
? ng.get('authoring').publish(orig, item, action)
: Promise.reject(false)
))
.then((response: IArticle) => {
notify.success(gettext('Item published.'));
scope.item = response;
scope.dirty = false;
ng.get('authoringWorkspace').close(true);

return true;
})
.catch((response) => {
const issues = response.data._issues;
const errors = issues?.['validator exception'];

if (errors != null) {
const 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 (errors.fields) {
Object.assign(scope.error, errors.fields);
}

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

if (errors.indexOf('9007') >= 0 || errors.indexOf('9009') >= 0) {
ng.get('authoring').open(item._id, true).then((res) => {
scope.origItem = res;
scope.dirty = false;
scope.item = copyJson(scope.origItem);
});
}
} else if (issues?.unique_name?.unique) {
notify.error(gettext('Error: Unique Name is not unique.'));
} else if (response && response.status === 412) {
notifyPreconditionFailed(scope);
} else if (warnings.length > 0) {
warnings.forEach((warning) => notify.error(warning.text));
}

return Promise.reject(false);
});
}

/**
* Gets opened items from your workspace.
*/
Expand Down Expand Up @@ -269,6 +439,14 @@ interface IArticleApi {

createNewUsingDeskTemplate(): void;
getWorkQueueItems(): Array<IArticle>;
canPublishOnDesk(deskType: string): boolean;
showPublishAndContinue(item: IArticle, dirty: boolean): boolean;
publishItem_legacy(orig: IArticle, item: IArticle, $scope: any, action?: string): Promise<boolean>;

// Instead of passing a fake scope from React
// every time to the publishItem_legacy we can use this function which
// creates a fake scope for us.
publishItem(orig: IArticle, item: IArticle): Promise<boolean | IArticle>;
}

export const article: IArticleApi = {
Expand Down Expand Up @@ -298,4 +476,8 @@ export const article: IArticleApi = {
createNewUsingDeskTemplate,
getWorkQueueItems,
get,
canPublishOnDesk,
showPublishAndContinue,
publishItem_legacy,
publishItem,
};
5 changes: 5 additions & 0 deletions scripts/api/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ function currentPathStartsWith(
return true;
}

function isPersonalSpace(): boolean {
return !(ng.get('$location').path() === '/workspace/personal');
}

export const navigation = {
getPath,
currentPathStartsWith,
isPersonalSpace,
};
9 changes: 9 additions & 0 deletions scripts/apps/authoring-bridge/authoring-api-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {ITEM_STATE} from 'apps/archive/constants';
import {isArticleLockedInCurrentSession} from 'core/get-superdesk-api-implementation';
import ng from 'core/services/ng';
import {runBeforeUpdateMiddlware, runAfterUpdateEvent} from 'apps/authoring/authoring/services/authoring-helpers';
import {appConfig} from 'appConfig';

export interface IAuthoringApiCommon {
saveBefore(current: IArticle, original: IArticle): Promise<IArticle>;
Expand All @@ -23,12 +24,20 @@ export interface IAuthoringApiCommon {
* and item is not locked.
*/
closeAuthoringForce(): void;
checkShortcutButtonAvailability: (item: IArticle, dirty?: boolean, personal?: boolean) => boolean
}

/**
* Immutable API that is used in both - angularjs and reactjs based authoring code.
*/
export const authoringApiCommon: IAuthoringApiCommon = {
checkShortcutButtonAvailability: (item: IArticle, dirty?: boolean, personal?: boolean): boolean => {
if (personal) {
return appConfig?.features?.publishFromPersonal && item.state !== 'draft';
}

return item.task && item.task.desk && item.state !== 'draft' || dirty;
},
saveBefore: (current, original) => {
return runBeforeUpdateMiddlware(current, original);
},
Expand Down
29 changes: 29 additions & 0 deletions scripts/apps/authoring-react/authoring-angular-integration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from 'core/interactive-article-actions-panel/index-hoc';
import {IArticleActionInteractive} from 'core/interactive-article-actions-panel/interfaces';
import {dispatchInternalEvent} from 'core/internal-events';
import {notify} from 'core/notify/notify';

export interface IProps {
itemId: IArticle['_id'];
Expand Down Expand Up @@ -204,6 +205,34 @@ function getInlineToolbarActions(options: IExposedFromAuthoring<IArticle>): IAut
availableOffline: false,
});

if (sdApi.article.showPublishAndContinue(item, hasUnsavedChanges())) {
actions.push({
group: 'middle',
priority: 0.3,
component: ({entity}) => (
<Button
type="highlight"
onClick={() => {
const getLatestItem = hasUnsavedChanges()
? handleUnsavedChanges()
: Promise.resolve(entity);

getLatestItem.then((article) => {
sdApi.article.publishItem(article, article).then((result) => {
typeof result !== 'boolean'
? ng.get('authoring').rewrite(result)
: notify.error(gettext('Failed to publish and continue.'));
});
});
}}
text={gettext('P & C')}
style="filled"
/>
),
availableOffline: false,
});
}

// FINISH: ensure locking is available in generic version of authoring
actions.push({
group: 'start',
Expand Down
Loading