Skip to content

Commit

Permalink
feat: enable cancel action when click outside the confirmation modal
Browse files Browse the repository at this point in the history
* feat: enable cancel action when click outside the confirmation modal

* fix: typo in identity

refs: SHELL-115 (#264)
  • Loading branch information
rodleyorosa authored Jun 21, 2023
1 parent 2d5d44d commit 42e7fae
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 38 deletions.
28 changes: 20 additions & 8 deletions src/settings/accounts-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import SettingsSentMessages from './components/account-settings/settings-sent-me
import Delegates, { DelegateType } from './components/account-settings/delegates';
import PersonaSettings from './components/account-settings/persona-settings';
import PersonaUseSection from './components/account-settings/persona-use-section';
import SettingsHeader from './components/settings-header';
import SettingsHeader, { SettingsHeaderProps } from './components/settings-header';
import { getXmlSoapFetch } from '../network/fetch';

// external accounts not yet activated, graphical part is complete
Expand Down Expand Up @@ -265,27 +265,34 @@ export const AccountsSettings = ({
setMods({});
}, [identitiesDefault]);

const onSave = useCallback(() => {
const onSave = useCallback<SettingsHeaderProps['onSave']>(() => {
if (
maxIdentities !== undefined &&
identitiesDefault.length +
(mods.identity?.createList?.length || 0) -
(mods?.identity?.deleteList?.length || 0) >
maxIdentities
maxIdentities
) {
createSnackbar({
key: `new`,
replace: true,
type: 'error',
label: t(
'message.snackbar.identities_quota_exceeded',
'The identitity could not be created because you have exceeded your identity quota'
'The identity could not be created because you have exceeded your identity quota'
),
autoHideTimeout: 5000,
hideButton: true
});
return;
return Promise.allSettled([
Promise.reject(
new Error(
'The identity could not be created because you have exceeded your identity quota'
)
)
]);
}
editSettings(mods)
const promise = editSettings(mods)
.then(() => {
createSnackbar({
key: `new`,
Expand All @@ -295,8 +302,9 @@ export const AccountsSettings = ({
autoHideTimeout: 3000,
hideButton: true
});
setMods({});
})
.catch(() => {
.catch((error: unknown) => {
createSnackbar({
key: `new`,
replace: true,
Expand All @@ -305,8 +313,12 @@ export const AccountsSettings = ({
autoHideTimeout: 3000,
hideButton: true
});
if (error instanceof Error) {
throw error;
}
throw new Error(typeof error === 'string' ? error : 'edit setting error');
});
setMods({});
return Promise.allSettled([promise]);
}, [identitiesDefault.length, mods, maxIdentities, createSnackbar, t]);

const onCancel = useCallback(() => setMods({}), []);
Expand Down
6 changes: 3 additions & 3 deletions src/settings/components/settings-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ import React, { useEffect, useMemo } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { SETTINGS_APP_ID } from '../../constants';
import { getT } from '../../store/i18n';
import { RouteLeavingGuard } from '../../ui-extras/nav-guard';
import { RouteLeavingGuard, RouteLeavingGuardProps } from '../../ui-extras/nav-guard';

type SettingsHeaderProps = {
export type SettingsHeaderProps = {
title: string;
onSave: () => void;
onSave: RouteLeavingGuardProps['onSave'];
onCancel: () => void;
isDirty: boolean;
};
Expand Down
16 changes: 11 additions & 5 deletions src/settings/general-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import Logout from './components/general-settings/logout';
import ModuleVersionSettings from './components/general-settings/module-version-settings';
import { OutOfOfficeSettings } from './components/general-settings/out-of-office-settings';
import UserQuota from './components/general-settings/user-quota';
import SettingsHeader from './components/settings-header';
import SettingsHeader, { SettingsHeaderProps } from './components/settings-header';
import LanguageAndTimeZoneSettings from './language-and-timezone-settings';
import { SearchSettings } from './components/general-settings/search-settings';
import { ScalingSettingSection } from './components/general-settings/scaling-setting-section';
Expand Down Expand Up @@ -83,7 +83,7 @@ const GeneralSettings = (): JSX.Element => {
}, []);
const createSnackbar = useSnackbar();

const onSave = useCallback(() => {
const onSave = useCallback<SettingsHeaderProps['onSave']>(() => {
setLocalStorageUnAppliedChanges((unAppliedPrevState) => {
if (size(unAppliedPrevState) > 0) {
setLocalStorageSettings((localStorageSettingsPrevState) => ({
Expand All @@ -95,7 +95,7 @@ const GeneralSettings = (): JSX.Element => {
return unAppliedPrevState;
});
if (size(mods) > 0) {
editSettings(mods)
const promise = editSettings(mods)
.then(() => {
if (mods.prefs && includes(Object.keys(mods.prefs), 'zimbraPrefLocale')) {
setOpen(true);
Expand All @@ -108,8 +108,9 @@ const GeneralSettings = (): JSX.Element => {
autoHideTimeout: 3000,
hideButton: true
});
setMods({});
})
.catch(() => {
.catch((error: unknown) => {
createSnackbar({
key: `new`,
replace: true,
Expand All @@ -118,9 +119,14 @@ const GeneralSettings = (): JSX.Element => {
autoHideTimeout: 3000,
hideButton: true
});
if (error instanceof Error) {
throw error;
}
throw new Error(typeof error === 'string' ? error : 'edit setting error');
});
setMods({});
return Promise.allSettled([promise]);
}
return Promise.allSettled([Promise.resolve()]);
}, [mods, setLocalStorageSettings, createSnackbar, t]);

const scalingSettingSectionRef = useRef<ResetComponentImperativeHandler>(null);
Expand Down
96 changes: 77 additions & 19 deletions src/ui-extras/nav-guard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,121 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Modal } from '@zextras/carbonio-design-system';
import React, { useEffect, useState, FC, useMemo } from 'react';

import { Modal, Button, ModalProps } from '@zextras/carbonio-design-system';
import { Location } from 'history';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { filter } from 'lodash';
import { useTranslation } from 'react-i18next';
import { Prompt, useHistory } from 'react-router-dom';
import { getT } from '../store/i18n';

export const RouteLeavingGuard: FC<{
export interface RouteLeavingGuardProps {
children: ModalProps['children'];
when?: boolean;
onSave: () => void;
}> = ({ children, when, onSave }) => {
onSave: () => Promise<PromiseSettledResult<Awaited<unknown>>[]>;
dataHasError?: boolean;
}

export const RouteLeavingGuard = ({
children,
when,
onSave,
dataHasError = false
}: RouteLeavingGuardProps): JSX.Element => {
const history = useHistory();
const lastLocationInitial = useMemo(() => history.location.pathname, [history]);
const lastLocationInitial = useMemo(() => history.location, [history]);
const [modalVisible, setModalVisible] = useState(false);
const [lastLocation, setLastLocation] = useState<Location>(lastLocationInitial);
const [confirmedNavigation, setConfirmedNavigation] = useState(false);
const t = getT();
const onClose = (): void => {
const [t] = useTranslation();
const cancel = (): void => {
setModalVisible(false);
setConfirmedNavigation(true);
setConfirmedNavigation(false);
};

const handleBlockedNavigation = (nextLocation: Location): boolean => {
if (
!confirmedNavigation &&
nextLocation.pathname !== (lastLocation?.pathname || lastLocationInitial)
`${nextLocation.pathname}${nextLocation.search || ''}` !==
`${history.location.pathname}${history.location.search}`
) {
setModalVisible(true);
setLastLocation(nextLocation);
return false;
}
return true;
};

const onConfirm = (): void => {
onSave()
.then((results) => {
const rejected = filter(
results,
(result): result is PromiseRejectedResult => result.status === 'rejected'
);
if (rejected.length > 0) {
console.error(rejected);
cancel();
} else {
setModalVisible(false);
setConfirmedNavigation(true);
}
})
.catch((reason) => {
console.error(reason);
cancel();
});
};

const onSecondaryAction = (): void => {
setModalVisible(false);
onSave();
setConfirmedNavigation(true);
};

useEffect(() => {
if (confirmedNavigation && lastLocation) {
// Navigate to the previous blocked location with your navigate function
history.push(lastLocation.pathname);
history.push(lastLocation);
}
}, [confirmedNavigation, history, lastLocation]);

return (
<>
<Prompt when={when} message={handleBlockedNavigation} />
{/* Your own alert/dialog/modal component */}
<Modal
showCloseIcon
closeIconTooltip={t('label.close', 'Close')}
open={modalVisible}
onClose={onClose}
onConfirm={onConfirm}
title={t('label.unsaved_changes', 'You have unsaved changes')}
dismissLabel={t('label.leave_anyway', 'Leave anyway')}
confirmLabel={t('label.save_and_leave', 'Save and leave')}
title={
dataHasError
? t('label.cannot_save_changes', 'Some changes cannot be saved')
: t('label.unsaved_changes', 'You have unsaved changes')
}
onClose={cancel}
onConfirm={dataHasError ? onSecondaryAction : onConfirm}
confirmLabel={
dataHasError
? t('label.leave_anyway', 'Leave anyway')
: t('label.save_and_leave', 'Save and leave')
}
onSecondaryAction={dataHasError ? cancel : onSecondaryAction}
secondaryActionLabel={
dataHasError ? t('label.cancel', 'Cancel') : t('label.leave_anyway', 'Leave anyway')
}
optionalFooter={
!dataHasError ? (
<Button
color="secondary"
type="outlined"
label={t('label.cancel', 'Cancel')}
onClick={cancel}
/>
) : undefined
}
>
{children}
</Modal>
</>
);
};
export default RouteLeavingGuard;
2 changes: 2 additions & 0 deletions translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@
"advanced_settings": "Advanced Settings",
"button": "Button",
"cancel": "Cancel",
"cannot_save_changes": "Some changes cannot be saved",
"change_pop": "Change POP port",
"choose_account": "Choose an account",
"choose_folder": "Choose Folder",
"clear_search_query": "CLEAR SEARCH",
"click_to_copy": "Click to copy",
"close": "Close",
"delegates": "Delegates",
"delete": "Delete",
"delete_after_download": "Delete messages from the server after the download",
Expand Down
11 changes: 8 additions & 3 deletions types/account/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,16 @@ export interface AccountSettingsPrefs {
[key: string]: string | number | Array<string | number>;
}

export interface AccountSettings {
attrs: Record<string, string | number | Array<string | number>>;
export type AccountSettingsAttrs = {
zimbraIdentityMaxNumEntries?: number;
[key: string]: string | number | Array<string | number>;
};

export type AccountSettings = {
attrs: AccountSettingsAttrs;
prefs: AccountSettingsPrefs;
props: Array<ZimletProp>;
}
};

export type AccountRightTargetEmail = {
addr: string;
Expand Down

0 comments on commit 42e7fae

Please sign in to comment.