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-780] RelatedItems: Add related items to preview panels #1938

Merged
merged 3 commits into from
Mar 26, 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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ Below sections include the config options that can be defined in settings.py.
* EVENT_EXPORT_BODY_TEMPLATE:
* default: https://github.com/superdesk/superdesk-planning/blob/develop/server/planning/planning_export_templates.py#L39
* Overrides the default event template used for event exports
* EVENT_RELATED_ITEM_SEARCH_PROVIDER_NAME:
* Default: None
* Required By: Event field ``related_items`` (otherwise this field will be automatically disabled)
* Defines the name of the Search Provider to use for adding related items to an Event

### Planning Config
* LONG_EVENT_DURATION_THRESHOLD:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('<AssignmentPreviewContainer />', () => {
assignment.assigned_to.state = 'assigned';
let wrapper = getWrapper().find('.AssignmentPreview');

expect(wrapper.children().length).toBe(4);
expect(wrapper.children().length).toBe(5);

expect(wrapper.hasClass('AssignmentPreview')).toBe(true);

Expand All @@ -61,7 +61,7 @@ describe('<AssignmentPreviewContainer />', () => {
showFulfilAssignment: true,
hideItemActions: true,
}).find('.AssignmentPreview');
expect(wrapper.children().length).toBe(5);
expect(wrapper.children().length).toBe(6);

expect(wrapper.hasClass('AssignmentPreview')).toBe(true);
expect(wrapper.childAt(0).type()).toEqual(AssignmentPreviewHeader);
Expand Down
114 changes: 73 additions & 41 deletions client/components/Assignments/AssignmentPreviewContainer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,72 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {get} from 'lodash';

import {IDesk, IUser} from 'superdesk-api';
import {
IAssignmentItem,
IAssignmentPriority,
IEventItem, IFile,
IFormProfiles,
IG2ContentType,
ILockedItems,
IPlanningItem,
ISession
} from '../../../interfaces';
import {superdeskApi} from '../../../superdeskApi';
import * as selectors from '../../../selectors';
import * as actions from '../../../actions';
import {assignmentUtils, gettext, eventUtils, planningUtils, getFileDownloadURL} from '../../../utils';
import {assignmentUtils, eventUtils, planningUtils, getFileDownloadURL} from '../../../utils';
import {ASSIGNMENTS, WORKSPACE} from '../../../constants';

import {Button} from 'superdesk-ui-framework/react';
import {AssignmentPreviewHeader} from './AssignmentPreviewHeader';
import {AssignmentPreview} from './AssignmentPreview';
import {Button} from '../../UI';
import {ContentBlock, ContentBlockInner} from '../../UI/SidePanel';

import {RelatedPlannings} from '../../RelatedPlannings';
import {EventMetadata} from '../../Events';
import {PreviewFieldRelatedArticles} from '../../fields/preview/RelatedArticles';

class AssignmentPreviewContainerComponent extends React.Component {
interface IOwnProps {
hideAvatar?: boolean;
hideItemActions?: boolean;
showFulfilAssignment?: boolean;
}

interface IStateProps {
assignment: IAssignmentItem;
session: ISession;
users: Array<IUser>;
desks: Array<IDesk>;
planningItem?: IPlanningItem;
eventItem?: IEventItem;

priorities: Array<IAssignmentPriority>;
privileges: {[key: string]: number};
formProfile: IFormProfiles;
lockedItems: ILockedItems;
currentWorkspace: 'ASSIGNMENTS' | 'AUTHORING' | 'AUTHORING_WIDGET';
contentTypes: Array<IG2ContentType>;
files: Array<IFile>;
}

interface IDispatchProps {
startWorking(assignment: IAssignmentItem): void;
reassign(assignment: IAssignmentItem): void;
completeAssignment(assignment: IAssignmentItem): void;
revertAssignment(assignment: IAssignmentItem): void;
editAssignmentPriority(assignment: IAssignmentItem): void;
onFulFilAssignment(assignment: IAssignmentItem): void;
removeAssignment(assignment: IAssignmentItem): void;
openArchivePreview(assignment: IAssignmentItem): void;
fetchEventFiles(event: IEventItem): void;
fetchPlanningFiles(planning: IPlanningItem): void;
}

type IProps = IOwnProps & IStateProps & IDispatchProps;

class AssignmentPreviewContainerComponent extends React.Component<IProps> {
componentDidMount() {
if (eventUtils.shouldFetchFilesForEvent(this.props.eventItem)) {
this.props.fetchEventFiles(this.props.eventItem);
Expand Down Expand Up @@ -83,19 +133,22 @@ class AssignmentPreviewContainerComponent extends React.Component {
contentTypes,
session,
privileges,
lockedItems,
files,
} = this.props;

if (!assignment) {
return null;
}

const {gettext} = superdeskApi.localization;
const planning = get(assignment, 'planning', {});
const itemActions = this.getItemActions();
const canFulfilAssignment = showFulfilAssignment && assignmentUtils.canFulfilAssignment(
assignment,
session,
privileges
privileges,
lockedItems
);

return (
Expand All @@ -116,9 +169,11 @@ class AssignmentPreviewContainerComponent extends React.Component {
<ContentBlock className="AssignmentPreview__fulfil" padSmall={true} flex={true}>
<ContentBlockInner grow={true}>
<Button
color="primary"
type="primary"
text={gettext('Fulfil Assignment')}
onClick={onFulFilAssignment.bind(null, assignment)}
onClick={() => {
onFulFilAssignment(assignment);
}}
/>
</ContentBlockInner>
</ContentBlock>
Expand All @@ -135,9 +190,18 @@ class AssignmentPreviewContainerComponent extends React.Component {
/>
</ContentBlock>

{eventItem && (
<div className="sd-padding--2 sd-padding-b--0">
<PreviewFieldRelatedArticles
item={eventItem}
languageFilter={assignment.planning.language}
/>
</div>
)}

{eventItem && (
<ContentBlock className="AssignmentPreview__event" padSmall={true}>
<h3 className="side-panel__heading--big">
<h3 className="side-panel__heading side-panel__heading--big">
{gettext('Associated Event')}
</h3>
<EventMetadata
Expand Down Expand Up @@ -170,38 +234,6 @@ class AssignmentPreviewContainerComponent extends React.Component {
}
}

AssignmentPreviewContainerComponent.propTypes = {
hideAvatar: PropTypes.bool,
assignment: PropTypes.object.isRequired,
onFulFilAssignment: PropTypes.func,
startWorking: PropTypes.func.isRequired,
reassign: PropTypes.func,
completeAssignment: PropTypes.func,
editAssignmentPriority: PropTypes.func,
removeAssignment: PropTypes.func,
session: PropTypes.object,
users: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
]),
desks: PropTypes.array,
planningItem: PropTypes.object,
eventItem: PropTypes.object,
priorities: PropTypes.array,
privileges: PropTypes.object,
formProfile: PropTypes.object,
lockedItems: PropTypes.object,
openArchivePreview: PropTypes.func,
revertAssignment: PropTypes.func,
hideItemActions: PropTypes.bool,
showFulfilAssignment: PropTypes.bool,
fetchEventFiles: PropTypes.func,
currentWorkspace: PropTypes.string,
contentTypes: PropTypes.array,
fetchPlanningFiles: PropTypes.func,
files: PropTypes.array,
};

const mapStateToProps = (state) => ({
assignment: selectors.getCurrentAssignment(state),
session: selectors.general.session(state),
Expand Down Expand Up @@ -233,7 +265,7 @@ const mapDispatchToProps = (dispatch) => ({
fetchPlanningFiles: (planning) => dispatch(actions.planning.api.fetchPlanningFiles(planning)),
});

export const AssignmentPreviewContainer = connect(
export const AssignmentPreviewContainer = connect<IStateProps, IDispatchProps>(
mapStateToProps,
mapDispatchToProps
)(AssignmentPreviewContainerComponent);
1 change: 1 addition & 0 deletions client/components/fields/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ const PREVIEW_GROUPS: IPreviewGroups = {
'place',
'location',
'event_contact_info',
'related_items',
],
}, {
name: 'details',
Expand Down
39 changes: 39 additions & 0 deletions client/components/fields/preview/RelatedArticles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react';

import {IArticle} from 'superdesk-api';
import {IListFieldProps, IEventItem} from '../../../interfaces';
import {superdeskApi} from '../../../superdeskApi';
import {ContentBlock} from '../../UI/SidePanel';

interface IProps extends IListFieldProps {
item: IEventItem;
languageFilter?: string;
}

export function PreviewFieldRelatedArticles({item, languageFilter}: IProps) {
const {ArticleItemConcise} = superdeskApi.components;
const {gettext} = superdeskApi.localization;
const relatedItems = (languageFilter?.length ?? 0) === 0 ?
item.related_items ?? [] :
(item.related_items ?? []).filter((relatedItem) => relatedItem.language === languageFilter);

return (
<ContentBlock className="sd-padding--0 sd-padding-b--2">
<h3 className="side-panel__heading side-panel__heading--big">
{gettext('Related Articles')}
</h3>
{relatedItems.length === 0 ? (
<span className="sd-text__info">
{gettext('No related articles')}
</span>
) : (
item.related_items.map((relatedItem) => (
<ArticleItemConcise
key={relatedItem.guid}
article={relatedItem as any as IArticle}
/>
))
)}
</ContentBlock>
);
}
4 changes: 4 additions & 0 deletions client/components/fields/preview/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {PreviewFieldContacts} from './Contacts';
import {PreviewFieldCustomVocabularies} from './CustomVocabularies';
import {PreviewFieldUrgency} from './Urgency';
import {PreviewFieldFlags} from './Flags';
import {PreviewFieldRelatedArticles} from './RelatedArticles';

import * as selectors from '../../../selectors';
import {planningUtils} from '../../../utils';
Expand Down Expand Up @@ -307,6 +308,9 @@ FIELD_TO_FORM_PREVIEW_COMPONENT.custom_vocabularies = PreviewFieldCustomVocabula
FIELD_TO_FORM_PREVIEW_COMPONENT.urgency = PreviewFieldUrgency;
FIELD_TO_FORM_PREVIEW_COMPONENT.flags = PreviewFieldFlags;

FIELD_TO_FORM_PREVIEW_COMPONENT.related_items = PreviewFieldRelatedArticles;
FIELD_TO_PREVIEW_COMPONENT.related_items = PreviewFieldRelatedArticles;

export {
FIELD_TO_PREVIEW_COMPONENT,
FIELD_TO_FORM_PREVIEW_COMPONENT,
Expand Down
15 changes: 15 additions & 0 deletions client/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,20 @@ export interface IEmbeddedPlanningItem {
coverages: Array<IEmbeddedCoverageItem>;
}

export interface IRelatedItem {
guid: string;
type: string;
state: string;
version: string;
headline: string;
slugline: string;
versioncreated: string;
source: string;
search_provider: string;
pubstatus: string;
language: string;
}

export interface IEventItem extends IBaseRestApiResponse {
guid?: string;
unique_id?: string;
Expand Down Expand Up @@ -558,6 +572,7 @@ export interface IEventItem extends IBaseRestApiResponse {
_plannings?: Array<IPlanningItem>;
template?: string;
_sortDate?: IDateTime;
related_items?: Array<IRelatedItem>;

translations?: Array<{
field: string;
Expand Down
11 changes: 11 additions & 0 deletions server/planning/assignments/assignments_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)
from planning.planning_notifications import PlanningNotifications
from planning.archive import create_item_from_template
from planning.signals import assignment_content_create


FIELDS_TO_COPY = ("anpa_category", "subject", "urgency", "place")
Expand Down Expand Up @@ -80,6 +81,7 @@ def get_item_from_assignment(assignment, template=None):
ednote = planning_data.get("ednote")

planning_item = assignment.get("planning_item")
planning = None
# we now merge planning data if they are set
if planning_item is not None:
planning = get_resource_service("planning").find_one(req=None, _id=planning_item)
Expand Down Expand Up @@ -123,6 +125,7 @@ def get_item_from_assignment(assignment, template=None):

# Load default content profile of the desk to the item
content_profile_id = template["data"].get("profile", desk.get("default_content_profile", None))
content_profile = None
if content_profile_id:
content_profiles = get_resource_service("content_types").find({"_id": content_profile_id})
# Pop those items not in the content_profile
Expand All @@ -137,6 +140,14 @@ def get_item_from_assignment(assignment, template=None):
if language:
item["language"] = language

assignment_content_create.send(
None,
assignment=assignment,
planning=planning,
item=item,
content_profile=content_profile,
)

return item, translations


Expand Down
4 changes: 4 additions & 0 deletions server/planning/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,10 @@ def get_config_event_fields_to_sync_with_planning(current_app=None) -> Set[str]:
return set(config_value.split(",") if isinstance(config_value, str) else config_value)


def get_config_event_related_item_search_provider_name(current_app=None) -> Optional[str]:
return (current_app or app).config.get("EVENT_RELATED_ITEM_SEARCH_PROVIDER_NAME")


def remove_lock_information(item):
item.update({LOCK_USER: None, LOCK_SESSION: None, LOCK_TIME: None, LOCK_ACTION: None})

Expand Down
7 changes: 7 additions & 0 deletions server/planning/content_profiles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from .resource import PlanningTypesResource
from .service import PlanningTypesService
from planning.common import get_config_event_related_item_search_provider_name


def init_app(app: Eve):
Expand All @@ -26,3 +27,9 @@ def init_app(app: Eve):

planning_type_service = PlanningTypesService(PlanningTypesResource.endpoint_name, backend=superdesk.get_backend())
PlanningTypesResource(PlanningTypesResource.endpoint_name, app=app, service=planning_type_service)

event_related_item_search_provider_name = get_config_event_related_item_search_provider_name(app)
if event_related_item_search_provider_name:
app.client_config.setdefault("planning", {})[
"event_related_item_search_provider_name"
] = event_related_item_search_provider_name
9 changes: 8 additions & 1 deletion server/planning/content_profiles/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import superdesk
from superdesk.utils import ListCursor

from planning.common import planning_link_updates_to_coverage
from planning.common import planning_link_updates_to_coverage, get_config_event_related_item_search_provider_name
from .profiles import DEFAULT_PROFILES


Expand Down Expand Up @@ -97,3 +97,10 @@ def merge_planning_type(self, planning_type, default_planning_type):
planning_type["schema"] = updated_planning_type["schema"]
planning_type["editor"] = updated_planning_type["editor"]
planning_type["groups"] = updated_planning_type["groups"]

# Disable Event ``related_items`` field
# if ``EVENT_RELATED_ITEM_SEARCH_PROVIDER_NAME`` config is not set
if planning_type["name"] == "event":
if not get_config_event_related_item_search_provider_name():
planning_type["editor"].pop("related_items", None)
planning_type["schema"].pop("related_items", None)
1 change: 1 addition & 0 deletions server/planning/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@

planning_created = signals.signal("planning:created")
planning_ingested = signals.signal("planning:ingested")
assignment_content_create = signals.signal("planning:assignment_content_create")
Loading
Loading