Skip to content

Commit

Permalink
[4.1.x][6527] Cut/Paste via the Sidebar should warn users or allow th…
Browse files Browse the repository at this point in the history
…em to act on broken dependencies caused by the move operation (#3761)

* Cut/Paste via the Sidebar should warn users or allow them to act on broken dependencies caused by the move operation #6527

* Merge BrokenReferencesDialog UI and container, rename utils.ts to types.ts #6527

* Add types to brokenReferences dialog actions, handle error in BrokenReferencesDialog #6527

* Update dialog texts #6527
  • Loading branch information
jvega190 committed May 7, 2024
1 parent e2ccf85 commit 39b8f72
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2007-2024 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import React from 'react';
import { BrokenReferencesDialogProps } from './types';
import EnhancedDialog from '../EnhancedDialog';
import { FormattedMessage } from 'react-intl';
import BrokenReferencesDialogContainer from './BrokenReferencesDialogContainer';

export function BrokenReferencesDialog(props: BrokenReferencesDialogProps) {
const { path, references, error, onContinue, ...rest } = props;

return (
<EnhancedDialog
title={<FormattedMessage defaultMessage="Broken References Warning" />}
subtitle={
<FormattedMessage defaultMessage="Proceeding would cause items listed below to have broken references" />
}
{...rest}
maxWidth="sm"
>
<BrokenReferencesDialogContainer path={path} references={references} onContinue={onContinue} error={error} />
</EnhancedDialog>
);
}

export default BrokenReferencesDialog;
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright (C) 2007-2024 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import React from 'react';
import { BrokenReferencesDialogContainerProps } from './types';
import { FormattedMessage } from 'react-intl';
import { EmptyState } from '../EmptyState';
import { useDispatch } from 'react-redux';
import { fetchBrokenReferences, showEditDialog } from '../../state/actions/dialogs';
import useActiveSiteId from '../../hooks/useActiveSiteId';
import useEnv from '../../hooks/useEnv';
import { DialogBody } from '../DialogBody';
import Grid from '@mui/material/Grid';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import Button from '@mui/material/Button';
import DialogFooter from '../DialogFooter';
import SecondaryButton from '../SecondaryButton';
import PrimaryButton from '../PrimaryButton';
import ApiResponseErrorState from '../ApiResponseErrorState';

export function BrokenReferencesDialogContainer(props: BrokenReferencesDialogContainerProps) {
const { references, error, onClose, onContinue } = props;
const dispatch = useDispatch();
const site = useActiveSiteId();
const { authoringBase } = useEnv();

const onContinueClick = (e) => {
onClose(e, null);
onContinue();
};

const onEditReferenceClick = (path: string) => {
dispatch(showEditDialog({ path, authoringBase, site, onSaveSuccess: fetchBrokenReferences() }));
};

return error ? (
<ApiResponseErrorState error={error} />
) : references.length > 0 ? (
<>
<DialogBody>
<Grid container spacing={3}>
<Grid item xs={12}>
<List
sx={{
border: (theme) => `1px solid ${theme.palette.divider}`,
background: (theme) => theme.palette.background.paper
}}
>
{references.map((reference, index) => (
<ListItem key={reference.path} divider={references.length - 1 !== index}>
<ListItemText
primary={reference.label}
secondary={reference.path}
primaryTypographyProps={{
title: reference.path,
sx: {
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis'
}
}}
/>
<ListItemSecondaryAction>
<Button
color="primary"
onClick={() => {
onEditReferenceClick?.(reference.path);
}}
size="small"
sx={{
marginLeft: 'auto',
fontWeight: 'bold',
verticalAlign: 'baseline'
}}
>
<FormattedMessage defaultMessage="Edit" />
</Button>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</Grid>
</Grid>
</DialogBody>
<DialogFooter>
{onClose && (
<SecondaryButton onClick={(e) => onClose(e, null)}>
<FormattedMessage defaultMessage="Cancel" />
</SecondaryButton>
)}
{onContinue && (
<PrimaryButton onClick={onContinueClick} autoFocus>
<FormattedMessage defaultMessage="Continue" />
</PrimaryButton>
)}
</DialogFooter>
</>
) : (
<EmptyState title={<FormattedMessage defaultMessage="No broken references have been detected" />} />
);
}

export default BrokenReferencesDialogContainer;
19 changes: 19 additions & 0 deletions ui/app/src/components/BrokenReferencesDialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (C) 2007-2024 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

export { default } from './BrokenReferencesDialog';

export * from './BrokenReferencesDialog';
41 changes: 41 additions & 0 deletions ui/app/src/components/BrokenReferencesDialog/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2007-2024 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { EnhancedDialogProps } from '../EnhancedDialog';
import { EnhancedDialogState } from '../../hooks/useEnhancedDialogState';
import StandardAction from '../../models/StandardAction';
import React from 'react';
import { ApiResponse, SandboxItem } from '../../models';

export interface BrokenReferencesDialogBaseProps {
path?: string;
references?: SandboxItem[];
error?: ApiResponse;
}

export interface BrokenReferencesDialogProps extends BrokenReferencesDialogBaseProps, EnhancedDialogProps {
onContinue?(response?: any): any;
}

export interface BrokenReferencesDialogStateProps extends BrokenReferencesDialogBaseProps, EnhancedDialogState {
onClose?: StandardAction;
onClosed?: StandardAction;
onContinue?: StandardAction;
}

export interface BrokenReferencesDialogContainerProps
extends BrokenReferencesDialogBaseProps,
Pick<BrokenReferencesDialogProps, 'onContinue' | 'onClose'> {}
10 changes: 10 additions & 0 deletions ui/app/src/components/GlobalDialogManager/GlobalDialogManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const PathSelectionDialog = lazy(() => import('../PathSelectionDialog'));
const UnlockPublisherDialog = lazy(() => import('../UnlockPublisherDialog'));
const WidgetDialog = lazy(() => import('../WidgetDialog'));
const CodeEditorDialog = lazy(() => import('../CodeEditorDialog'));
const BrokenReferencesDialog = lazy(() => import('../BrokenReferencesDialog'));
// endregion

// @formatter:off
Expand Down Expand Up @@ -395,6 +396,15 @@ function GlobalDialogManager() {
/>
{/* endregion */}

{/* region Broken References */}
<BrokenReferencesDialog
{...state.brokenReferences}
onClose={createCallback(state.brokenReferences.onClose, dispatch)}
onClosed={createCallback(state.brokenReferences.onClosed, dispatch)}
onContinue={createCallback(state.brokenReferences.onContinue, dispatch)}
/>
{/* endregion */}

{/* region Reject */}
<RejectDialog
{...state.reject}
Expand Down
2 changes: 2 additions & 0 deletions ui/app/src/models/GlobalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { ModelHierarchyMap } from '../utils/content';
import { UIBlockerStateProps } from '../components/UIBlocker';
import { RenameAssetStateProps } from '../components/RenameAssetDialog';
import Person from './Person';
import { BrokenReferencesDialogStateProps } from '../components/BrokenReferencesDialog/types';

export type HighlightMode = 'all' | 'move';

Expand Down Expand Up @@ -239,6 +240,7 @@ export interface GlobalState {
unlockPublisher: UnlockPublisherDialogStateProps;
widget: WidgetDialogStateProps;
uiBlocker: UIBlockerStateProps;
brokenReferences: BrokenReferencesDialogStateProps;
};
uiConfig: {
error: ApiResponse;
Expand Down
22 changes: 22 additions & 0 deletions ui/app/src/state/actions/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import { SingleFileUploadDialogStateProps } from '../../components/SingleFileUpl
import ContentInstance from '../../models/ContentInstance';
import { ContentTypeFieldValidation, DetailedItem } from '../../models';
import { RenameAssetStateProps } from '../../components/RenameAssetDialog';
import { BrokenReferencesDialogStateProps } from '../../components/BrokenReferencesDialog/types';
import { AjaxError } from 'rxjs/ajax';

// region History
export const showHistoryDialog = /*#__PURE__*/ createAction<Partial<HistoryDialogStateProps>>('SHOW_HISTORY_DIALOG');
Expand Down Expand Up @@ -303,3 +305,23 @@ export const rtePickerActionResult = /*#__PURE__*/ createAction<{ path: string;
'RTE_PICKER_ACTION_RESULT'
);
// endregion

// region BrokenReferences Cancellation

export const showBrokenReferencesDialog = /*#__PURE__*/ createAction<Partial<BrokenReferencesDialogStateProps>>(
'SHOW_BROKEN_REFERENCES_DIALOG'
);

export const closeBrokenReferencesDialog = /*#__PURE__*/ createAction('CLOSE_BROKEN_REFERENCES_DIALOG');

export const brokenReferencesDialogClosed = /*#__PURE__*/ createAction('BROKEN_REFERENCES_DIALOG_CLOSED');

export const fetchBrokenReferences = /*#__PURE__*/ createAction('FETCH_BROKEN_REFERENCES');

export const fetchBrokenReferencesFailed = /*#__PURE__*/ createAction<AjaxError>('FETCH_BROKEN_REFERENCES_FAILED');

export const updateBrokenReferencesDialog = /*#__PURE__*/ createAction<Partial<BrokenReferencesDialogStateProps>>(
'UPDATE_BROKEN_REFERENCES_DIALOG'
);

// endregion
23 changes: 21 additions & 2 deletions ui/app/src/state/epics/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ import {
updateCodeEditorDialog,
updateEditConfig,
updatePreviewDialog,
closeRenameAssetDialog
closeRenameAssetDialog,
fetchBrokenReferences,
updateBrokenReferencesDialog,
fetchBrokenReferencesFailed
} from '../actions/dialogs';
import { fetchDeleteDependencies as fetchDeleteDependenciesService, fetchDependant } from '../../services/dependencies';
import { fetchContentXML, fetchItemVersion } from '../../services/content';
Expand All @@ -64,6 +67,7 @@ import { getHostToGuestBus } from '../../utils/subjects';
import { unlockItem } from '../actions/content';
import { parseLegacyItemToDetailedItem } from '../../utils/content';
import { LegacyItem } from '../../models';
import { parseLegacyItemToSandBoxItem } from '../../utils/content';

function getDialogNameFromType(type: string): string {
let name = getDialogActionNameFromType(type);
Expand Down Expand Up @@ -257,8 +261,23 @@ const dialogEpics: CrafterCMSEpic[] = [
catchAjaxError(fetchRenameAssetDependantsFailed)
)
)
)
),
// endregion
// region fetchBrokenReferences
(action$, state$) =>
action$.pipe(
ofType(fetchBrokenReferences.type),
withLatestFrom(state$),
switchMap(([, state]) =>
fetchDependant(state.sites.active, state.dialogs.brokenReferences.path).pipe(
map((response: LegacyItem[]) => {
const references = parseLegacyItemToSandBoxItem(response);
return updateBrokenReferencesDialog({ references });
}),
catchAjaxError(fetchBrokenReferencesFailed)
)
)
)
] as CrafterCMSEpic[];

export default dialogEpics;
55 changes: 55 additions & 0 deletions ui/app/src/state/reducers/dialogs/brokenReferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2007-2024 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { createReducer } from '@reduxjs/toolkit';
import GlobalState from '../../../models/GlobalState';
import { BrokenReferencesDialogStateProps } from '../../../components/BrokenReferencesDialog/types';
import {
brokenReferencesDialogClosed,
closeBrokenReferencesDialog,
fetchBrokenReferencesFailed,
showBrokenReferencesDialog,
updateBrokenReferencesDialog
} from '../../actions/dialogs';

const initialState: BrokenReferencesDialogStateProps = {
open: false,
isSubmitting: null,
isMinimized: null,
hasPendingChanges: null,
error: null
};

export default createReducer<GlobalState['dialogs']['brokenReferences']>(initialState, (builder) => {
builder
.addCase(showBrokenReferencesDialog, (state, { payload }) => ({
...state,
onClose: closeBrokenReferencesDialog(),
onClosed: brokenReferencesDialogClosed(),
...(payload as Partial<BrokenReferencesDialogStateProps>),
open: true
}))
.addCase(closeBrokenReferencesDialog, (state) => ({ ...state, open: false }))
.addCase(brokenReferencesDialogClosed, () => initialState)
.addCase(updateBrokenReferencesDialog, (state, { payload }) => ({
...state,
...(payload as Partial<BrokenReferencesDialogStateProps>)
}))
.addCase(fetchBrokenReferencesFailed, (state, { payload }) => ({
...state,
error: payload.response
}));
});

0 comments on commit 39b8f72

Please sign in to comment.