Skip to content

Commit

Permalink
Merge branch 'feature/multiple-events-in-planning' into async
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkLark86 committed Sep 25, 2024
2 parents abd9fb4 + a524be3 commit 0b53134
Show file tree
Hide file tree
Showing 104 changed files with 1,350 additions and 648 deletions.
13 changes: 13 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Front-end checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [ ] This pull request is adding missing TypeScript types to modified code segments where it's easy to do so with confidence
- [ ] This pull request is using TypeScript interfaces instead of prop-types and updates usages where it's quick to do so
- [ ] This pull request is using `memo` or `PureComponent` to define new React components (and updates existing usages in modified code segments)
- [ ] This pull request is replacing `lodash.get` with optional chaining and nullish coalescing for modified code segments
- [ ] This pull request is not importing anything from client-core directly (use `superdeskApi`)
- [ ] This pull request is importing UI components from `superdesk-ui-framework` and `superdeskApi` when possible instead of using ones defined in this repository.
- [ ] This pull request is not using `planningApi` where it is possible to use `superdeskApi`
- [ ] This pull request is not adding redux based modals
- [ ] In this pull request, properties of redux state are not being passed as props to components; instead, we connect it to the component that needs them. Except components where using a react key is required - do not connect those due to performance reasons.
- [ ] This pull request is not adding redux actions that do not modify state (e.g. only calling angular services; those should be moved to `planningApi`)
2 changes: 1 addition & 1 deletion .github/workflows/ci-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
CYPRESS_SCREENSHOTS_FOLDER: /tmp/cypress
- name: Upload screenshots
if: ${{ failure() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: screenshots-e2e-${{ matrix.e2e }}
path: /tmp/cypress/**/*.png
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ Below sections include the config options that can be defined in settings.py.
* language (includes `languages` if multilingual is enabled)
* definition_short (copies to Planning item's `Description Text`)
* priority
* PLANNING_DUPLICATE_RETAIN_ASSIGNEE_DETAILS
* Default: False (the current behavior where assignee details are removed)
* If true, the `assigned_to` field (assignee details) is retained when duplicating planning items with coverages.

### Assignments Config
* SLACK_BOT_TOKEN
Expand Down
4 changes: 4 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Long term refactoring goals

* Replace current event and planning item editor with react-based authoring component from superdesk (see `getAuthoringComponent` in `superdeskApi`)
* Refactor `EventItem` and `PlanningItem` components so they do not connect to a redux store
6 changes: 5 additions & 1 deletion TAGS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# TAG: MULTIPLE_PRIMARY_EVENTS

As we are changing the schema to allow more than one event to be associated to a planning item. To be able to do this gradually, we have introduced event linking types - primary and secondary. Secondary events will only be displayed as linked in the user interface, but will not be used for logic e.g. prevent spiking of a planning item. Events linked via a primary link type will work the same as a single related event used to work before we supported multiple linked events. It is only supported to have one related event that it using primary link. The goal is to eventually support linking multiple events using a primary link. The purpose of this tag is to mark places in the code where that support it missing.
As we are changing the schema to allow more than one event to be associated to a planning item. To be able to do this gradually, we have introduced event linking types - primary and secondary. Secondary events will only be displayed as linked in the user interface, but will not be used for logic e.g. prevent spiking of a planning item. Events linked via a primary link type will work the same as a single related event used to work before we supported multiple linked events. It is only supported to have one related event that it using primary link. The goal is to eventually support linking multiple events using a primary link. The purpose of this tag is to mark places in the code where that support it missing.

# TAG: AUTHORING-ANGULAR

AUTHORING-ANGULAR tag is meant to mark code that has to be removed together with angular based authoring component when time comes.
3 changes: 2 additions & 1 deletion client/actions/agenda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {AGENDA, MODALS, EVENTS} from '../constants';
import {getErrorMessage, gettext, planningUtils} from '../utils';
import {planning, showModal, main} from './index';
import {convertStringFields} from '../utils/strings';
import planningApis from '../actions/planning/api';

const openAgenda = () => (
(dispatch) => (
Expand Down Expand Up @@ -309,7 +310,7 @@ const createPlanningFromEvent = (
newPlanningItem.agendas = newPlanningItem.agendas.concat(agendas);

return (dispatch) => (
dispatch(planning.api.save({}, newPlanningItem))
dispatch(planningApis.save({}, newPlanningItem))
);
};

Expand Down
4 changes: 2 additions & 2 deletions client/actions/assignments/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {lockUtils, assignmentUtils, gettext, isExistingItem} from '../../utils';
import * as selectors from '../../selectors';
import assignments from './index';
import main from '../main';
import planning from '../planning';
import {hideModal, showModal} from '../index';
import * as actions from '../../actions';
import planningApis from '../planning/api';

const _notifyAssignmentEdited = (assignmentId) => (
(dispatch, getState, {notify}) => {
Expand Down Expand Up @@ -191,7 +191,7 @@ const _updatePlannigRelatedToAssignment = (data) => (
return Promise.resolve();
}

dispatch(planning.api.loadPlanningByIds([data.planning]));
dispatch(planningApis.loadPlanningByIds([data.planning]));
dispatch(main.fetchItemHistory(planningItem));
}
);
Expand Down
2 changes: 1 addition & 1 deletion client/actions/eventsPlanning/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const refetchPlanning = (planningId) => (
(dispatch, getState) => {
const storedPlannings = selectors.planning.storedPlannings(getState());
const plan = get(storedPlannings, planningId);
const relatedEventIds = getRelatedEventIdsForPlanning(plan, 'primary');
const relatedEventIds = getRelatedEventIdsForPlanning(plan);
const eventId = relatedEventIds.length > 0 ? relatedEventIds[0] : undefined;
const events = selectors.eventsPlanning.getRelatedPlanningsList(getState()) || {};

Expand Down
41 changes: 23 additions & 18 deletions client/actions/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {get, isEmpty, isEqual, isNil, omit} from 'lodash';
import moment from 'moment';

import {appConfig} from 'appConfig';
import {appConfig as config} from 'appConfig';

const appConfig = config as IPlanningConfig;

import {IUser} from 'superdesk-api';
import {planningApi, superdeskApi} from '../superdeskApi';
import {
Expand All @@ -18,6 +21,7 @@ import {
ITEM_TYPE,
IEventTemplate,
IEventItem,
IPlanningConfig,
} from '../interfaces';

import {
Expand Down Expand Up @@ -68,6 +72,7 @@ import eventsPlanningUi from './eventsPlanning/ui';
import * as selectors from '../selectors';
import {validateItem} from '../validators';
import {searchParamsToOld} from '../utils/search';
import {searchAndStore} from '../api/combined';

function openForEdit(item: IEventOrPlanningItem, updateUrl: boolean = true, modal: boolean = false) {
return (dispatch, getState) => {
Expand Down Expand Up @@ -716,7 +721,7 @@ const openIgnoreCancelSaveModal = ({
const storedItems = itemType === ITEM_TYPE.EVENT ?
selectors.events.storedEvents(getState()) :
selectors.planning.storedPlannings(getState());
const item = get(storedItems, itemId) || {};
const item = storedItems[itemId] ?? {};

if (!isExistingItem(item)) {
delete item._id;
Expand All @@ -725,19 +730,17 @@ const openIgnoreCancelSaveModal = ({
let promise = Promise.resolve(item);

if (itemType === ITEM_TYPE.EVENT && eventUtils.isEventRecurring(item)) {
const originalEvent = get(storedItems, itemId, {});

promise = dispatch(eventsApi.query({
recurrenceId: originalEvent.recurrence_id,
maxResults: appConfig.max_recurrent_events,
onlyFuture: false,
}))
.then((relatedEvents) => ({
...item,
_recurring: relatedEvents || [item],
_events: [],
_originalEvent: originalEvent,
}));
promise = searchAndStore({
recurrence_id: item.recurrence_id,
max_results: appConfig.max_recurrent_events,
only_future: false,
include_associated_planning: true,
}).then((relatedEvents) => ({
...item,
_recurring: relatedEvents.filter((item) => item.type === 'event') ?? [item],
_events: [],
_originalEvent: item,
}));
}

return promise.then((itemWithAssociatedData) => (
Expand Down Expand Up @@ -1199,8 +1202,10 @@ const openFromLockActions = () => (
if (action) {
/* get the item we're operating on */
dispatch(self.fetchById(sessionLastLock.item_id, sessionLastLock.item_type)).then((item) => {
actionUtils.getActionDispatches({dispatch: dispatch, eventOnly: false,
planningOnly: false})[action[0].actionName](item, false, false);
actionUtils.getActionDispatches({
dispatch: dispatch, eventOnly: false,
planningOnly: false
})[action[0].actionName](item, false, false);
});
}
}
Expand Down Expand Up @@ -1459,7 +1464,7 @@ function onItemUnlocked(
}));

if (getItemType(item) === ITEM_TYPE.PLANNING && selectors.general.currentWorkspace(state)
=== WORKSPACE.AUTHORING) {
=== WORKSPACE.AUTHORING) {
dispatch(self.closePreviewAndEditorForItems([item]));
}
}
Expand Down
4 changes: 2 additions & 2 deletions client/actions/planning/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ const fetchPlanningsEvents = (plannings: Array<IPlanningItem>) => (
const loadedEvents = selectors.events.storedEvents(getState());

const linkedEventIds = plannings
.map((plan) => getRelatedEventIdsForPlanning(plan, 'primary'))
.map((plan) => getRelatedEventIdsForPlanning(plan))
.flat()
.filter((eventId) => loadedEvents[eventId] == null);

Expand Down Expand Up @@ -482,7 +482,7 @@ const unpost = (original, updates) => (
* Also loads all the associated contacts (if any)
* @param {array, object} plannings - An array of planning item objects
*/
const receivePlannings = (plannings) => (
const receivePlannings = (plannings): any => (
(dispatch) => {
dispatch(actions.contacts.fetchContactsFromPlanning(plannings));
dispatch({
Expand Down
3 changes: 2 additions & 1 deletion client/actions/planning/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {events, fetchAgendas} from '../index';
import main from '../main';
import {showModal, hideModal} from '../index';
import eventsPlanning from '../eventsPlanning';
import planningApis from '../planning/api';

/**
* WS Action when a new Planning item is created
Expand Down Expand Up @@ -103,7 +104,7 @@ const onPlanningLocked = (e: {}, data: IWebsocketMessageData['ITEM_LOCKED']) =>

const sessionId = selectors.general.session(getState()).sessionId;

return dispatch(planning.api.getPlanning(data.item, false))
return dispatch(planningApis.getPlanning(data.item, false))
.then((planInStore) => {
let plan = {
...planInStore,
Expand Down
4 changes: 1 addition & 3 deletions client/actions/planning/tests/api_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import {
restoreSinonStub,
convertEventDatesToMoment,
} from '../../../utils/testUtils';
import {createTestStore} from '../../../utils';
import {PLANNING, SPIKED_STATE, WORKFLOW_STATE} from '../../../constants';
import {PLANNING, SPIKED_STATE} from '../../../constants';
import {MAIN} from '../../../constants';
import * as selectors from '../../../selectors';
import contactsApi from '../../contacts';
import {planningApis} from '../../../api';

Expand Down
5 changes: 2 additions & 3 deletions client/actions/planning/tests/notifications_test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {planningApi} from '../../../superdeskApi';
import planningApis from '../api';
import planningApis from '../../planning/api';
import planningUi from '../ui';
import featuredPlanning from '../featuredPlanning';
import eventsPlanningUi from '../../eventsPlanning/ui';
Expand Down Expand Up @@ -199,7 +199,7 @@ describe('actions.planning.notifications', () => {
describe('onPlanningLocked', () => {
beforeEach(() => {
sinon.stub(planningApi.locks, 'setItemAsLocked').returns(undefined);
sinon.stub(planningApis, 'getPlanning').returns(Promise.resolve(data.plannings[0]));
sinon.stub(planningApis, 'getPlanning').returns(() => Promise.resolve(data.plannings[0]));
});

afterEach(() => {
Expand All @@ -221,7 +221,6 @@ describe('actions.planning.notifications', () => {
))
.then(() => {
expect(planningApi.locks.setItemAsLocked.callCount).toBe(1);
expect(planningApis.getPlanning.callCount).toBe(1);
expect(planningApis.getPlanning.args[0]).toEqual([
'p1',
false,
Expand Down
2 changes: 1 addition & 1 deletion client/actions/planning/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ const duplicate = (plan) => (
.then((newPlan) => {
notify.success(gettext('Planning duplicated'));
const openInModal = selectors.forms.currentItemIdModal(getState());
const relatedEventIds = getRelatedEventIdsForPlanning(plan, 'primary');
const relatedEventIds = getRelatedEventIdsForPlanning(plan);

if (relatedEventIds.length > 0) {
dispatch(main.unlockAndCancel(plan)).then(() => {
Expand Down
17 changes: 15 additions & 2 deletions client/api/combined.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {
IEventOrPlanningItem,
} from '../interfaces';
import {IRestApiResponse} from 'superdesk-api';
import {searchRaw, searchRawGetAll, convertCommonParams, cvsToString, arrayToString} from './search';
import {searchRaw, searchRawGetAll, convertCommonParams, cvsToString, arrayToString, searchRawAndStore} from './search';
import {eventUtils, planningUtils} from '../utils';
import {planningApi} from '../superdeskApi';
import {combinedSearchProfile} from '../selectors/forms';
import {searchPlanningGetAll} from './planning';
import {searchPlanningGetAll, convertPlanningParams} from './planning';
import {searchEventsGetAll} from './events';

type IResponse = IRestApiResponse<IEventOrPlanningItem>;
Expand Down Expand Up @@ -67,6 +67,18 @@ export function searchCombinedGetAll(params: ISearchParams): Promise<Array<IEven
});
}

export function searchAndStore(params: ISearchParams) {
return searchRawAndStore<IEventOrPlanningItem>({
...convertCommonParams(params),
...convertPlanningParams(params),
repo: FILTER_TYPE.COMBINED,
}).then((res) => {
res._items.forEach(modifyItemForClient);

return res._items;
});
}

export function getEventsAndPlanning(params: ISearchParams): Promise<{
events: Array<IEventItem>;
plannings: Array<IPlanningItem>;
Expand Down Expand Up @@ -145,5 +157,6 @@ export const combined: IPlanningAPI['combined'] = {
getRecurringEventsAndPlanningItems: getRecurringEventsAndPlanningItems,
getEventsAndPlanning: getEventsAndPlanning,
getSearchProfile: getCombinedSearchProfile,
searchAndStore: searchAndStore,
};

2 changes: 1 addition & 1 deletion client/api/editor/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function getItemInstance(type: EDITOR_TYPE): IEditorAPI['item'] {
return Object.keys(plans)
.filter((planId) => (
plans[planId] != null &&
getRelatedEventIdsForPlanning(plans[planId], 'primary').includes(eventId))
getRelatedEventIdsForPlanning(plans[planId]).includes(eventId))
)
.map((planId) => cloneDeep(plans[planId]));
}
Expand Down
2 changes: 1 addition & 1 deletion client/api/editor/item_planning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function getPlanningInstance(type: EDITOR_TYPE): IEditorAPI['item']['plan
const profile = planningApi.contentProfiles.get('planning');
const groups = getEditorFormGroupsFromProfile(profile);

if (getRelatedEventLinksForPlanning(item, 'primary').length === 0) {
if (getRelatedEventLinksForPlanning(item).length === 0) {
delete groups['associated_event'];
}
const bookmarks = getBookmarksFromFormGroups(groups);
Expand Down
6 changes: 3 additions & 3 deletions client/api/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {EVENTS, TEMP_ID_PREFIX} from '../constants';
import {arrayToString, convertCommonParams, cvsToString, searchRaw, searchRawGetAll} from './search';
import {eventUtils} from '../utils';
import {eventProfile, eventSearchProfile} from '../selectors/forms';
import * as actions from '../actions';
import planningApis from '../actions/planning/api';

const appConfig = config as IPlanningConfig;

Expand Down Expand Up @@ -161,7 +161,7 @@ function create(updates: Partial<IEventItem>): Promise<Array<IEventItem>> {
}).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));
planningApi.redux.store.dispatch<any>(planningApis.receivePlannings(planningItems));

return events;
});
Expand Down Expand Up @@ -207,7 +207,7 @@ function update(original: IEventItem, updates: Partial<IEventItem>): Promise<Arr
}).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));
planningApi.redux.store.dispatch<any>(planningApis.receivePlannings(planningItems));

return events;
});
Expand Down
10 changes: 5 additions & 5 deletions client/api/planning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import {planningProfile, planningSearchProfile} from '../selectors/forms';
import {featured} from './featured';
import {PLANNING} from '../constants';
import * as selectors from '../selectors';
import * as actions from '../actions';
import planningApis from '../actions/planning/api';

const appConfig = config as IPlanningConfig;

function convertPlanningParams(params: ISearchParams): Partial<ISearchAPIParams> {
export function convertPlanningParams(params: ISearchParams): Partial<ISearchAPIParams> {
return {
agendas: arrayToString(params.agendas),
no_agenda_assigned: params.no_agenda_assigned,
Expand Down Expand Up @@ -90,7 +90,7 @@ export function getPlanningById(
.then(modifyItemForClient)
.then((item) => {
if (saveToStore) {
dispatch<any>(actions.planning.api.receivePlannings([item]));
dispatch<any>(planningApis.receivePlannings([item]));
}

return item;
Expand Down Expand Up @@ -223,7 +223,7 @@ function bulkAddCoverageToWorkflow(planningItems: Array<IPlanningItem>): Promise

return planning.update(plan, updates)
.then((updatedPlan) => {
dispatch<any>(actions.planning.api.receivePlannings([updatedPlan]));
dispatch<any>(planningApis.receivePlannings([updatedPlan]));

return updatedPlan;
});
Expand Down Expand Up @@ -262,7 +262,7 @@ function addCoverageToWorkflow(
return planning.update(plan, updates)
.then((updatedPlan) => {
notify.success(gettext('Coverage added to workflow.'));
dispatch<any>(actions.planning.api.receivePlannings([updatedPlan]));
dispatch<any>(planningApis.receivePlannings([updatedPlan]));

return updatedPlan;
})
Expand Down
Loading

0 comments on commit 0b53134

Please sign in to comment.