modify disable app modal

This commit is contained in:
Davor 2022-08-01 23:02:11 +02:00
parent 1c153ac5ad
commit 08094e233c
12 changed files with 153 additions and 35 deletions

View file

@ -0,0 +1,43 @@
import React from 'react';
import { useController } from 'react-hook-form';
/* eslint-disable react/react-in-jsx-scope */
export const Checkbox = ({ control, name, label, ...props }: CheckboxProps) => {
const {
field,
// fieldState: { invalid, isTouched, isDirty },
// formState: { touchedFields, dirtyFields },
} = useController({
name,
control,
defaultValue: false,
});
return (
<>
{label && (
<label htmlFor={name} className="block text-sm font-medium text-gray-700 mb-1">
{label}
</label>
)}
<input
type="checkbox"
id={name}
onChange={field.onChange} // send value to hook form
onBlur={field.onBlur} // notify when input is touched/blur
checked={field.value}
name={name} // send down the checkbox name
className="shadow-sm focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded"
{...props}
/>
</>
);
};
type CheckboxProps = {
control: any;
name: string;
id?: string;
label?: string;
className?: string;
};

View file

@ -0,0 +1 @@
export { Checkbox } from './Checkbox';

View file

@ -6,7 +6,7 @@ function classNames(...classes: any) {
return classes.filter(Boolean).join(' '); return classes.filter(Boolean).join(' ');
} }
/* eslint-disable react/react-in-jsx-scope */ /* eslint-disable react/react-in-jsx-scope */
export const Switch = ({ control, name, label, required }: SwitchProps) => { export const Switch = ({ control, name, label }: SwitchProps) => {
const { const {
field, field,
// fieldState: { invalid, isTouched, isDirty }, // fieldState: { invalid, isTouched, isDirty },
@ -14,7 +14,6 @@ export const Switch = ({ control, name, label, required }: SwitchProps) => {
} = useController({ } = useController({
name, name,
control, control,
rules: { required },
defaultValue: '', defaultValue: '',
}); });
@ -49,5 +48,4 @@ type SwitchProps = {
control: any; control: any;
name: string; name: string;
label?: string; label?: string;
required?: boolean;
}; };

View file

@ -3,3 +3,4 @@ export { Select } from './Select';
export { Switch } from './Switch'; export { Switch } from './Switch';
export { CodeEditor } from './CodeEditor'; export { CodeEditor } from './CodeEditor';
export { TextArea } from './TextArea'; export { TextArea } from './TextArea';
export { Checkbox } from './Checkbox';

View file

@ -7,9 +7,11 @@ export const Modal: React.FC<ModalProps> = ({
open, open,
onClose, onClose,
onSave, onSave,
saveButtonTitle = 'Save Changes',
children, children,
title = '', title = '',
useCancelButton = false, useCancelButton = false,
cancelButtonTitle = 'Cancel',
isLoading = false, isLoading = false,
leftActions = <></>, leftActions = <></>,
saveButtonDisabled = false, saveButtonDisabled = false,
@ -94,7 +96,7 @@ export const Modal: React.FC<ModalProps> = ({
ref={saveButtonRef} ref={saveButtonRef}
disabled={saveButtonDisabled} disabled={saveButtonDisabled}
> >
Save Changes {saveButtonTitle}
</button> </button>
{useCancelButton && ( {useCancelButton && (
<button <button
@ -103,7 +105,7 @@ export const Modal: React.FC<ModalProps> = ({
onClick={onClose} onClick={onClose}
ref={cancelButtonRef} ref={cancelButtonRef}
> >
Cancel {cancelButtonTitle}
</button> </button>
)} )}
</div> </div>

View file

@ -5,7 +5,9 @@ export type ModalProps = {
onClose: () => void; onClose: () => void;
title?: string; title?: string;
onSave?: () => void; onSave?: () => void;
saveButtonTitle?: string;
useCancelButton?: boolean; useCancelButton?: boolean;
cancelButtonTitle?: string;
isLoading?: boolean; isLoading?: boolean;
leftActions?: React.ReactNode; leftActions?: React.ReactNode;
saveButtonDisabled?: boolean; saveButtonDisabled?: boolean;

View file

@ -1,29 +1,53 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useForm, useWatch } from 'react-hook-form';
import _ from 'lodash'; import _ from 'lodash';
import { XCircleIcon } from '@heroicons/react/outline'; import { XCircleIcon } from '@heroicons/react/outline';
import { useApps } from 'src/services/apps'; import { DisableAppForm, useApps } from 'src/services/apps';
import { Modal, Tabs } from 'src/components'; import { Modal, Tabs } from 'src/components';
import { Checkbox } from 'src/components/Form';
import { appAccessList } from 'src/components/UserModal/consts'; import { appAccessList } from 'src/components/UserModal/consts';
import { AdvancedTab, GeneralTab } from './components'; import { AdvancedTab, GeneralTab } from './components';
export const AppSingle: React.FC = () => { export const AppSingle: React.FC = () => {
const [disableApp, setDisableApp] = useState(false); const [disableAppModal, setDisableAppModal] = useState(false);
const [removeAppData, setRemoveAppData] = useState(false); const [removeAppData, setRemoveAppData] = useState(false);
const params = useParams(); const params = useParams();
const appSlug = params.slug; const appSlug = params.slug;
const { app, loadApp } = useApps(); const { app, loadApp, disableApp, clearSelectedApp } = useApps();
const navigate = useNavigate();
const initialDisableData = { slug: appSlug, removeAppData: false };
const { control, reset, handleSubmit } = useForm<DisableAppForm>({
defaultValues: initialDisableData,
});
const removeAppDataWatch = useWatch({
control,
name: 'removeAppData',
});
useEffect(() => {
setRemoveAppData(removeAppDataWatch);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [removeAppDataWatch]);
useEffect(() => { useEffect(() => {
if (appSlug) { if (appSlug) {
loadApp(appSlug); loadApp(appSlug);
} }
return () => {
clearSelectedApp();
};
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [appSlug]); }, [appSlug]);
if (!app) { if (!app) {
return null; return null;
} }
const appImageSrc = _.find(appAccessList, { name: appSlug })?.image; const appImageSrc = _.find(appAccessList, { name: appSlug })?.image;
const appDocumentationUrl = _.find(appAccessList, { name: appSlug })?.documentationUrl; const appDocumentationUrl = _.find(appAccessList, { name: appSlug })?.documentationUrl;
@ -31,20 +55,28 @@ export const AppSingle: React.FC = () => {
window.open(appDocumentationUrl, '_blank', 'noopener,noreferrer'); window.open(appDocumentationUrl, '_blank', 'noopener,noreferrer');
}; };
const handleAutomaticUpdatesChange = () => {
app.automaticUpdates = !app.automaticUpdates;
};
const tabs = [ const tabs = [
{ {
name: 'General', name: 'General',
component: <GeneralTab automaticUpdates={app.automaticUpdates} onChange={handleAutomaticUpdatesChange} />, component: <GeneralTab automaticUpdates={app.automaticUpdates} onChange={_.noop} />,
}, },
{ name: 'Advanced Configuration', component: <AdvancedTab /> }, { name: 'Advanced Configuration', component: <AdvancedTab /> },
]; ];
const onDisableApp = () => { const onDisableApp = async () => {
// TODO: implement try {
await handleSubmit((data) => disableApp(data))();
} catch (e: any) {
// Continue
}
setDisableAppModal(false);
clearSelectedApp();
navigate('/apps');
};
const handleCloseDisableModal = () => {
reset(initialDisableData);
setDisableAppModal(false);
}; };
return ( return (
@ -62,7 +94,7 @@ export const AppSingle: React.FC = () => {
<button <button
type="button" type="button"
className="mb-3 inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-md text-yellow-900 bg-yellow-300 hover:bg-yellow-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 justify-center" className="mb-3 inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-md text-yellow-900 bg-yellow-300 hover:bg-yellow-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 justify-center"
onClick={() => setDisableApp(true)} onClick={() => setDisableAppModal(true)}
> >
<XCircleIcon className="-ml-0.5 mr-2 h-4 w-4 text-yellow-900" aria-hidden="true" /> <XCircleIcon className="-ml-0.5 mr-2 h-4 w-4 text-yellow-900" aria-hidden="true" />
Disable App Disable App
@ -85,8 +117,15 @@ export const AppSingle: React.FC = () => {
</div> </div>
</div> </div>
{disableApp && ( {disableAppModal && (
<Modal onClose={() => setDisableApp(false)} open={disableApp} onSave={onDisableApp} useCancelButton> <Modal
onClose={handleCloseDisableModal}
open={disableAppModal}
onSave={onDisableApp}
saveButtonTitle={removeAppData ? `Yes, delete and it's data` : 'Yes, delete'}
cancelButtonTitle="No, cancel"
useCancelButton
>
<div className="bg-white px-4"> <div className="bg-white px-4">
<div className="space-y-10 divide-y divide-gray-200"> <div className="space-y-10 divide-y divide-gray-200">
<div> <div>
@ -97,25 +136,16 @@ export const AppSingle: React.FC = () => {
Are you sure you want to disable {app.name}? The app will get uninstalled and none of your users will Are you sure you want to disable {app.name}? The app will get uninstalled and none of your users will
be able to access the app. be able to access the app.
</div> </div>
<fieldset className="space-y-5 -mt-4"> <fieldset className="px-4 py-5 sm:p-6">
<legend className="sr-only">Remove app data</legend>
<div className="relative flex items-start"> <div className="relative flex items-start">
<div className="flex items-center h-5"> <div className="flex items-center h-5">
<input <Checkbox control={control} name="removeAppData" id="removeAppData" />
id="comments"
aria-describedby="comments-description"
name="disableAppData"
type="checkbox"
checked={removeAppData}
onChange={() => setRemoveAppData(!removeAppData)}
className="focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded"
/>
</div> </div>
<div className="ml-3 text-sm"> <div className="ml-3 text-sm">
<label htmlFor="comments" className="font-medium text-gray-700"> <label htmlFor="removeAppData" className="font-medium text-gray-700">
Remove app data Remove app data
</label> </label>
<p id="comments-description" className="text-gray-500"> <p id="removeAppData-description" className="text-gray-500">
{removeAppData {removeAppData
? `The app's data will be removed. After this operation is done you will not be able to access the app, nor the app data. If you re-install the app, it will have none of the data it had before.` ? `The app's data will be removed. After this operation is done you will not be able to access the app, nor the app data. If you re-install the app, it will have none of the data it had before.`
: `The app's data does not get removed. If you install the app again, you will be able to access the data again.`} : `The app's data does not get removed. If you install the app again, you will be able to access the data again.`}

View file

@ -22,6 +22,10 @@ export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps
if (appSlug) { if (appSlug) {
loadApp(appSlug); loadApp(appSlug);
} }
return () => {
reset(initialAppForm);
};
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [appSlug, open]); }, [appSlug, open]);

View file

@ -1,5 +1,5 @@
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { fetchApps, fetchAppBySlug, updateAppBySlug, installAppBySlug, clearCurrentApp } from '../redux'; import { fetchApps, fetchAppBySlug, updateAppBySlug, installAppBySlug, clearCurrentApp, deleteApp } from '../redux';
import { getCurrentApp, getAppLoading, getAppsLoading, getApps } from '../redux/selectors'; import { getCurrentApp, getAppLoading, getAppsLoading, getApps } from '../redux/selectors';
export function useApps() { export function useApps() {
@ -25,6 +25,10 @@ export function useApps() {
return dispatch(installAppBySlug(data)); return dispatch(installAppBySlug(data));
} }
function disableApp(data: any) {
return dispatch(deleteApp(data));
}
function clearSelectedApp() { function clearSelectedApp() {
return dispatch(clearCurrentApp()); return dispatch(clearCurrentApp());
} }
@ -38,6 +42,7 @@ export function useApps() {
appLoading, appLoading,
appTableLoading, appTableLoading,
installApp, installApp,
disableApp,
clearSelectedApp, clearSelectedApp,
}; };
} }

View file

@ -8,6 +8,7 @@ export enum AppActionTypes {
FETCH_APP = 'apps/fetch_app', FETCH_APP = 'apps/fetch_app',
UPDATE_APP = 'apps/update_app', UPDATE_APP = 'apps/update_app',
INSTALL_APP = 'apps/install_app', INSTALL_APP = 'apps/install_app',
DELETE_APP = 'apps/delete_app',
CLEAR_APP = 'apps/clear_app', CLEAR_APP = 'apps/clear_app',
SET_APP_LOADING = 'apps/app_loading', SET_APP_LOADING = 'apps/app_loading',
SET_APPS_LOADING = 'apps/apps_loading', SET_APPS_LOADING = 'apps/apps_loading',
@ -82,8 +83,6 @@ export const updateAppBySlug = (app: any) => async (dispatch: Dispatch<any>) =>
payload: transformApp(data), payload: transformApp(data),
}); });
showToast('App updated successfully.', ToastType.Success);
dispatch(fetchApps()); dispatch(fetchApps());
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -119,6 +118,33 @@ export const installAppBySlug = (app: any) => async (dispatch: Dispatch<any>) =>
dispatch(setAppLoading(false)); dispatch(setAppLoading(false));
}; };
export const deleteApp = (appData: any) => async (dispatch: Dispatch<any>) => {
dispatch(setAppLoading(true));
try {
const { data } = await performApiCall({
path: `/apps/${appData.slug}`,
method: 'DELETE',
body: { remove_app_data: appData.removeAppData },
});
dispatch({
type: AppActionTypes.DELETE_APP,
payload: {},
});
showToast('App disabled', ToastType.Success);
dispatch(fetchApps());
} catch (err: any) {
dispatch(setAppLoading(false));
showToast(`${err}`, ToastType.Error);
throw err;
}
dispatch(setAppLoading(false));
};
export const clearCurrentApp = () => (dispatch: Dispatch<any>) => { export const clearCurrentApp = () => (dispatch: Dispatch<any>) => {
dispatch({ dispatch({
type: AppActionTypes.CLEAR_APP, type: AppActionTypes.CLEAR_APP,

View file

@ -32,6 +32,7 @@ const appsReducer = (state: any = initialUsersState, action: any) => {
currentApp: action.payload, currentApp: action.payload,
}; };
case AppActionTypes.CLEAR_APP: case AppActionTypes.CLEAR_APP:
case AppActionTypes.DELETE_APP:
return { return {
...state, ...state,
currentApp: {}, currentApp: {},

View file

@ -11,6 +11,11 @@ export interface AppForm extends App {
configuration: string; configuration: string;
} }
export interface DisableAppForm {
slug: string;
removeAppData: boolean;
}
export enum AppStatus { export enum AppStatus {
NotInstalled = 'Not installed', NotInstalled = 'Not installed',
Installed = 'Installed', Installed = 'Installed',