From b42c33107efc660479bc92c80ac9d2c30047a55c Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Sun, 19 Mar 2023 00:34:14 +0530 Subject: [PATCH] feat(ui): added api hooks and state hooks for dashboard --- frontend/src/hooks/api/index.tsx | 2 + .../src/hooks/api/secretSnapshots/index.tsx | 6 + .../src/hooks/api/secretSnapshots/queries.tsx | 154 ++++++++++++++++ .../src/hooks/api/secretSnapshots/types.ts | 32 ++++ frontend/src/hooks/api/secrets/index.ts | 1 + frontend/src/hooks/api/secrets/queries.tsx | 173 ++++++++++++++++++ frontend/src/hooks/api/secrets/types.ts | 104 +++++++++++ frontend/src/hooks/api/types.ts | 1 + frontend/src/hooks/api/users/index.tsx | 2 + frontend/src/hooks/api/users/queries.tsx | 26 +++ frontend/src/hooks/api/workspace/index.tsx | 4 +- frontend/src/hooks/api/workspace/queries.tsx | 22 ++- frontend/src/hooks/api/workspace/types.ts | 13 +- frontend/src/hooks/index.ts | 1 + frontend/src/hooks/useLeaveConfirm.tsx | 48 ++--- frontend/src/hooks/usePersistentState.ts | 25 +++ 16 files changed, 588 insertions(+), 26 deletions(-) create mode 100644 frontend/src/hooks/api/secretSnapshots/index.tsx create mode 100644 frontend/src/hooks/api/secretSnapshots/queries.tsx create mode 100644 frontend/src/hooks/api/secretSnapshots/types.ts create mode 100644 frontend/src/hooks/api/secrets/index.ts create mode 100644 frontend/src/hooks/api/secrets/queries.tsx create mode 100644 frontend/src/hooks/api/secrets/types.ts create mode 100644 frontend/src/hooks/usePersistentState.ts diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index c2035bbba..66d1a3933 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -2,6 +2,8 @@ export * from './auth'; export * from './incidentContacts'; export * from './keys'; export * from './organization'; +export * from './secrets'; +export * from './secretSnapshots'; export * from './serviceTokens'; export * from './subscriptions'; export * from './tags'; diff --git a/frontend/src/hooks/api/secretSnapshots/index.tsx b/frontend/src/hooks/api/secretSnapshots/index.tsx new file mode 100644 index 000000000..7d6b368e7 --- /dev/null +++ b/frontend/src/hooks/api/secretSnapshots/index.tsx @@ -0,0 +1,6 @@ +export { + useGetSnapshotSecrets, + useGetWorkspaceSecretSnapshots, + useGetWsSnapshotCount, + usePerformSecretRollback +} from './queries'; diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx new file mode 100644 index 000000000..3aa9dfbe5 --- /dev/null +++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx @@ -0,0 +1,154 @@ +/* eslint-disable no-param-reassign */ +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { + decryptAssymmetric, + decryptSymmetric +} from '@app/components/utilities/cryptography/crypto'; +import { apiRequest } from '@app/config/request'; + +import { DecryptedSecret } from '../secrets/types'; +import { + GetWorkspaceSecretSnapshotsDTO, + TSecretRollbackDTO, + TSnapshotSecret, + TSnapshotSecretProps, + TWorkspaceSecretSnapshot +} from './types'; + +export const secretSnapshotKeys = { + list: (workspaceId: string) => [{ workspaceId }, 'secret-snapshot'] as const, + snapshotSecrets: (snapshotId: string) => [{ snapshotId }, 'secret-snapshot'] as const, + count: (workspaceId: string) => [{ workspaceId }, 'count', 'secret-snapshot'] +}; + +const fetchWorkspaceSecretSnaphots = async (workspaceId: string, limit = 10, offset = 0) => { + const res = await apiRequest.get<{ secretSnapshots: TWorkspaceSecretSnapshot[] }>( + `/api/v1/workspace/${workspaceId}/secret-snapshots`, + { + params: { + limit, + offset + } + } + ); + + return res.data.secretSnapshots; +}; + +export const useGetWorkspaceSecretSnapshots = (dto: GetWorkspaceSecretSnapshotsDTO) => + useInfiniteQuery({ + enabled: Boolean(dto.workspaceId), + queryKey: secretSnapshotKeys.list(dto.workspaceId), + queryFn: ({ pageParam }) => fetchWorkspaceSecretSnaphots(dto.workspaceId, dto.limit, pageParam), + getNextPageParam: (lastPage, pages) => + lastPage.length !== 0 ? pages.length * dto.limit : undefined + }); + +const fetchSnapshotEncSecrets = async (snapshotId: string) => { + const res = await apiRequest.get<{ secretSnapshot: TSnapshotSecret }>( + `/api/v1/secret-snapshot/${snapshotId}` + ); + return res.data.secretSnapshot; +}; + +export const useGetSnapshotSecrets = ({ decryptFileKey, env, snapshotId }: TSnapshotSecretProps) => + useQuery({ + queryKey: secretSnapshotKeys.snapshotSecrets(snapshotId), + enabled: Boolean(snapshotId && decryptFileKey), + queryFn: () => fetchSnapshotEncSecrets(snapshotId), + select: (data) => { + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; + const latestKey = decryptFileKey; + const key = decryptAssymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + const sharedSecrets: DecryptedSecret[] = []; + const personalSecrets: Record = {}; + data.secretVersions + .filter(({ environment }) => environment === env) + .forEach((encSecret) => { + const secretKey = decryptSymmetric({ + ciphertext: encSecret.secretKeyCiphertext, + iv: encSecret.secretKeyIV, + tag: encSecret.secretKeyTag, + key + }); + + const secretValue = decryptSymmetric({ + ciphertext: encSecret.secretValueCiphertext, + iv: encSecret.secretValueIV, + tag: encSecret.secretValueTag, + key + }); + + const secretComment = ''; + + const decryptedSecret = { + _id: encSecret.secret, + env: encSecret.environment, + key: secretKey, + value: secretValue, + tags: encSecret.tags, + comment: secretComment, + createdAt: encSecret.createdAt, + updatedAt: encSecret.updatedAt, + type: 'modified' + }; + + if (encSecret.type === 'personal') { + personalSecrets[decryptedSecret.key] = { id: encSecret.secret, value: secretValue }; + } else { + sharedSecrets.push(decryptedSecret); + } + }); + + sharedSecrets.forEach((val) => { + if (personalSecrets?.[val.key]) { + val.idOverride = personalSecrets[val.key].id; + val.valueOverride = personalSecrets[val.key].value; + val.overrideAction = 'modified'; + } + }); + + return { version: data.version, secrets: sharedSecrets, createdAt: data.createdAt }; + } + }); + +const fetchWorkspaceSecretSnaphotCount = async (workspaceId: string) => { + const res = await apiRequest.get<{ count: number }>( + `/api/v1/workspace/${workspaceId}/secret-snapshots/count` + ); + return res.data.count; +}; + +export const useGetWsSnapshotCount = (workspaceId: string) => + useQuery({ + enabled: Boolean(workspaceId), + queryKey: secretSnapshotKeys.count(workspaceId), + queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId) + }); + +export const usePerformSecretRollback = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TSecretRollbackDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post( + `/api/v1/workspace/${dto.workspaceId}/secret-snapshots/rollback`, + { + version: dto.version + } + ); + return data; + }, + onSuccess: (_, dto) => { + queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId)); + queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId)); + } + }); +}; diff --git a/frontend/src/hooks/api/secretSnapshots/types.ts b/frontend/src/hooks/api/secretSnapshots/types.ts new file mode 100644 index 000000000..dbe8ccd17 --- /dev/null +++ b/frontend/src/hooks/api/secretSnapshots/types.ts @@ -0,0 +1,32 @@ +import { UserWsKeyPair } from '../keys/types'; +import { EncryptedSecretVersion } from '../secrets/types'; + +export type TWorkspaceSecretSnapshot = { + _id: string; + workspace: string; + version: number; + secretVersions: string[]; + createdAt: string; + updatedAt: string; + __v: number; +}; + +export type TSnapshotSecret = Omit & { + secretVersions: EncryptedSecretVersion[]; +}; + +export type TSnapshotSecretProps = { + snapshotId: string; + env: string; + decryptFileKey: UserWsKeyPair; +}; + +export type GetWorkspaceSecretSnapshotsDTO = { + workspaceId: string; + limit: number; +}; + +export type TSecretRollbackDTO = { + workspaceId: string; + version: number; +}; diff --git a/frontend/src/hooks/api/secrets/index.ts b/frontend/src/hooks/api/secrets/index.ts new file mode 100644 index 000000000..c27cecfb1 --- /dev/null +++ b/frontend/src/hooks/api/secrets/index.ts @@ -0,0 +1 @@ +export { useBatchSecretsOp, useGetProjectSecrets, useGetSecretVersion } from './queries'; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx new file mode 100644 index 000000000..9b9c49df5 --- /dev/null +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -0,0 +1,173 @@ +/* eslint-disable no-param-reassign */ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { + decryptAssymmetric, + decryptSymmetric +} from '@app/components/utilities/cryptography/crypto'; +import { apiRequest } from '@app/config/request'; + +import { secretSnapshotKeys } from '../secretSnapshots/queries'; +import { + BatchSecretDTO, + DecryptedSecret, + EncryptedSecret, + EncryptedSecretVersion, + GetProjectSecretsDTO, + GetSecretVersionsDTO +} from './types'; + +export const secretKeys = { + // this is also used in secretSnapshot part + getProjectSecret: (workspaceId: string, env: string) => [{ workspaceId, env }, 'secrets'], + getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions'] +}; + +const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string) => { + const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', { + params: { + environment: env, + workspaceId + } + }); + return data.secrets; +}; + +export const useGetProjectSecrets = ({ + workspaceId, + env, + decryptFileKey, + isPaused +}: GetProjectSecretsDTO) => + useQuery({ + // wait for all values to be available + enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, + queryKey: secretKeys.getProjectSecret(workspaceId, env), + queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env), + select: (data) => { + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; + const latestKey = decryptFileKey; + const key = decryptAssymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + const sharedSecrets: DecryptedSecret[] = []; + const personalSecrets: Record = {}; + // this used for add-only mode in dashboard + // type won't be there thus only one key is shown + const duplicateSecretKey: Record = {}; + data.forEach((encSecret) => { + const secretKey = decryptSymmetric({ + ciphertext: encSecret.secretKeyCiphertext, + iv: encSecret.secretKeyIV, + tag: encSecret.secretKeyTag, + key + }); + + const secretValue = decryptSymmetric({ + ciphertext: encSecret.secretValueCiphertext, + iv: encSecret.secretValueIV, + tag: encSecret.secretValueTag, + key + }); + + const secretComment = decryptSymmetric({ + ciphertext: encSecret.secretCommentCiphertext, + iv: encSecret.secretCommentIV, + tag: encSecret.secretCommentTag, + key + }); + + const decryptedSecret = { + _id: encSecret._id, + env: encSecret.environment, + key: secretKey, + value: secretValue, + tags: encSecret.tags, + comment: secretComment, + createdAt: encSecret.createdAt, + updatedAt: encSecret.updatedAt + }; + + if (encSecret.type === 'personal') { + personalSecrets[decryptedSecret.key] = { id: encSecret._id, value: secretValue }; + } else { + if (!duplicateSecretKey?.[decryptedSecret.key]) { + sharedSecrets.push(decryptedSecret); + } + duplicateSecretKey[decryptedSecret.key] = true; + } + }); + sharedSecrets.forEach((val) => { + if (personalSecrets?.[val.key]) { + val.idOverride = personalSecrets[val.key].id; + val.valueOverride = personalSecrets[val.key].value; + val.overrideAction = 'modified'; + } + }); + + return { secrets: sharedSecrets }; + } + }); + +const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => { + const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>( + `/api/v1/secret/${secretId}/secret-versions`, + { + params: { + limit, + offset + } + } + ); + return data.secretVersions; +}; + +export const useGetSecretVersion = (dto: GetSecretVersionsDTO) => + useQuery({ + enabled: Boolean(dto.secretId && dto.decryptFileKey), + queryKey: secretKeys.getSecretVersion(dto.secretId), + queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit), + select: (data) => { + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; + const latestKey = dto.decryptFileKey; + const key = decryptAssymmetric({ + ciphertext: latestKey.encryptedKey, + nonce: latestKey.nonce, + publicKey: latestKey.sender.publicKey, + privateKey: PRIVATE_KEY + }); + + return data + .map((el) => ({ + createdAt: el.createdAt, + id: el._id, + value: decryptSymmetric({ + ciphertext: el.secretValueCiphertext, + iv: el.secretValueIV, + tag: el.secretValueTag, + key + }) + })) + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)); + } + }); + +export const useBatchSecretsOp = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, BatchSecretDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post('/api/v2/secrets/batch', dto); + return data; + }, + onSuccess: (_, dto) => { + queryClient.invalidateQueries(secretKeys.getProjectSecret(dto.workspaceId, dto.environment)); + queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId)); + queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId)); + } + }); +}; diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts new file mode 100644 index 000000000..7888e64b3 --- /dev/null +++ b/frontend/src/hooks/api/secrets/types.ts @@ -0,0 +1,104 @@ +import type { UserWsKeyPair } from '../keys/types'; +import type { WsTag } from '../tags/types'; + +export type EncryptedSecret = { + _id: string; + version: number; + workspace: string; + type: 'shared' | 'personal'; + environment: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + __v: number; + createdAt: string; + updatedAt: string; + secretCommentCiphertext: string; + secretCommentIV: string; + secretCommentTag: string; + tags: WsTag[]; +}; + +export type DecryptedSecret = { + _id: string; + key: string; + value: string; + comment: string; + tags: WsTag[]; + createdAt: string; + updatedAt: string; + env: string; + valueOverride?: string; + idOverride?: string; + overrideAction?: string; +}; + +export type EncryptedSecretVersion = { + _id: string; + secret: string; + version: number; + workspace: string; + type: string; + environment: string; + isDeleted: boolean; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + tags: WsTag[]; + __v: number; + createdAt: string; + updatedAt: string; +}; + +// dto +type SecretTagArg = { _id: string; name: string; slug: string }; + +export type UpdateSecretArg = { + _id: string; + type: 'shared' | 'personal'; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretCommentCiphertext: string; + secretCommentIV: string; + secretCommentTag: string; + tags: SecretTagArg[]; +}; + +export type CreateSecretArg = Omit; + +export type DeleteSecretArg = { _id: string }; + +export type BatchSecretDTO = { + workspaceId: string; + environment: string; + requests: Array< + | { method: 'POST'; secret: CreateSecretArg } + | { method: 'PATCH'; secret: UpdateSecretArg } + | { method: 'DELETE'; secret: DeleteSecretArg } + >; +}; + +export type GetProjectSecretsDTO = { + workspaceId: string; + env: string; + decryptFileKey: UserWsKeyPair; + isPaused?: boolean; + onSuccess?: (data: DecryptedSecret[]) => void; +}; + +export type GetSecretVersionsDTO = { + secretId: string; + limit: number; + offset: number; + decryptFileKey: UserWsKeyPair; +}; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index a8821e5a5..554521828 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -4,6 +4,7 @@ export type { UserWsKeyPair } from './keys/types'; export type { Organization } from './organization/types'; export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types'; export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types'; +export type { WsTag } from './tags/types'; export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './users/types'; export type { CreateEnvironmentDTO, diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx index 0980d4108..d6cfcab2b 100644 --- a/frontend/src/hooks/api/users/index.tsx +++ b/frontend/src/hooks/api/users/index.tsx @@ -5,6 +5,8 @@ export { useDeleteOrgMembership, useGetOrgUsers, useGetUser, + useGetUserAction, useLogoutUser, + useRegisterUserAction, useUpdateOrgUserRole } from './queries'; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 5d0759755..ef6dcc726 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -20,6 +20,7 @@ import { const userKeys = { getUser: ['user'] as const, + userAction: ['user-action'] as const, getOrgUsers: (orgId: string) => [{ orgId }, 'user'] }; @@ -31,6 +32,21 @@ const fetchUserDetails = async () => { export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails); +const fetchUserAction = async (action: string) => { + const { data } = await apiRequest.get<{ userAction: string }>('/api/v1/user-action', { + params: { + action + } + }); + return data.userAction; +}; + +export const useGetUserAction = (action: string) => + useQuery({ + queryKey: userKeys.userAction, + queryFn: () => fetchUserAction(action) + }); + export const fetchOrgUsers = async (orgId: string) => { const { data } = await apiRequest.get<{ users: OrgUser[] }>( `/api/v1/organization/${orgId}/users` @@ -128,6 +144,16 @@ export const useUpdateOrgUserRole = () => { }); }; +export const useRegisterUserAction = () => { + const queryClient = useQueryClient(); + return useMutation<{}, {}, string>({ + mutationFn: (action) => apiRequest.post('/api/v1/user-action', { action }), + onSuccess: () => { + queryClient.invalidateQueries(userKeys.userAction); + } + }); +}; + export const useLogoutUser = () => useMutation({ mutationFn: () => apiRequest.post('/api/v1/auth/logout'), diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index cab8617bd..622c5196c 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -5,7 +5,9 @@ export { useDeleteWsEnvironment, useGetUserWorkspaceMemberships, useGetUserWorkspaces, + useGetUserWsEnvironments, useGetWorkspaceById, useRenameWorkspace, useToggleAutoCapitalization, - useUpdateWsEnvironment} from './queries'; + useUpdateWsEnvironment +} from './queries'; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 2bca2e9a5..4736da076 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -7,16 +7,19 @@ import { CreateWorkspaceDTO, DeleteEnvironmentDTO, DeleteWorkspaceDTO, + GetWsEnvironmentDTO, RenameWorkspaceDTO, ToggleAutoCapitalizationDTO, UpdateEnvironmentDTO, - Workspace + Workspace, + WorkspaceEnv } from './types'; const workspaceKeys = { getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const, getWorkspaceMemberships: (orgId: string) => [{ orgId }, 'workspace-memberships'], - getAllUserWorkspace: ['workspaces'] as const + getAllUserWorkspace: ['workspaces'] as const, + getUserWsEnvironments: (workspaceId: string) => ['workspace-env', { workspaceId }] as const }; const fetchWorkspaceById = async (workspaceId: string) => { @@ -49,6 +52,21 @@ const fetchUserWorkspaceMemberships = async (orgId: string) => { return data; }; +const fetchUserWsEnvironments = async (workspaceId: string) => { + const { data } = await apiRequest.get<{ accessibleEnvironments: WorkspaceEnv[] }>( + `/api/v2/workspace/${workspaceId}/environments` + ); + return data.accessibleEnvironments; +}; + +export const useGetUserWsEnvironments = ({ workspaceId, onSuccess }: GetWsEnvironmentDTO) => + useQuery({ + enabled: Boolean(workspaceId), + onSuccess, + queryKey: workspaceKeys.getUserWsEnvironments(workspaceId), + queryFn: () => fetchUserWsEnvironments(workspaceId) + }); + // to get all userids in an org with the workspace they are part of export const useGetUserWorkspaceMemberships = (orgId: string) => useQuery({ diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 738f5188c..44d4c4322 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -7,7 +7,13 @@ export type Workspace = { environments: WorkspaceEnv[]; }; -export type WorkspaceEnv = { name: string; slug: string }; +export type WorkspaceEnv = { + name: string; + slug: string; + isReadDenied: boolean; + isWriteDenied: boolean; +}; + export type WorkspaceTag = { _id: string; name: string; slug: string }; // mutation dto @@ -16,6 +22,11 @@ export type CreateWorkspaceDTO = { organizationId: string; }; +export type GetWsEnvironmentDTO = { + workspaceId: string; + onSuccess?: (data: WorkspaceEnv[]) => void; +}; + export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string }; export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean }; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index 94bb7abc0..2c2058ef7 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -1,3 +1,4 @@ export { useLeaveConfirm } from './useLeaveConfirm'; +export { usePersistentState } from './usePersistentState'; export { usePopUp } from './usePopUp'; export { useToggle } from './useToggle'; diff --git a/frontend/src/hooks/useLeaveConfirm.tsx b/frontend/src/hooks/useLeaveConfirm.tsx index e136f41e4..3cbba8e19 100644 --- a/frontend/src/hooks/useLeaveConfirm.tsx +++ b/frontend/src/hooks/useLeaveConfirm.tsx @@ -4,52 +4,56 @@ import { useRouter } from 'next/router'; import { leaveConfirmDefaultMessage } from '@app/const'; type LeaveConfirmProps = { - initialValue: boolean, - message?: string -} + initialValue: boolean; + message?: string; +}; interface LeaveConfirmReturn { - hasUnsavedChanges: boolean, - setHasUnsavedChanges: Dispatch>, + hasUnsavedChanges: boolean; + setHasUnsavedChanges: Dispatch>; } export function useLeaveConfirm({ - initialValue, - message = leaveConfirmDefaultMessage, + initialValue, + message = leaveConfirmDefaultMessage }: LeaveConfirmProps): LeaveConfirmReturn { - const router = useRouter() + const router = useRouter(); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(initialValue); const onRouteChangeStart = useCallback(() => { if (hasUnsavedChanges) { + // eslint-disable-next-line no-alert if (window.confirm(message)) { - return true + return true; } - throw new Error("Abort route change by user's confirmation.") + throw new Error("Abort route change by user's confirmation."); } return false; - }, [hasUnsavedChanges]) + }, [hasUnsavedChanges]); - const handleWindowClose = useCallback((e: any) => { - if (!hasUnsavedChanges) { - return; - } - e.preventDefault(); - e.returnValue = message; - }, []); + const handleWindowClose = useCallback( + (e: any) => { + if (!hasUnsavedChanges) { + return; + } + e.preventDefault(); + e.returnValue = message; + }, + [hasUnsavedChanges] + ); useEffect(() => { - router.events.on("routeChangeStart", onRouteChangeStart); + router.events.on('routeChangeStart', onRouteChangeStart); window.addEventListener('beforeunload', handleWindowClose); return () => { - router.events.off("routeChangeStart", onRouteChangeStart); + router.events.off('routeChangeStart', onRouteChangeStart); window.removeEventListener('beforeunload', handleWindowClose); - } + }; }, [onRouteChangeStart, handleWindowClose]); return { hasUnsavedChanges, - setHasUnsavedChanges, + setHasUnsavedChanges }; } diff --git a/frontend/src/hooks/usePersistentState.ts b/frontend/src/hooks/usePersistentState.ts new file mode 100644 index 000000000..0230e35ef --- /dev/null +++ b/frontend/src/hooks/usePersistentState.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; + +type TPersisntentStateReturn = [T, (val: T) => void]; + +export const usePersistentState = ( + initialValue: T, + persistenceKey: string +): TPersisntentStateReturn => { + const [val, setVal] = useState(initialValue); + + useEffect(() => { + const temp = localStorage.getItem(persistenceKey); + if (temp) { + const { key } = JSON.parse(temp); + setVal(key); + } + }, []); + + const setState = (state: T) => { + localStorage.setItem(persistenceKey, JSON.stringify({ key: state })); + setVal(state); + }; + + return [val, setState]; +};