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

[SDBELGA-759] Feature: Sync Event metadata to Planning & Coverages #1892

Merged
merged 13 commits into from
Jan 8, 2024
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,21 @@ Below sections include the config options that can be defined in settings.py.
* DEFAULT_CREATE_PLANNING_SERIES_WITH_EVENT_SERIES
* Default: False
* If true, will default to creating series of Planning items with a recurring series of Events,
* SYNC_EVENT_FIELDS_TO_PLANNING
* Default: ""
* Comma separated list of Planning & Coverage fields to keep in sync with the associated Event
* Supported Fields:
* slugline
* internal_note
* name
* place (list CVs)
* subject (list CVs, exclude items with scheme)
* custom_vocabularies (list CVs, inside subject with scheme)
* anpa_category (list CVs)
* ednote
* language (includes `languages` if multilingual is enabled)
* definition_short (copies to Planning item's `Description Text`)
* priority

### Assignments Config
* SLACK_BOT_TOKEN
Expand Down
116 changes: 73 additions & 43 deletions client/api/events.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import {
FILTER_TYPE,
IEventItem,
IPlanningAPI, IPlanningItem,
IPlanningAPI,
ISearchAPIParams,
ISearchParams,
ISearchSpikeState,
LOCK_STATE
IPlanningConfig,
} from '../interfaces';
import {appConfig as config} from 'appConfig';
import {IRestApiResponse} from 'superdesk-api';
import {planningApi, superdeskApi} from '../superdeskApi';
import {EVENTS, TEMP_ID_PREFIX} from '../constants';

import {arrayToString, convertCommonParams, cvsToString, searchRaw, searchRawGetAll} from './search';
import {eventUtils, planningUtils} from '../utils';
import {eventUtils} from '../utils';
import {eventProfile, eventSearchProfile} from '../selectors/forms';
import * as actions from '../actions';

const appConfig = config as IPlanningConfig;

function convertEventParams(params: ISearchParams): Partial<ISearchAPIParams> {
return {
reference: params.reference,
Expand Down Expand Up @@ -118,62 +121,89 @@ function getEventSearchProfile() {
return eventSearchProfile(planningApi.redux.store.getState());
}

function createOrUpdatePlannings(
event: IEventItem,
items: Array<Partial<IPlanningItem>>
): Promise<Array<IPlanningItem>> {
return Promise.all(
items.map(
(updates) => (
updates._id.startsWith(TEMP_ID_PREFIX) ?
planningApi.planning.createFromEvent(event, updates) :
planningApi.planning.getById(updates._id)
.then((original) => (
planningApi.planning.update(original, updates)
))
)
)
)
.then((newOrUpdatedItems) => {
newOrUpdatedItems.forEach(planningUtils.modifyForClient);

return newOrUpdatedItems;
});
}

function create(updates: Partial<IEventItem>): Promise<Array<IEventItem>> {
return superdeskApi.dataApi.create<IEventItem | IRestApiResponse<IEventItem>>('events', {
const url = appConfig.planning.default_create_planning_series_with_event_series === true ?
'events?add_to_series=true' :
'events';

return superdeskApi.dataApi.create<IEventItem | IRestApiResponse<IEventItem>>(url, {
...updates,
associated_plannings: undefined,
embedded_planning: updates.associated_plannings.map((planning) => ({
coverages: planning.coverages.map((coverage) => ({
coverage_id: coverage.coverage_id,
g2_content_type: coverage.planning.g2_content_type,
desk: coverage.assigned_to.desk,
user: coverage.assigned_to.user,
language: coverage.planning.language,
news_coverage_status: coverage.news_coverage_status.qcode,
scheduled: coverage.planning.scheduled,
genre: coverage.planning.genre?.qcode,
slugline: coverage.planning.slugline,
ednote: coverage.planning.ednote,
internal_note: coverage.planning.internal_note,
})),
})),
})
.then((response) => {
const events: Array<IEventItem> = modifySaveResponseForClient(response);

return createOrUpdatePlannings(events[0], updates.associated_plannings ?? [])
.then((plannings) => {
// Make sure to update the Redux Store with the latest Planning items
// So that the Editor can set the state with these latest items
planningApi.redux.store.dispatch<any>(actions.planning.api.receivePlannings(plannings));
const events = modifySaveResponseForClient(response);

return events;
});
return planningApi.planning.searchGetAll({
recurrence_id: events[0].recurrence_id,
event_item: events[0].recurrence_id != null ? null : events.map((event) => event._id),
spike_state: 'both',
only_future: false,
}).then((planningItems) => {
// Make sure to update the Redux Store with the latest Planning items
// So that the Editor can set the state with these latest items
planningApi.redux.store.dispatch<any>(actions.planning.api.receivePlannings(planningItems));

return events;
});
})
.catch((error) => {
console.error(error);

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

function update(original: IEventItem, updates: Partial<IEventItem>): Promise<Array<IEventItem>> {
return superdeskApi.dataApi.patch<any>('events', original, {
return superdeskApi.dataApi.patch<IEventItem>('events', original, {
...updates,
associated_plannings: undefined,
embedded_planning: updates.associated_plannings.map((planning) => ({
planning_id: planning._id.startsWith(TEMP_ID_PREFIX) ? undefined : planning._id,
coverages: planning.coverages.map((coverage) => ({
coverage_id: coverage.coverage_id,
g2_content_type: coverage.planning.g2_content_type,
desk: coverage.assigned_to.desk,
user: coverage.assigned_to.user,
language: coverage.planning.language,
news_coverage_status: coverage.news_coverage_status.qcode,
scheduled: coverage.planning.scheduled,
genre: coverage.planning.genre?.qcode,
slugline: coverage.planning.slugline,
ednote: coverage.planning.ednote,
internal_note: coverage.planning.internal_note,
})),
})),
})
.then((response) => {
const events = modifySaveResponseForClient(response);

return createOrUpdatePlannings(events[0], updates.associated_plannings ?? [])
.then((plannings) => {
planningApi.redux.store.dispatch<any>(actions.planning.api.receivePlannings(plannings));

return events;
});
return planningApi.planning.searchGetAll({
recurrence_id: events[0].recurrence_id,
event_item: events[0].recurrence_id != null ? null : events.map((event) => event._id),
spike_state: 'both',
only_future: false,
}).then((planningItems) => {
// Make sure to update the Redux Store with the latest Planning items
// So that the Editor can set the state with these latest items
planningApi.redux.store.dispatch<any>(actions.planning.api.receivePlannings(planningItems));

return events;
});
});
}

Expand Down
11 changes: 10 additions & 1 deletion client/api/locks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,16 @@ function unlockItem<T extends IAssignmentOrPlanningItem>(item: T, reloadLocksIfN
}
}

const lockedItemId = currentLock.item_id;
let lockedItemId: string;

if (item.type === 'event' && item.recurrence_id === currentLock.item_id) {
lockedItemId = item._id;
} else if (item.type === 'planning' && item.recurrence_id === currentLock.item_id) {
lockedItemId = item.event_item;
} else {
lockedItemId = currentLock.item_id;
}

const resource = getLockResourceName(currentLock.item_type);
const endpoint = `${resource}/${lockedItemId}/unlock`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,23 +171,21 @@ export class EmbeddedCoverageFormComponent extends React.PureComponent<IProps> {
<List.Row>
<Row
testId="user"
noPadding={true}
style={{padding: '2rem 0'}}
>
<Row style={{padding: '2rem 0'}}>
<SelectUser
onSelect={(user) => {
this.onUserChange(null, user);
}}
autoFocus={false}
horizontalSpacing={true}
clearable={true}
/>
</Row>
<SelectUser
onSelect={(user) => {
this.onUserChange(null, user);
}}
autoFocus={false}
horizontalSpacing={true}
clearable={true}
/>
</Row>
</List.Row>
<List.Row>
<Row>
{this.props.coverageProfile.language != null && (
{this.props.coverageProfile.language?.enabled !== true ? null : (
<List.Row>
<Row>
<Select
label={gettext('Language:')}
value={language}
Expand All @@ -205,9 +203,9 @@ export class EmbeddedCoverageFormComponent extends React.PureComponent<IProps> {
)
)}
</Select>
)}
</Row>
</List.Row>
</Row>
</List.Row>
)}
<List.Row>
<EditorFieldNewsCoverageStatus
testId="status"
Expand Down
17 changes: 17 additions & 0 deletions client/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,23 @@ export interface IEventItem extends IBaseRestApiResponse {
// Used only to add/modify Plannings/Coverages from the Event form
// These are only stored with the Autosave and not the actual Event
associated_plannings: Array<Partial<IPlanningItem>>;
embedded_planning: Array<{
planning_id?: IPlanningItem['_id'];
coverages: Array<{
coverage_id?: IPlanningCoverageItem['coverage_id'];
g2_content_type: ICoveragePlanningDetails['g2_content_type'];
desk: IPlanningAssignedTo['desk'];
user: IPlanningAssignedTo['user'];
language: ICoveragePlanningDetails['language'];
news_coverage_status: IPlanningNewsCoverageStatus['qcode'];
scheduled: ICoveragePlanningDetails['scheduled'];

genre: ICoveragePlanningDetails['genre']['qcode'];
slugline: ICoveragePlanningDetails['slugline'];
ednote: ICoveragePlanningDetails['ednote'];
internal_note: ICoveragePlanningDetails['internal_note'];
}>;
}>;

// Attributes added by API (removed via modifyForClient)
// The `_status` field is available when the item comes from a POST/PATCH request
Expand Down
2 changes: 1 addition & 1 deletion client/utils/strings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export function convertStringFields<Src extends IEventOrPlanningItem, Dest exten
}));

if (translationsDest.length > 0) {
itemDest.translations = (itemSrc.translations ?? []).concat(translationsDest);
itemDest.translations = (itemDest.translations ?? []).concat(translationsDest);
}

return itemDest;
Expand Down
2 changes: 1 addition & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
"scripts": {
"cypress-ui": "cypress open",
"cypress-ci": "cypress run",
"cypress-ci": "cypress run --browser chrome",
"clean": "grunt clean",
"build": "npx @superdesk/build-tools build-root-repo ./",
"serve": "node --max-old-space-size=8192 ./node_modules/.bin/grunt server"
Expand Down
Loading
Loading