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

As a planning editor I want to link items (events and planning items) via drag and drop #2101

Open
wants to merge 2 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
21 changes: 1 addition & 20 deletions client/actions/events/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,12 +394,8 @@ const post = (original, updates) => (
etag: original._etag,
pubstatus: get(updates, 'pubstatus', POST_STATE.USABLE),
update_method: get(updates, 'update_method.value', EVENTS.UPDATE_METHODS[0].value),
failed_planning_ids: get(updates, 'failed_planning_ids', []),
}).then(
(data) => Promise.all([
dispatch(self.fetchById(original._id, {force: true})),
{failedPlanningIds: data?.failed_planning_ids}
]),
() => dispatch(self.fetchById(original._id, {force: true})),
(error) => Promise.reject(error)
)
)
Expand Down Expand Up @@ -679,21 +675,6 @@ const createEventTemplate = (item: IEventItem) => (dispatch, getState, {api, mod
template_name: templateName,
based_on_event: item._id,
data: {
embedded_planning: item.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(() => {
Expand Down
22 changes: 22 additions & 0 deletions client/api/editor/item_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,27 @@ export function getEventsInstance(type: EDITOR_TYPE): IEditorAPI['item']['events
});
}

function dropPlanningItem(newPlanningItem: IPlanningItem) {
const editor = planningApi.editor(type);
const event = editor.form.getDiff<IEventItem>();
const plans = cloneDeep(event.associated_plannings || []);
const id = generateTempId();

plans.push(newPlanningItem);

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

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

function removePlanningItem(item: DeepPartial<IPlanningItem>) {
if (!item._id.startsWith(TEMP_ID_PREFIX)) {
// We don't support removing existing Planning items
Expand Down Expand Up @@ -195,6 +216,7 @@ export function getEventsInstance(type: EDITOR_TYPE): IEditorAPI['item']['events
getGroupsForItem,
getRelatedPlanningDomRef,
addPlanningItem,
dropPlanningItem,
removePlanningItem,
updatePlanningItem,
onEventDatesChanged,
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
47 changes: 7 additions & 40 deletions client/api/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
ISearchParams,
ISearchSpikeState,
IPlanningConfig,
IEventUpdateMethod,
} from '../interfaces';
import {appConfig as config} from 'appConfig';
import {IRestApiResponse} from 'superdesk-api';
Expand Down Expand Up @@ -123,31 +122,13 @@ function getEventSearchProfile() {
}

function create(updates: Partial<IEventItem>): Promise<Array<IEventItem>> {
const {default_create_planning_series_with_event_series} = appConfig.planning;
const planningDefaultCreateMethod: IEventUpdateMethod = default_create_planning_series_with_event_series === true ?
'all' :
'single';
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>>('events', {
return superdeskApi.dataApi.create<IEventItem | IRestApiResponse<IEventItem>>(url, {
...updates,
associated_plannings: undefined,
embedded_planning: updates.associated_plannings.map((planning) => ({
update_method: planning.update_method ?? planningDefaultCreateMethod,
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,
headline: coverage.planning.headline,
})),
})),
update_method: updates.update_method?.value ?? updates.update_method
})
.then((response) => {
Expand Down Expand Up @@ -177,26 +158,12 @@ function update(original: IEventItem, updates: Partial<IEventItem>): Promise<Arr
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,
update_method: planning.update_method,
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,
})),
})),
update_method: updates.update_method?.value ?? updates.update_method ?? original.update_method
})
.then((response) => {
// we don't store associated_plannings field on the server but we need to preserve it on the client
// TODO: update the planningItem

const events = modifySaveResponseForClient(response);

return planningApi.planning.searchGetAll({
Expand Down
1 change: 1 addition & 0 deletions client/components/Events/EventEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ class EventEditorComponent extends React.PureComponent<IProps> {
editor.item.events.getRelatedPlanningDomRef(value._id)
),
addPlanningItem: editor.item.events.addPlanningItem,
dropPlanningItem: editor.item.events.dropPlanningItem,
removePlanningItem: editor.item.events.removePlanningItem,
updatePlanningItem: editor.item.events.updatePlanningItem,
addCoverageToWorkflow: editor.item.events.addCoverageToWorkflow,
Expand Down
1 change: 1 addition & 0 deletions client/components/Main/ItemEditor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class EditorComponent extends React.Component<IEditorProps, IEditorState>
}
}

// TODO: beginning of function which remove associated_planning after saving
onChangeHandler(field, value, updateDirtyFlag = true, saveAutosave = true) {
// Use a callback to `this.setState` so we get the state value at the exact point when updating.
// This allows consecutive updates to the state, while allowing React to batch these updates.
Expand Down
27 changes: 23 additions & 4 deletions client/components/Main/ListGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import {ListGroupItem} from './';
import {Group, Header} from '../UI/List';
import {
ICommonAdvancedSearchParams,
IEventOrPlanningItem, ISearchFilter,
LIST_VIEW_TYPE, SORT_FIELD
IEventOrPlanningItem,
ILockedItems,
ISearchFilter,
LIST_VIEW_TYPE,
SORT_FIELD
} from '../../interfaces';
import {timeUtils} from '../../utils';
import {lockUtils, timeUtils} from '../../utils';

const TIME_COLUMN_MIN_WIDTH = {
WITH_YEAR: '11rem',
Expand Down Expand Up @@ -55,7 +58,7 @@ interface IProps {
onDoubleClick?(): void;
editItem?: {};
previewItem?: string;
lockedItems: {};
lockedItems: ILockedItems;
agendas: Array<any>;
session?: {};
privileges?: {};
Expand Down Expand Up @@ -224,13 +227,29 @@ export class ListGroup extends React.Component<IProps> {

const id = listBoxGroupProps.getChildId(index);
const selectedId = listBoxGroupProps.containerProps['aria-activedescendant'];
const isItemLocked = lockUtils.isItemLocked(item, this.props.lockedItems);

return (
<div
id={id}
role="option"
aria-selected={id === selectedId ? true : undefined}
key={item._id}
draggable={!isItemLocked}
onDragStart={(event) => {
const dataTransfer = event.dataTransfer;
const mimeTypes = ['application/superdesk.planningItem'];

if (item.mimetype && item.mimetype.includes('application')) {
mimeTypes.push(item.mimetype);
}

mimeTypes.forEach((mimetype) => {
dataTransfer.setData(mimetype, JSON.stringify(item));
});

dataTransfer.effectAllowed = 'link';
}}
>
<ListGroupItem {...itemProps} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion client/components/Planning/PlanningEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ class PlanningEditorComponent extends React.Component<IProps, IState> {
files: this.props.files,
},
associated_event: {
events: (this.props.item.related_events ?? [])
events: (this.props.diff.related_events ?? [])
.map((relatedEvent) => this.props.events[relatedEvent._id]),
},
coverages: {
Expand Down
82 changes: 64 additions & 18 deletions client/components/fields/editor/AssociatedEvent.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,91 @@
import * as React from 'react';
import {connect} from 'react-redux';

import {IEditorFieldProps, IEventItem, IFile, ILockedItems} from '../../../interfaces';

import {getFileDownloadURL} from '../../../utils';
import {EDITOR_TYPE, IEditorFieldProps, IEventItem, IFile, ILockedItems, IPlanningItem} from '../../../interfaces';
import {getFileDownloadURL, gettext} from '../../../utils';
import * as selectors from '../../../selectors';

import {EventMetadata} from '../../Events';
import {DropZone3} from 'superdesk-core/scripts/core/ui/components/drop-zone-3';
import {planningApi} from '../../../../client/superdeskApi';
import {cloneDeep} from 'lodash';

interface IProps extends IEditorFieldProps {
events?: Array<IEventItem>;
lockedItems: ILockedItems;
files: Array<IFile>;
tabEnabled?: boolean; // defaults to true
dispatch: (action) => Promise<IEventItem>;
}

const mapStateToProps = (state) => ({
lockedItems: selectors.locks.getLockedItems(state),
files: selectors.general.files(state),
});

const mapDispatchToProps = (dispatch) => ({
dispatch,
});

function dropEventItem(newPlanningItem: IEventItem) {
const editor = planningApi.editor(EDITOR_TYPE.INLINE);
const event = editor.form.getDiff<IPlanningItem>();

const plans = cloneDeep(event.related_events || []);

plans.push({_id: newPlanningItem._id});

editor.form.changeField('related_events', plans, true, true);
}

function canDrop(eventItem: IEventItem) {
if (eventItem.type !== 'event' || this.props.item.events.includes(eventItem)) {
return false;
}
return true;
}

class EditorFieldAssociatedEventComponent extends React.PureComponent<IProps> {
render() {
return (this.props.events?.length ?? 0) < 1 ? null : this.props.events.map((event) => (
<EventMetadata
key={event._id}
ref={this.props.refNode}
testId={`${this.props.testId}--${event._id}`}
event={event}
navigation={{}}
createUploadLink={getFileDownloadURL}
files={this.props.files}
tabEnabled={this.props.tabEnabled ?? true}
/>
));
return (
<div>
<label className="InputArray__label side-panel__heading side-panel__heading--big">
{gettext('Related Events')}
</label>
{(this.props.events?.length ?? 0) < 1 ? null : this.props.events.map((event) => (
<EventMetadata
key={event._id}
ref={this.props.refNode}
testId={`${this.props.testId}--${event._id}`}
event={event}
navigation={{}}
createUploadLink={getFileDownloadURL}
files={this.props.files}
tabEnabled={this.props.tabEnabled ?? true}
/>
))}

<DropZone3
className="basic-drag-block"
canDrop={(event) => canDrop(
JSON.parse(event.dataTransfer.getData('application/superdesk.planningItem'))
)}
onDrop={(event) => {
event.preventDefault();
const eventItem = JSON.parse(
event.dataTransfer.getData('application/superdesk.planningItem')
);

dropEventItem(eventItem);
}}
multiple={true}
/>
</div>
);
}
}

export const EditorFieldAssociatedEvents = connect(
mapStateToProps,
null,
mapDispatchToProps,
null,
{forwardRef: true}
)(EditorFieldAssociatedEventComponent);
Loading
Loading