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

[5369] Design and implement a Recycle Bin #3126

Closed
wants to merge 7 commits into from
Closed
3 changes: 3 additions & 0 deletions ui/app/src/components/ActionsBar/ActionsBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import TranslationOrText from '../../models/TranslationOrText';
import Skeleton from '@mui/material/Skeleton';
import { rand } from '../PathNavigator/utils';
import Box, { BoxProps } from '@mui/material/Box';
import SystemIcon, { SystemIconDescriptor } from '../SystemIcon';

export interface ActionsBarAction {
id: string;
label: TranslationOrText;
icon?: SystemIconDescriptor;
}

interface ActionsBarProps {
Expand Down Expand Up @@ -97,6 +99,7 @@ export function ActionsBar(props: ActionsBarProps) {
disabled={disabled}
{...buttonProps}
onClick={() => onOptionClicked(option.id)}
startIcon={option.icon ? <SystemIcon icon={option.icon} /> : null}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this for?

>
{getPossibleTranslation(option.label, formatMessage)}
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const states = {
disabled: { stateMap: { disabled: true } }
};

const status = {
export const status = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a much better name if it is to be exported

staged: { stateMap: { staged: true } },
live: { stateMap: { live: true } }
};
Expand Down
13 changes: 11 additions & 2 deletions ui/app/src/components/LauncherLinkTile/LauncherLinkTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ const LauncherLinkTile = (props: LauncherLinkTileProps) => {
const site = useActiveSiteId();
const dispatch = useDispatch();
const title = usePossibleTranslation(propTitle);
const isDialog = ['siteDashboardDialog', 'siteToolsDialog', 'siteSearchDialog'].includes(systemLinkId);
const isDialog = ['siteDashboardDialog', 'siteToolsDialog', 'siteSearchDialog', 'siteRecycleBin'].includes(
systemLinkId
);

const onClick = isDialog
? (e) => {
Expand All @@ -50,6 +52,8 @@ const LauncherLinkTile = (props: LauncherLinkTileProps) => {
const id = systemLinkId === 'siteDashboardDialog' ? 'craftercms.components.Dashboard' : (
systemLinkId === 'siteToolsDialog'
? 'craftercms.components.EmbeddedSiteTools'
: systemLinkId === 'siteRecycleBin'
? 'craftercms.components.RecycleBin'
: 'craftercms.components.Search'
);
Comment on lines 52 to 58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace this with a lookup table

dispatch(
Expand All @@ -58,7 +62,12 @@ const LauncherLinkTile = (props: LauncherLinkTileProps) => {
showWidgetDialog({
id: systemLinkId,
title,
widget: { id, ...(systemLinkId === 'siteSearchDialog' && { configuration: { embedded: true } }) }
widget: {
id,
...((systemLinkId === 'siteSearchDialog' || systemLinkId === 'siteRecycleBin') && {
configuration: { embedded: true }
})
}
})
])
);
Expand Down
221 changes: 221 additions & 0 deletions ui/app/src/components/RecycleBin/RecycleBin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
* Copyright (C) 2007-2022 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, { useCallback, useEffect, useState } from 'react';
import RecycleBinGridUI from './RecycleBinGridUI';
import Box from '@mui/material/Box';
import { ViewToolbar } from '../ViewToolbar';
import { SearchBar } from '../SearchBar';
import { ActionsBar } from '../ActionsBar';
import useEnhancedDialogState from '../../hooks/useEnhancedDialogState';
import useWithPendingChangesCloseRequest from '../../hooks/useWithPendingChangesCloseRequest';
import { RecycleBinPackageDialog } from '../RecycleBinPackageDialog';
import { RecycleBinPackage, RecycleBinProps } from './utils';
import { FormattedMessage, useIntl } from 'react-intl';
import { translations } from './translations';
import UseWithPendingChangesCloseRequest from '../../hooks/useWithPendingChangesCloseRequest';
import { RecycleBinRestoreDialog } from '../RecycleBinRestoreDialog';
import { asArray } from '../../utils/array';
import { fetchRecycleBinPackages, restoreRecycleBinPackages } from '../../services/content';
import useActiveSiteId from '../../hooks/useActiveSiteId';
import { useDispatch } from 'react-redux';
import { showErrorDialog } from '../../state/reducers/dialogs/error';
import { showSystemNotification } from '../../state/actions/system';
import { useStyles } from './styles';
import { GlobalAppToolbar } from '../GlobalAppToolbar';
import { ApiResponseErrorState } from '../ApiResponseErrorState';
import { LoadingState } from '../LoadingState';

export function RecycleBin(props: RecycleBinProps) {
const { embedded } = props;
const { classes } = useStyles();
const [pageSize, setPageSize] = useState(10);
const [recycleBinPackages, setRecycleBinPackages] = useState<RecycleBinPackage[]>([]);
const [selectedPackages, setSelectedPackages] = useState([]);
const [recycleBinPackage, setRecycleBinPackage] = useState(null);
const [restorePackages, setRestorePackages] = useState([]);
const isAllChecked = recycleBinPackages.length > 0 && selectedPackages.length === recycleBinPackages.length;
const isIndeterminate = selectedPackages.length > 0 && selectedPackages.length < recycleBinPackages.length;
const { formatMessage } = useIntl();
const siteId = useActiveSiteId();
const dispatch = useDispatch();
const [fetchingPackages, setFetchingPackages] = useState(false);
const [error, setError] = useState();
const fetchPackages = useCallback((siteId) => {
setFetchingPackages(true);
return fetchRecycleBinPackages(siteId).subscribe({
next(packages) {
setRecycleBinPackages(packages);
setFetchingPackages(false);
},
error(error) {
setError(error);
setFetchingPackages(false);
}
});
}, []);

const recycleBinPackageDialogState = useEnhancedDialogState();
const recycleBinPackageDialogPendingChangesCloseRequest = useWithPendingChangesCloseRequest(
recycleBinPackageDialogState.onClose
);

const recycleBinRestoreDialogState = useEnhancedDialogState();
const recycleBinRestoreDialogPendingChangesCloseRequest = UseWithPendingChangesCloseRequest(
recycleBinRestoreDialogState.onClose
);

useEffect(() => {
fetchPackages(siteId);
}, [siteId, fetchPackages]);

const onToggleCheckedAll = () => {
if (isAllChecked) {
setSelectedPackages([]);
} else {
const checked = [];
recycleBinPackages.forEach((recycleBinPackage) => checked.push(recycleBinPackage.id));
setSelectedPackages(checked);
}
};

const onOpenPackageDetails = (recycleBinPackage) => {
setRecycleBinPackage(recycleBinPackage);
recycleBinPackageDialogState.onOpen();
};

const onShowRestoreDialog = (packages: RecycleBinPackage[] | RecycleBinPackage) => {
setRestorePackages(asArray(packages));
recycleBinRestoreDialogState.onOpen();
};

const onRestore = (ids: string[]) => {
recycleBinRestoreDialogState.onSubmittingAndOrPendingChange({ isSubmitting: true });
restoreRecycleBinPackages(ids).subscribe({
next() {
recycleBinRestoreDialogState.onSubmittingAndOrPendingChange({ isSubmitting: false });
recycleBinRestoreDialogState.onClose();
setSelectedPackages([]);
fetchPackages(siteId);
dispatch(
showSystemNotification({
message: formatMessage(translations.restoreSuccess)
})
);
},
error(error) {
dispatch(showErrorDialog({ error }));
recycleBinRestoreDialogState.onSubmittingAndOrPendingChange({ isSubmitting: false });
}
});
};

const onActionBarOptionClicked = (option: string) => {
if (option === 'restore') {
const packages = recycleBinPackages.filter((recycleBinPackage) =>
selectedPackages.includes(recycleBinPackage.id)
);
onShowRestoreDialog(packages);
}
};

return (
<Box>
{!embedded && (
<GlobalAppToolbar
title={<FormattedMessage id="recycleBin.title" defaultMessage="Recycle Bin" />}
showHamburgerMenuButton
showAppsButton
/>
)}
{error ? (
<ApiResponseErrorState error={error} />
) : fetchingPackages ? (
<LoadingState />
) : (
<>
<ViewToolbar styles={{ toolbar: { justifyContent: 'center' } }}>
<section className={classes.searchBarContainer}>
<SearchBar
onChange={() => {}} // TODO: pending
keyword={null}
showActionButton={false}
showDecoratorIcon
classes={{ root: classes.searchPaper }}
/>
</section>
</ViewToolbar>
<Box>
{(isIndeterminate || isAllChecked) && (
<ActionsBar
classes={{
root: classes.actionsBarRoot,
checkbox: classes.actionsBarCheckbox
}}
options={[
{
id: 'restore',
label: formatMessage(translations.restore),
icon: { id: '@mui/icons-material/SettingsBackupRestoreOutlined' }
},
{
id: 'publish',
label: formatMessage(translations.publishDeletion),
icon: { id: '@mui/icons-material/CloudUploadOutlined' }
}
]}
isIndeterminate={isIndeterminate}
isChecked={isAllChecked}
numOfSkeletonItems={2}
onOptionClicked={onActionBarOptionClicked}
onCheckboxChange={onToggleCheckedAll}
/>
)}
<RecycleBinGridUI
packages={recycleBinPackages}
pageSize={pageSize}
setPageSize={setPageSize}
selectedPackages={selectedPackages}
setSelectedPackages={setSelectedPackages}
onOpenPackageDetails={onOpenPackageDetails}
/>
</Box>
</>
)}
<RecycleBinPackageDialog
open={recycleBinPackageDialogState.open}
onClose={recycleBinPackageDialogState.onClose}
recycleBinPackage={recycleBinPackage}
onRestore={() => {
recycleBinPackageDialogState.onClose();
onShowRestoreDialog(recycleBinPackage);
}}
onWithPendingChangesCloseRequest={recycleBinPackageDialogPendingChangesCloseRequest}
onSubmittingAndOrPendingChange={recycleBinPackageDialogState.onSubmittingAndOrPendingChange}
/>
<RecycleBinRestoreDialog
open={recycleBinRestoreDialogState.open}
onClose={recycleBinRestoreDialogState.onClose}
packages={restorePackages}
onRestore={onRestore}
onWithPendingChangesCloseRequest={recycleBinRestoreDialogPendingChangesCloseRequest}
onSubmittingAndOrPendingChange={recycleBinRestoreDialogState.onSubmittingAndOrPendingChange}
/>
</Box>
);
}

export default RecycleBin;
Loading