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

[4.1.x][6527] Cut/Paste via the Sidebar should warn users or allow them to act on broken dependencies caused by the move operation #3761

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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'> {}
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 @@ -389,6 +390,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
}));
});