modify disable app modal
This commit is contained in:
parent
1c153ac5ad
commit
08094e233c
12 changed files with 153 additions and 35 deletions
43
src/components/Form/Checkbox/Checkbox.tsx
Normal file
43
src/components/Form/Checkbox/Checkbox.tsx
Normal 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;
|
||||||
|
};
|
1
src/components/Form/Checkbox/index.ts
Normal file
1
src/components/Form/Checkbox/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { Checkbox } from './Checkbox';
|
|
@ -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;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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.`}
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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: {},
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in a new issue