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

Adding related items to plannings and events #2110

Open
wants to merge 16 commits into
base: feature/multiple-events-in-planning
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ module.exports = Object.assign({}, sharedConfigs, {
'camelcase': 0,
'no-prototype-builtins': 0, // allow hasOwnProperty
'react/prop-types': 0, // using interfaces

// can make functions harder to read; forces into rewriting the function to insert a debugger
'arrow-body-style': 0,
},
},
{
Expand Down
72 changes: 69 additions & 3 deletions client/actions/events/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {get, isEqual, cloneDeep, pickBy, has, find, every, take} from 'lodash';
import {get, cloneDeep, has, find, every, take} from 'lodash';

import {planningApi} from '../../superdeskApi';
import {ISearchSpikeState, IEventSearchParams, IEventItem, IPlanningItem, IEventTemplate} from '../../interfaces';
Expand All @@ -19,14 +19,16 @@ import {
isPublishedItemId,
isTemporaryId,
gettext,
getTimeZoneOffset,
} from '../../utils';

import planningApis from '../planning/api';
import eventsUi from './ui';
import main from '../main';
import {eventParamsToSearchParams} from '../../utils/search';
import {getRelatedEventIdsForPlanning} from '../../utils/planning';
import {planning, searchPlanning} from '../../api/planning';
import {IRestApiResponse} from 'superdesk-api';
import * as actions from '../../actions';

/**
* Action dispatcher to load a series of recurring events into the local store.
Expand Down Expand Up @@ -547,6 +549,58 @@ const uploadFiles = (event) => (
}
);

function getLinkedPlanningItems(eventId: string): Promise<IRestApiResponse<IPlanningItem>> {
return searchPlanning({only_future: false, event_item: [eventId]});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking this function might be useful in other places, so it would be good to have this available under IPlanningAPI.events.getLinkedPlanningItems (to be implemented in client/api/events.ts)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be good to just return Array<IPlanningItem> instead of IRestApiResponse<IPlanningItem>? The function name states get me the list of Planning items, or do you see the need for attributes under the IRestApiResponse as well?

}

function updateLinkedPlanningsForEvent(
eventId: IEventItem['_id'],

/**
* these must be final values
* missing items will be linked, extra items unlinked
*/
planningIds: Array<IPlanningItem['_id']>,
):Promise<void> {
return getLinkedPlanningItems(eventId).then((res) => {
// note: planning items to which `eventId` is linked
const currentlyLinked: Array<IPlanningItem> = res._items;

const currentLinkedIds = new Set(currentlyLinked.map((item) => item._id));
const toLink: Array<IPlanningItem['_id']> = planningIds.filter((id) => currentLinkedIds.has(id) !== true);
const toUnlink = currentlyLinked.filter((planningItem) => planningIds.includes(planningItem._id) !== true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set.has and Array.includes already return a boolean value, what is the use case for explicitly checking for !== true?


return Promise.all(
[
...toLink.map((planningId) => {
return planning.getById(planningId).then((planningItem) => {
const patch: Partial<IPlanningItem> = {
related_events: [
...(planningItem.related_events ?? []),
{_id: eventId, link_type: 'secondary'},
],
};

return planning.update(planningItem, patch);
});
}),
...toUnlink.map((planningItem) => {
const patch: Partial<IPlanningItem> = {
related_events: (planningItem.related_events ?? [])
.filter((item) => item._id !== eventId),
};

return planning.update(planningItem, patch);
}),
],
).then((updatedPlanningItems) => {
planningApi.redux.store.dispatch<any>(actions.planning.api.receivePlannings(updatedPlanningItems));

return null;
});
});
}

const save = (original, updates) => (
(dispatch) => {
let promise;
Expand Down Expand Up @@ -574,9 +628,21 @@ const save = (original, updates) => (
EVENTS.UPDATE_METHODS[0].value :
eventUpdates.update_method?.value ?? eventUpdates.update_method;

return originalEvent?._id != null ?
const createOrUpdatePromise = originalEvent?._id != null ?
planningApi.events.update(originalItem, eventUpdates) :
planningApi.events.create(eventUpdates);

return createOrUpdatePromise.then((updatedEvents) => {
if (updates.associated_plannings == null) {
return Promise.resolve();
}

const nextAssociatedPlanningIds = updates.associated_plannings.map(({_id}) => _id);

return Promise.all(
updatedEvents.map((event) => updateLinkedPlanningsForEvent(event._id, nextAssociatedPlanningIds)),
).then(() => updatedEvents);
});
});
}
);
Expand Down
9 changes: 8 additions & 1 deletion client/api/editor/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,15 @@ export function getEventsInstance(type: EDITOR_TYPE): IEditorAPI['events'] {
}

function setEventsPlanningsToAdd(newState: Partial<IEditorState>) {
const associatedPlannings = planningApi.editor(type).item.getAssociatedPlannings();

if (newState.diff.type === 'event' && newState.diff.associated_plannings == null) {
newState.diff.associated_plannings = planningApi.editor(type).item.getAssociatedPlannings();
newState.diff.associated_plannings = associatedPlannings;
}

// needs to be set on initial values as well for correct computation of state.dirty
if (newState.initialValues.type === 'event' && newState.initialValues.associated_plannings == null) {
newState.initialValues.associated_plannings = associatedPlannings;
}
}

Expand Down
4 changes: 2 additions & 2 deletions client/api/editor/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ export function getFormInstance(type: EDITOR_TYPE): IEditorAPI['form'] {
return planningApi.editor(type).manager.getState();
}

function scrollToBookmarkGroup(bookmarkId: IEditorBookmarkGroup['group_id']) {
function scrollToBookmarkGroup(bookmarkId: IEditorBookmarkGroup['group_id'], options?: {focus?: boolean}) {
const editor = planningApi.editor(type);

editor.dom.groups[bookmarkId]?.current?.scrollIntoView();
editor.dom.groups[bookmarkId]?.current?.scrollIntoView({focus: options?.focus ?? true});
}

function scrollToTop() {
Expand Down
7 changes: 6 additions & 1 deletion client/api/editor/item.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {cloneDeep} from 'lodash';
import {EDITOR_TYPE, IEditorAPI} from '../../interfaces';
import {EDITOR_TYPE, IEditorAPI, IEditorProps} from '../../interfaces';
import {planningApi} from '../../superdeskApi';

import * as selectors from '../../selectors';
Expand All @@ -19,6 +19,10 @@ export function getItemInstance(type: EDITOR_TYPE): IEditorAPI['item'] {
return planningApi.editor(type).form.getProps().itemId;
}

function getItemAction(): IEditorProps['itemAction'] {
return planningApi.editor(type).form.getProps().itemAction;
}

function getAssociatedPlannings() {
const state = planningApi.redux.store.getState();
const eventId = planningApi.editor(type).item.getItemId();
Expand All @@ -37,6 +41,7 @@ export function getItemInstance(type: EDITOR_TYPE): IEditorAPI['item'] {
planning,
getItemType,
getItemId,
getItemAction,
getAssociatedPlannings,
};
}
50 changes: 30 additions & 20 deletions client/api/editor/item_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {planningApi, superdeskApi} from '../../superdeskApi';

import {generateTempId} from '../../utils';
import {getBookmarksFromFormGroups, getEditorFormGroupsFromProfile} from '../../utils/contentProfiles';
import {TEMP_ID_PREFIX} from '../../constants';

import {AddPlanningBookmark, AssociatedPlanningsBookmark} from '../../components/Editor/bookmarks';
import {RelatedPlanningItem} from '../../components/fields/editor/EventRelatedPlannings/RelatedPlanningItem';
Expand Down Expand Up @@ -75,38 +74,49 @@ export function getEventsInstance(type: EDITOR_TYPE): IEditorAPI['item']['events
return editor.dom.fields[field];
}

function addPlanningItem() {
function addPlanningItem(
item?: IPlanningItem,
options?: {
scrollIntoViewAndFocus?: boolean;
},
): Promise<Partial<IPlanningItem>> {
const editor = planningApi.editor(type);
const event = editor.form.getDiff<IEventItem>();
const plans = cloneDeep(event.associated_plannings || []);
const id = generateTempId();

const newPlanningItem: Partial<IPlanningItem> = {
_id: id,
...convertEventToPlanningItem(event as IEventItem),
};
const newPlanningItem = (() => {
if (item == null) {
const newPlanningItem: Partial<IPlanningItem> = {
_id: generateTempId(),
...convertEventToPlanningItem(event as IEventItem),
};

return newPlanningItem;
} else {
return item;
}
})();

plans.push(newPlanningItem);

editor.form.changeField('associated_plannings', plans)
return editor.form.changeField('associated_plannings', plans)
.then(() => {
const node = getRelatedPlanningDomRef(id);

if (node.current != null) {
node.current.scrollIntoView();
editor.form.waitForScroll().then(() => {
node.current.focus();
});
if (options?.scrollIntoViewAndFocus ?? true) {
const node = getRelatedPlanningDomRef(newPlanningItem._id);

if (node.current != null) {
node.current.scrollIntoView();
editor.form.waitForScroll().then(() => {
node.current.focus();
});
}
}

return newPlanningItem;
});
}

function removePlanningItem(item: DeepPartial<IPlanningItem>) {
if (!item._id.startsWith(TEMP_ID_PREFIX)) {
// We don't support removing existing Planning items
return;
}

const editor = planningApi.editor(type);
const event = editor.form.getDiff<IEventItem>();
const plans = (event.associated_plannings || []).filter(
Expand Down
3 changes: 0 additions & 3 deletions client/api/editor/item_planning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ export function getPlanningInstance(type: EDITOR_TYPE): IEditorAPI['item']['plan
const profile = planningApi.contentProfiles.get('planning');
const groups = getEditorFormGroupsFromProfile(profile);

if (getRelatedEventLinksForPlanning(item).length === 0) {
delete groups['associated_event'];
}
const bookmarks = getBookmarksFromFormGroups(groups);
let index = bookmarks.length;

Expand Down
4 changes: 3 additions & 1 deletion client/components/Editor/EditorBookmarksBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ class EditorBookmarksBarComponent extends React.PureComponent<IProps> {
style="hollow"
type="primary"
expand={true}
onClick={editor.item.events.addPlanningItem}
onClick={() => {
editor.item.events.addPlanningItem();
}}
/>
</div>
);
Expand Down
10 changes: 8 additions & 2 deletions client/components/Editor/EditorGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class EditorGroup extends React.PureComponent<IProps> implements IEditorR
}
}

scrollIntoView() {
scrollIntoView(options?: {focus?: boolean}) {
if (this.dom.div.current != null) {
this.dom.div.current.scrollIntoView({behavior: 'smooth'});
} else if (this.dom.toggle.current != null) {
Expand All @@ -61,7 +61,13 @@ export class EditorGroup extends React.PureComponent<IProps> implements IEditorR
// Wait for scroll to complete, then attempt to focus the first field
this.editorApi.form
.waitForScroll()
.then(this.focus);
.then(() => {
const shouldFocus = options?.focus ?? true;

if (shouldFocus) {
this.focus();
}
});
}

getBoundingClientRect() {
Expand Down
5 changes: 5 additions & 0 deletions client/components/Events/EventItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ class EventItemComponent extends React.Component<IProps, IState> {
onMouseLeave={this.onItemHoverOff}
onMouseEnter={this.onItemHoverOn}
refNode={refNode}
draggable={!isItemLocked}
onDragstart={(dragEvent) => {
dragEvent.dataTransfer.setData('application/superdesk.planning.event', JSON.stringify(item));
dragEvent.dataTransfer.effectAllowed = 'link';
}}
>
<Border state={borderState} />
<ItemType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface IProps {
showIcon?: boolean;
shadow?: number;
dateOnly?: boolean;
editEventComponent?: React.ReactNode;
eventActions?: React.ReactNode;
onClick?(): void;

// Redux Store
Expand Down Expand Up @@ -87,9 +87,9 @@ class RelatedEventListItemComponent extends React.PureComponent<IProps> {
withExpiredStatus={true}
/>
</List.Column>
{!this.props.editEventComponent ? null : (
{!this.props.eventActions ? null : (
<List.ActionMenu>
{this.props.editEventComponent}
{this.props.eventActions}
</List.ActionMenu>
)}
</List.Item>
Expand Down
Loading
Loading