diff --git a/client/components/LockContainer/index.tsx b/client/components/LockContainer/index.tsx index 21c96c3a0..98eae98ac 100644 --- a/client/components/LockContainer/index.tsx +++ b/client/components/LockContainer/index.tsx @@ -1,15 +1,26 @@ import React from 'react'; -import PropTypes from 'prop-types'; import {get} from 'lodash'; import classNames from 'classnames'; - -import {UserAvatarWithMargin} from '../../components/UserAvatar'; +import {UserAvatar} from '../../components/UserAvatar'; import {LockContainerPopup} from './LockContainerPopup'; import './style.scss'; -export class LockContainer extends React.Component { - constructor(props) { +interface LockContainerProps { + lockedUser: any; + users: any[] | any; + displayText?: string; + showUnlock?: boolean; + onUnlock?: () => void; + noMargin?: boolean; +} + +interface LockContainerState { + openUnlockPopup: boolean; +} + +export class LockContainer extends React.Component { + constructor(props: LockContainerProps) { super(props); this.state = {openUnlockPopup: false}; this.toggleOpenUnlockPopup = this.toggleOpenUnlockPopup.bind(this); @@ -24,10 +35,8 @@ export class LockContainer extends React.Component { lockedUser, users, displayText, - showUnlock, - withLoggedInfo, + showUnlock = true, onUnlock, - small, noMargin, } = this.props; @@ -49,7 +58,7 @@ export class LockContainer extends React.Component { )} > - + {this.state.openUnlockPopup && ( @@ -66,23 +75,3 @@ export class LockContainer extends React.Component { ); } } - -LockContainer.propTypes = { - lockedUser: PropTypes.object, - users: PropTypes.oneOfType([ - PropTypes.array, - PropTypes.object, - ]), - displayText: PropTypes.string, - showUnlock: PropTypes.bool, - withLoggedInfo: PropTypes.bool, - onUnlock: PropTypes.func, - small: PropTypes.bool, - noMargin: PropTypes.bool, -}; - -LockContainer.defaultProps = { - showUnlock: true, - withLoggedInfo: true, - small: true, -}; diff --git a/client/components/Main/ItemEditor/Editor.tsx b/client/components/Main/ItemEditor/Editor.tsx index 22ac128d9..d9316e97f 100644 --- a/client/components/Main/ItemEditor/Editor.tsx +++ b/client/components/Main/ItemEditor/Editor.tsx @@ -19,6 +19,7 @@ import {ItemManager} from './ItemManager'; import {AutoSave} from './AutoSave'; import {EditorHeader} from './EditorHeader'; import {pickRelatedEventsForPlanning} from './../../../utils/planning'; +import {embeddedPlanningHasUnsavedChanges} from '../../../components/editor-standalone/save-handling'; export class EditorComponent extends React.Component { autoSave: AutoSave; @@ -58,7 +59,8 @@ export class EditorComponent extends React.Component tabProps: { forEditor: !this.props.inModalView, forEditorModal: this.props.inModalView, - }}, + } + }, ]; if (this.props.addNewsItemToPlanning) { @@ -195,68 +197,54 @@ export class EditorComponent extends React.Component return; } - this.autoSave.flushAutosave() - .then(() => { - const { - openCancelModal, - itemId, - itemType, - addNewsItemToPlanning, - } = this.props; - const {dirty, errorMessages, initialValues} = this.state; - - this.setState({submitting: true}); + this.autoSave.flushAutosave().then(() => { + const {openCancelModal, itemId, itemType, addNewsItemToPlanning} = this.props; + const {dirty, errorMessages, initialValues} = this.state; + const updateStates = !addNewsItemToPlanning; - const updateStates = !addNewsItemToPlanning; + this.setState({submitting: true}); - if (!dirty) { - this.onCancel(); - } else { - const hasErrors = !isEqual(errorMessages, []); - const isKilled = isItemKilled(initialValues); + if (!(dirty || embeddedPlanningHasUnsavedChanges())) { + this.onCancel(); + return; + } - const onCancel = () => { - if (updateStates) { - this.setState({submitting: false}); - } - }; - - const onIgnore = () => { - this.itemManager.unlockAndCancel(); - }; - - const onSave = (isKilled || hasErrors) ? null : - (withConfirmation, updateMethod, planningUpdateMethods) => ( - this.itemManager.save( - withConfirmation, - {name: updateMethod, value: updateMethod}, - true, - updateStates, - planningUpdateMethods - ) - ); - - const onSaveAndPost = (!isKilled || hasErrors) ? null : - (withConfirmation, updateMethod, planningUpdateMethods) => ( - this.itemManager.saveAndPost( - withConfirmation, - updateMethod, - true, - updateStates, - planningUpdateMethods - ) - ); - - openCancelModal({ - itemId: itemId, - itemType: itemType, - onCancel: onCancel, - onIgnore: onIgnore, - onSave: onSave, - onSaveAndPost: onSaveAndPost, - }); - } + const hasErrors = !isEqual(errorMessages, []); + const isKilled = isItemKilled(initialValues); + const onSave = (isKilled || hasErrors) ? null : (withConfirmation, updateMethod) => ( + this.itemManager.save( + withConfirmation, + {name: updateMethod, value: updateMethod}, + true, + updateStates, + ) + ); + const onSaveAndPost = (!isKilled || hasErrors) ? null : (withConfirmation, updateMethod) => ( + this.itemManager.saveAndPost( + withConfirmation, + updateMethod, + true, + updateStates, + ) + ); + + openCancelModal({ + itemId: itemId, + itemType: itemType, + onCancel: () => { + if (updateStates) { + this.setState({submitting: false}); + } + }, + onIgnore: () => { + this.itemManager.unlockAndCancel( + embeddedPlanningHasUnsavedChanges() ? 'HANDLE_UNSAVED_CHANGES' : 'DISCARD', + ); + }, + onSave: onSave, + onSaveAndPost: onSaveAndPost, }); + }); } onCancel(updateStates = true) { @@ -276,7 +264,9 @@ export class EditorComponent extends React.Component this.props.onCancel(); } - return this.itemManager.unlockAndCancel(); + return this.itemManager.unlockAndCancel( + embeddedPlanningHasUnsavedChanges() ? 'HANDLE_UNSAVED_CHANGES' : 'DISCARD', + ); } setActiveTab(tab) { diff --git a/client/components/Main/ItemEditor/EditorHeader.tsx b/client/components/Main/ItemEditor/EditorHeader.tsx index 8a4170759..5b43fe7f9 100644 --- a/client/components/Main/ItemEditor/EditorHeader.tsx +++ b/client/components/Main/ItemEditor/EditorHeader.tsx @@ -22,14 +22,15 @@ import {StretchBar} from '../../UI/SubNav'; import {LockContainer, ItemIcon} from '../../index'; import {EditorItemActions} from './index'; import {ButtonGroup} from 'superdesk-ui-framework'; -import {IEditorProps, IEditorState, ILockedItems, IPrivileges, ISession} from 'interfaces'; +import {IEditorProps, IEditorState, IEventOrPlanningItem, ILockedItems, IPrivileges, ISession} from 'interfaces'; import {IUser} from 'superdesk-api'; import {ItemManager} from './ItemManager'; import {AutoSave} from './AutoSave'; +import {IUIButtonProps} from 'components/UI/Button'; interface IProps { - diff: IEditorState['diff']; - initialValues: IEditorState['initialValues']; + diff: IEventOrPlanningItem; + initialValues: IEventOrPlanningItem; cancel(): void; minimize(): void; submitting: boolean; @@ -124,17 +125,19 @@ export class EditorHeader extends React.Component { states.showEdit = states.existingItem && !states.isLockedInContext && - eventUtils.canEditEvent(initialValues, session, privileges, lockedItems); + eventUtils.canEditEvent(initialValues as IEventItem, session, privileges, lockedItems); if (states.readOnly) { return; } if (states.isLockedInContext && get(states.itemLock, 'action') === 'edit') { - states.canPost = eventUtils.canPostEvent(initialValues, session, privileges, lockedItems); - states.canUnpost = eventUtils.canUnpostEvent(initialValues, session, privileges, lockedItems); - states.canUpdate = eventUtils.canUpdateEvent(initialValues, session, privileges, lockedItems); - states.canEdit = eventUtils.canEditEvent(initialValues, session, privileges, lockedItems); + const initialVal = initialValues as IEventItem; + + states.canPost = eventUtils.canPostEvent(initialVal, session, privileges, lockedItems); + states.canUnpost = eventUtils.canUnpostEvent(initialVal, session, privileges, lockedItems); + states.canUpdate = eventUtils.canUpdateEvent(initialVal, session, privileges, lockedItems); + states.canEdit = eventUtils.canEditEvent(initialVal, session, privileges, lockedItems); } } @@ -153,9 +156,12 @@ export class EditorHeader extends React.Component { return; } + const initialVals = initialValues as IPlanningItem; + const diffCasted = diff as IPlanningItem; + states.showEdit = states.existingItem && !states.isLockedInContext && - planningUtils.canEditPlanning(initialValues, null, session, privileges, lockedItems) && + planningUtils.canEditPlanning(initialVals, null, session, privileges, lockedItems) && !addNewsItemToPlanning; if (states.readOnly) { @@ -165,27 +171,51 @@ export class EditorHeader extends React.Component { if (states.isLockedInContext) { switch (get(states, 'itemLock.action')) { case 'edit': - states.canPost = planningUtils.canPostPlanning(diff, - associatedEvents, session, privileges, lockedItems); - states.canUnpost = planningUtils.canUnpostPlanning(initialValues, - session, privileges, lockedItems); - states.canUpdate = planningUtils.canUpdatePlanning(initialValues, - associatedEvents, session, privileges, lockedItems); - states.canEdit = planningUtils.canEditPlanning(initialValues, - associatedEvents, session, privileges, lockedItems); + states.canPost = planningUtils.canPostPlanning( + diffCasted, + associatedEvents, + session, + privileges, + lockedItems, + ); + states.canUnpost = planningUtils.canUnpostPlanning( + initialVals, + session, + privileges, + lockedItems, + ); + states.canUpdate = planningUtils.canUpdatePlanning( + initialVals, + associatedEvents, + session, + privileges, + lockedItems, + ); + states.canEdit = planningUtils.canEditPlanning( + initialVals, + associatedEvents, + session, + privileges, + lockedItems, + ); break; case 'add_to_planning': states.canPost = planningUtils.canPostPlanning( - diff, + diffCasted, associatedEvents, session, privileges, lockedItems ); - states.canUpdate = planningUtils.canUpdatePlanning(initialValues, - associatedEvents, session, privileges, lockedItems); + states.canUpdate = planningUtils.canUpdatePlanning( + initialVals, + associatedEvents, + session, + privileges, + lockedItems, + ); states.canEdit = planningUtils.canEditPlanning( - initialValues, + initialVals, associatedEvents, session, privileges, @@ -234,7 +264,7 @@ export class EditorHeader extends React.Component { canEditExpired: false, }; - states.isExpired = isItemExpired(initialValues); + states.isExpired = isItemExpired(initialValues) ?? false; states.canEditExpired = privileges[PRIVILEGES.EDIT_EXPIRED]; states.itemLock = lockUtils.getLock(initialValues, lockedItems); states.isLockedInContext = addNewsItemToPlanning ? @@ -280,15 +310,12 @@ export class EditorHeader extends React.Component { doubleSize={true} color={states.isEvent ? ICON_COLORS.WHITE : ICON_COLORS.LIGHT_BLUE} /> - - {!showLockContainer ? null : ( + {showLockContainer && ( )} @@ -322,7 +349,13 @@ export class EditorHeader extends React.Component { } const notDirtyOrSubmitting = !dirty || submitting; - const buttons = [{ + + type IButtonProps = Array<{ + state: string, + props: IUIButtonProps + }>; + + const buttons: IButtonProps = [{ state: 'showCancel', props: { color: states.isEvent ? 'ui-dark' : null, @@ -451,7 +484,6 @@ export class EditorHeader extends React.Component { )} @@ -461,7 +493,6 @@ export class EditorHeader extends React.Component { onClick={closeEditorAndOpenModal} aria-label={gettext('Edit in popup')} icon="icon-external" - title={gettext('Edit in popup')} /> )} diff --git a/client/components/Main/ItemEditor/ItemManager.ts b/client/components/Main/ItemEditor/ItemManager.ts index 2b2fd570d..80718cb2f 100644 --- a/client/components/Main/ItemEditor/ItemManager.ts +++ b/client/components/Main/ItemEditor/ItemManager.ts @@ -28,7 +28,10 @@ import {EditorComponent} from './Editor'; import {AutoSave} from './AutoSave'; import {EditorGroup} from '../../Editor/EditorGroup'; import * as selectors from '../../../selectors'; - +import { + handleEmbeddedPlannings, + IEmbeddedPlanningsActionType, +} from '../../../components/editor-standalone/save-handling'; export class ItemManager { editor: EditorComponent; @@ -471,40 +474,42 @@ export class ItemManager { } post() { - const newState = {}; + return handleEmbeddedPlannings(this.props.editorType, 'SAVE').then(() => { + const newState = {}; - this.validate(this.props, newState, this.state); - if (!isEqual(this.state.errorMessages, [])) { - return this.setState({ - submitting: false, - submitFailed: true, - }) - .then(() => { + this.validate(this.props, newState, this.state); + if (!isEqual(this.state.errorMessages, [])) { + return this.setState({ + submitting: false, + submitFailed: true, + }).then(() => { this.props.notifyValidationErrors(this.state.errorMessages); return Promise.reject(); }); - } - return this.setState({ - submitting: true, - submitFailed: false, - }) - .then(() => this.autoSave.flushAutosave()) - .then(() => this.dispatch( - actions.main.post(this.state.initialValues) - )) - .then( - this.afterPostOrUnpost, - (error) => { - if (get(error, 'status') === 412) { - // If etag error, then notify user and change editor to read-only - this.dispatch( - actions.main.notifyPreconditionFailed(this.props.inModalView) - ); - } + } - return this.setState({submitting: false}); - } - ); + return this.setState({ + submitting: true, + submitFailed: false, + }) + .then(() => this.autoSave.flushAutosave()) + .then(() => this.dispatch( + actions.main.post(this.state.initialValues) + )) + .then( + this.afterPostOrUnpost, + (error) => { + if (get(error, 'status') === 412) { + // If etag error, then notify user and change editor to read-only + this.dispatch( + actions.main.notifyPreconditionFailed(this.props.inModalView) + ); + } + + return this.setState({submitting: false}); + } + ); + }); } unpost() { @@ -656,12 +661,9 @@ export class ItemManager { }); } - const promise = !updateStates ? - Promise.resolve() : - this.setState({ - submitting: true, - submitFailed: false, - }); + const promise = handleEmbeddedPlannings(this.props.editorType, 'SAVE').then(() => + !updateStates ? Promise.resolve({}) : this.setState({submitting: true, submitFailed: false}), + ); if (this.props.addNewsItemToPlanning) { return promise.then(() => this._saveFromAuthoring({post, unpost})); @@ -828,26 +830,30 @@ export class ItemManager { )); } - unlockAndCancel() { - const {session, currentWorkspace} = this.props; - const {initialValues, diff} = this.state; - let promises = []; + unlockAndCancel(embeddedEditorAction?: IEmbeddedPlanningsActionType) { + return handleEmbeddedPlannings( + this.props.editorType, + embeddedEditorAction, + ).then(() => { + const {session, currentWorkspace} = this.props; + const {initialValues, diff} = this.state; + let promises = []; - if (shouldUnLockItem(initialValues, session, currentWorkspace, this.props.lockedItems)) { - promises.push(planningApi.locks.unlockItem(this.props.item)); - // promises.push(this.unlock()); - } + if (shouldUnLockItem(initialValues, session, currentWorkspace, this.props.lockedItems)) { + promises.push(planningApi.locks.unlockItem(this.props.item)); + } - // If event was created by a planning item, unlock the planning item - if (diff?.type === 'event' && diff._planning_item) { - planningApi.locks.unlockItemById(diff._planning_item, 'planning'); - } + // If event was created by a planning item, unlock the planning item + if (diff?.type === 'event' && diff._planning_item) { + planningApi.locks.unlockItemById(diff._planning_item, 'planning'); + } - promises.push(this.autoSave.remove()); + promises.push(this.autoSave.remove()); - this.editor.closeEditor(); + this.editor.closeEditor(); - return Promise.all(promises); + return Promise.all(promises); + }); } changeAction(action, newItem = null) { diff --git a/client/components/PlanningTemplatesModal/TemplatesListView.tsx b/client/components/PlanningTemplatesModal/TemplatesListView.tsx index f1d2e872c..3155614d4 100644 --- a/client/components/PlanningTemplatesModal/TemplatesListView.tsx +++ b/client/components/PlanningTemplatesModal/TemplatesListView.tsx @@ -1,7 +1,7 @@ import {ICalendar, IEventTemplate} from 'interfaces'; import React from 'react'; -import {gettext} from 'superdesk-core/scripts/core/utils'; import {Heading, BoxedList, BoxedListItem} from 'superdesk-ui-framework/react'; +import {superdeskApi} from '../../superdeskApi'; type ITemplatesListViewProps = { closeModal: () => void; @@ -25,6 +25,7 @@ export const TemplatesListView: React.FC = ({ const calendarsFiltered = activeCalendarFilter ? [calendars.find(({qcode}) => activeCalendarFilter === qcode)] : calendars; + const {gettext} = superdeskApi.localization; const filteredTemplates = calendarsFiltered .map((_calendar) => ({ diff --git a/client/components/UI/Button.tsx b/client/components/UI/Button.tsx index e8d84d6c4..d195874b5 100644 --- a/client/components/UI/Button.tsx +++ b/client/components/UI/Button.tsx @@ -1,11 +1,9 @@ import React from 'react'; import classNames from 'classnames'; - import {KEYCODES} from './constants'; import {onEventCapture} from './utils'; - -interface IButtonProps { +export interface IUIButtonProps { id?: string; className?: string; onClick: (...args: any) => any; diff --git a/client/components/UI/Nav/Button.tsx b/client/components/UI/Nav/Button.tsx index 317dab627..1a885fabe 100644 --- a/client/components/UI/Nav/Button.tsx +++ b/client/components/UI/Nav/Button.tsx @@ -1,29 +1,39 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; - import './style.scss'; -/** - * @ngdoc react - * @name Button - * @description Button Component for a NavBar - */ -export const Button = ({ +interface IProps { + className?: string; + onClick?: () => void; + icon?: string; + tooltip?: string; + tooltipDirection?: 'top' | 'down' | 'left' | 'right'; + children?: React.ReactNode; + dropdown?: boolean; + textWithIcon?: boolean; + left?: boolean; + darker?: boolean; + active?: boolean; + navbtn?: boolean; + noBorderNoPadding?: boolean; + disabled?: boolean; +} + +export const Button: React.FC = ({ className, onClick, icon, tooltip, - tooltipDirection, + tooltipDirection = 'top', children, - dropdown, - textWithIcon, - left, - darker, - active, - navbtn, + dropdown = false, + textWithIcon = false, + left = false, + darker = false, + active = false, + navbtn = true, noBorderNoPadding, - disabled, + disabled = false, ...props }) => (