diff --git a/src/components/Form/Checkbox/Checkbox.tsx b/src/components/Form/Checkbox/Checkbox.tsx new file mode 100644 index 0000000..e31c20c --- /dev/null +++ b/src/components/Form/Checkbox/Checkbox.tsx @@ -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} + + )} + + > + ); +}; + +type CheckboxProps = { + control: any; + name: string; + id?: string; + label?: string; + className?: string; +}; diff --git a/src/components/Form/Checkbox/index.ts b/src/components/Form/Checkbox/index.ts new file mode 100644 index 0000000..8f59e4f --- /dev/null +++ b/src/components/Form/Checkbox/index.ts @@ -0,0 +1 @@ +export { Checkbox } from './Checkbox'; diff --git a/src/components/Form/Switch/Switch.tsx b/src/components/Form/Switch/Switch.tsx index 4c10780..d1596da 100644 --- a/src/components/Form/Switch/Switch.tsx +++ b/src/components/Form/Switch/Switch.tsx @@ -6,7 +6,7 @@ function classNames(...classes: any) { return classes.filter(Boolean).join(' '); } /* eslint-disable react/react-in-jsx-scope */ -export const Switch = ({ control, name, label, required }: SwitchProps) => { +export const Switch = ({ control, name, label }: SwitchProps) => { const { field, // fieldState: { invalid, isTouched, isDirty }, @@ -14,7 +14,6 @@ export const Switch = ({ control, name, label, required }: SwitchProps) => { } = useController({ name, control, - rules: { required }, defaultValue: '', }); @@ -49,5 +48,4 @@ type SwitchProps = { control: any; name: string; label?: string; - required?: boolean; }; diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts index 19728f2..efb5065 100644 --- a/src/components/Form/index.ts +++ b/src/components/Form/index.ts @@ -3,3 +3,4 @@ export { Select } from './Select'; export { Switch } from './Switch'; export { CodeEditor } from './CodeEditor'; export { TextArea } from './TextArea'; +export { Checkbox } from './Checkbox'; diff --git a/src/components/Modal/Modal/Modal.tsx b/src/components/Modal/Modal/Modal.tsx index e61aaee..d64acfe 100644 --- a/src/components/Modal/Modal/Modal.tsx +++ b/src/components/Modal/Modal/Modal.tsx @@ -7,9 +7,11 @@ export const Modal: React.FC = ({ open, onClose, onSave, + saveButtonTitle = 'Save Changes', children, title = '', useCancelButton = false, + cancelButtonTitle = 'Cancel', isLoading = false, leftActions = <>>, saveButtonDisabled = false, @@ -94,7 +96,7 @@ export const Modal: React.FC = ({ ref={saveButtonRef} disabled={saveButtonDisabled} > - Save Changes + {saveButtonTitle} {useCancelButton && ( = ({ onClick={onClose} ref={cancelButtonRef} > - Cancel + {cancelButtonTitle} )} diff --git a/src/components/Modal/Modal/types.ts b/src/components/Modal/Modal/types.ts index e679e69..52cf784 100644 --- a/src/components/Modal/Modal/types.ts +++ b/src/components/Modal/Modal/types.ts @@ -5,7 +5,9 @@ export type ModalProps = { onClose: () => void; title?: string; onSave?: () => void; + saveButtonTitle?: string; useCancelButton?: boolean; + cancelButtonTitle?: string; isLoading?: boolean; leftActions?: React.ReactNode; saveButtonDisabled?: boolean; diff --git a/src/modules/apps/AppSingle.tsx b/src/modules/apps/AppSingle.tsx index d65b825..2d074a2 100644 --- a/src/modules/apps/AppSingle.tsx +++ b/src/modules/apps/AppSingle.tsx @@ -1,29 +1,53 @@ 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 { 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 { Checkbox } from 'src/components/Form'; import { appAccessList } from 'src/components/UserModal/consts'; import { AdvancedTab, GeneralTab } from './components'; export const AppSingle: React.FC = () => { - const [disableApp, setDisableApp] = useState(false); + const [disableAppModal, setDisableAppModal] = useState(false); const [removeAppData, setRemoveAppData] = useState(false); const params = useParams(); 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({ + defaultValues: initialDisableData, + }); + + const removeAppDataWatch = useWatch({ + control, + name: 'removeAppData', + }); + + useEffect(() => { + setRemoveAppData(removeAppDataWatch); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [removeAppDataWatch]); useEffect(() => { if (appSlug) { loadApp(appSlug); } + + return () => { + clearSelectedApp(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [appSlug]); if (!app) { return null; } + const appImageSrc = _.find(appAccessList, { name: appSlug })?.image; const appDocumentationUrl = _.find(appAccessList, { name: appSlug })?.documentationUrl; @@ -31,20 +55,28 @@ export const AppSingle: React.FC = () => { window.open(appDocumentationUrl, '_blank', 'noopener,noreferrer'); }; - const handleAutomaticUpdatesChange = () => { - app.automaticUpdates = !app.automaticUpdates; - }; - const tabs = [ { name: 'General', - component: , + component: , }, { name: 'Advanced Configuration', component: }, ]; - const onDisableApp = () => { - // TODO: implement + const onDisableApp = async () => { + try { + await handleSubmit((data) => disableApp(data))(); + } catch (e: any) { + // Continue + } + setDisableAppModal(false); + clearSelectedApp(); + navigate('/apps'); + }; + + const handleCloseDisableModal = () => { + reset(initialDisableData); + setDisableAppModal(false); }; return ( @@ -62,7 +94,7 @@ export const AppSingle: React.FC = () => { setDisableApp(true)} + onClick={() => setDisableAppModal(true)} > Disable App @@ -85,8 +117,15 @@ export const AppSingle: React.FC = () => { - {disableApp && ( - setDisableApp(false)} open={disableApp} onSave={onDisableApp} useCancelButton> + {disableAppModal && ( + @@ -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 be able to access the app. - - Remove app data + - setRemoveAppData(!removeAppData)} - className="focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded" - /> + - + Remove app data - + {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 does not get removed. If you install the app again, you will be able to access the data again.`} diff --git a/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx b/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx index 8bd8d4e..3c6f7ee 100644 --- a/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx +++ b/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx @@ -22,6 +22,10 @@ export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps if (appSlug) { loadApp(appSlug); } + + return () => { + reset(initialAppForm); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [appSlug, open]); diff --git a/src/services/apps/hooks/use-apps.ts b/src/services/apps/hooks/use-apps.ts index 9137f36..919bebe 100644 --- a/src/services/apps/hooks/use-apps.ts +++ b/src/services/apps/hooks/use-apps.ts @@ -1,5 +1,5 @@ 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'; export function useApps() { @@ -25,6 +25,10 @@ export function useApps() { return dispatch(installAppBySlug(data)); } + function disableApp(data: any) { + return dispatch(deleteApp(data)); + } + function clearSelectedApp() { return dispatch(clearCurrentApp()); } @@ -38,6 +42,7 @@ export function useApps() { appLoading, appTableLoading, installApp, + disableApp, clearSelectedApp, }; } diff --git a/src/services/apps/redux/actions.ts b/src/services/apps/redux/actions.ts index 47c365f..8bd4d48 100644 --- a/src/services/apps/redux/actions.ts +++ b/src/services/apps/redux/actions.ts @@ -8,6 +8,7 @@ export enum AppActionTypes { FETCH_APP = 'apps/fetch_app', UPDATE_APP = 'apps/update_app', INSTALL_APP = 'apps/install_app', + DELETE_APP = 'apps/delete_app', CLEAR_APP = 'apps/clear_app', SET_APP_LOADING = 'apps/app_loading', SET_APPS_LOADING = 'apps/apps_loading', @@ -82,8 +83,6 @@ export const updateAppBySlug = (app: any) => async (dispatch: Dispatch) => payload: transformApp(data), }); - showToast('App updated successfully.', ToastType.Success); - dispatch(fetchApps()); } catch (err) { console.error(err); @@ -119,6 +118,33 @@ export const installAppBySlug = (app: any) => async (dispatch: Dispatch) => dispatch(setAppLoading(false)); }; +export const deleteApp = (appData: any) => async (dispatch: Dispatch) => { + 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) => { dispatch({ type: AppActionTypes.CLEAR_APP, diff --git a/src/services/apps/redux/reducers.ts b/src/services/apps/redux/reducers.ts index c978377..e303153 100644 --- a/src/services/apps/redux/reducers.ts +++ b/src/services/apps/redux/reducers.ts @@ -32,6 +32,7 @@ const appsReducer = (state: any = initialUsersState, action: any) => { currentApp: action.payload, }; case AppActionTypes.CLEAR_APP: + case AppActionTypes.DELETE_APP: return { ...state, currentApp: {}, diff --git a/src/services/apps/types.ts b/src/services/apps/types.ts index 76a418f..e3af6aa 100644 --- a/src/services/apps/types.ts +++ b/src/services/apps/types.ts @@ -11,6 +11,11 @@ export interface AppForm extends App { configuration: string; } +export interface DisableAppForm { + slug: string; + removeAppData: boolean; +} + export enum AppStatus { NotInstalled = 'Not installed', Installed = 'Installed',
+
{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 does not get removed. If you install the app again, you will be able to access the data again.`} diff --git a/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx b/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx index 8bd8d4e..3c6f7ee 100644 --- a/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx +++ b/src/modules/apps/components/AppInstallModal/AppInstallModal.tsx @@ -22,6 +22,10 @@ export const AppInstallModal = ({ open, onClose, appSlug }: AppInstallModalProps if (appSlug) { loadApp(appSlug); } + + return () => { + reset(initialAppForm); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [appSlug, open]); diff --git a/src/services/apps/hooks/use-apps.ts b/src/services/apps/hooks/use-apps.ts index 9137f36..919bebe 100644 --- a/src/services/apps/hooks/use-apps.ts +++ b/src/services/apps/hooks/use-apps.ts @@ -1,5 +1,5 @@ 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'; export function useApps() { @@ -25,6 +25,10 @@ export function useApps() { return dispatch(installAppBySlug(data)); } + function disableApp(data: any) { + return dispatch(deleteApp(data)); + } + function clearSelectedApp() { return dispatch(clearCurrentApp()); } @@ -38,6 +42,7 @@ export function useApps() { appLoading, appTableLoading, installApp, + disableApp, clearSelectedApp, }; } diff --git a/src/services/apps/redux/actions.ts b/src/services/apps/redux/actions.ts index 47c365f..8bd4d48 100644 --- a/src/services/apps/redux/actions.ts +++ b/src/services/apps/redux/actions.ts @@ -8,6 +8,7 @@ export enum AppActionTypes { FETCH_APP = 'apps/fetch_app', UPDATE_APP = 'apps/update_app', INSTALL_APP = 'apps/install_app', + DELETE_APP = 'apps/delete_app', CLEAR_APP = 'apps/clear_app', SET_APP_LOADING = 'apps/app_loading', SET_APPS_LOADING = 'apps/apps_loading', @@ -82,8 +83,6 @@ export const updateAppBySlug = (app: any) => async (dispatch: Dispatch) => payload: transformApp(data), }); - showToast('App updated successfully.', ToastType.Success); - dispatch(fetchApps()); } catch (err) { console.error(err); @@ -119,6 +118,33 @@ export const installAppBySlug = (app: any) => async (dispatch: Dispatch) => dispatch(setAppLoading(false)); }; +export const deleteApp = (appData: any) => async (dispatch: Dispatch) => { + 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) => { dispatch({ type: AppActionTypes.CLEAR_APP, diff --git a/src/services/apps/redux/reducers.ts b/src/services/apps/redux/reducers.ts index c978377..e303153 100644 --- a/src/services/apps/redux/reducers.ts +++ b/src/services/apps/redux/reducers.ts @@ -32,6 +32,7 @@ const appsReducer = (state: any = initialUsersState, action: any) => { currentApp: action.payload, }; case AppActionTypes.CLEAR_APP: + case AppActionTypes.DELETE_APP: return { ...state, currentApp: {}, diff --git a/src/services/apps/types.ts b/src/services/apps/types.ts index 76a418f..e3af6aa 100644 --- a/src/services/apps/types.ts +++ b/src/services/apps/types.ts @@ -11,6 +11,11 @@ export interface AppForm extends App { configuration: string; } +export interface DisableAppForm { + slug: string; + removeAppData: boolean; +} + export enum AppStatus { NotInstalled = 'Not installed', Installed = 'Installed',