diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 4182be9dc..c1125dad8 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -1297,7 +1297,7 @@ export const createSecretBatchHelper = async ({ secrets.map(({ secretName, type }) => ({ secretBlindIndex: secretBlindIndexes[secretName], type: type, - ...getAuthDataPayloadUserObj(authData) + ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}) })) ) .exec(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f34d3fbab..689950a01 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -93,7 +93,8 @@ "uuidv4": "^6.2.13", "yaml": "^2.2.2", "yup": "^0.32.11", - "zod": "^3.22.0" + "zod": "^3.22.0", + "zustand": "^4.4.1" }, "devDependencies": { "@storybook/addon-essentials": "^7.0.23", @@ -23653,6 +23654,33 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz", + "integrity": "sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } }, "dependencies": { @@ -40792,6 +40820,14 @@ "version": "3.22.0", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz", "integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q==" + }, + "zustand": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz", + "integrity": "sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==", + "requires": { + "use-sync-external-store": "1.2.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 0ff0c89eb..f71e92302 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -101,7 +101,8 @@ "uuidv4": "^6.2.13", "yaml": "^2.2.2", "yup": "^0.32.11", - "zod": "^3.22.0" + "zod": "^3.22.0", + "zustand": "^4.4.1" }, "devDependencies": { "@storybook/addon-essentials": "^7.0.23", diff --git a/frontend/src/components/v2/ContentLoader/ContentLoader.tsx b/frontend/src/components/v2/ContentLoader/ContentLoader.tsx index 09dbd4db9..343d8adeb 100644 --- a/frontend/src/components/v2/ContentLoader/ContentLoader.tsx +++ b/frontend/src/components/v2/ContentLoader/ContentLoader.tsx @@ -28,9 +28,9 @@ export const ContentLoader = ({ text, frequency = 2000 }: Props) => { loading animation {text && isTextArray && ( - + ; + action: { + toggle: (id: string) => void; + reset: () => void; + }; +}; +const createSelectedSecretStore: StateCreator = (set) => ({ + selectedSecret: {}, + action: { + toggle: (id) => + set((state) => { + const isChecked = Boolean(state.selectedSecret?.[id]); + const newChecks = { ...state.selectedSecret }; + // remove selection if its present else add it + if (isChecked) delete newChecks[id]; + else newChecks[id] = true; + return { selectedSecret: newChecks }; + }), + reset: () => set({ selectedSecret: {} }) + } +}); + +const StoreContext = createContext | null>(null); +export const StoreProvider = ({ children }: { children: ReactNode }) => { + const storeRef = useRef>(); + const router = useRouter(); + if (!storeRef.current) { + storeRef.current = createStore((...a) => ({ + ...createSelectedSecretStore(...a) + })); + } + + useEffect(() => { + const onRouteChangeStart = () => { + const state = storeRef.current?.getState(); + state?.action.reset(); + }; + + router.events.on("routeChangeStart", onRouteChangeStart); + return () => { + router.events.off("routeChangeStart", onRouteChangeStart); + }; + }, []); + + return {children}; +}; + +const useStoreContext = (selector: (state: SelectedSecretState) => T): T => { + const ctx = useContext(StoreContext); + if (!ctx) throw new Error("Missing "); + return useStore(ctx, selector); +}; + +// selected secret context +export const useSelectedSecrets = () => useStoreContext((state) => state.selectedSecret); +export const useSelectedSecretActions = () => useStoreContext((state) => state.action); diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index 3bed3a945..e41543990 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/router"; import { subject } from "@casl/ability"; @@ -34,10 +34,11 @@ import { SecretDropzone } from "./components/SecretDropzone"; import { SecretImportListView } from "./components/SecretImportListView"; import { SecretListView } from "./components/SecretListView"; import { SnapshotView } from "./components/SnapshotView"; +import { StoreProvider } from "./SecretMainPage.store"; import { Filter, GroupBy, SortDir } from "./SecretMainPage.types"; const LOADER_TEXT = [ - "Retriving and decrypting your E2EE encrypted secrets", + "Retriving your encrypted secrets", "Fetching folders", "Getting secret import links" ]; @@ -54,7 +55,6 @@ export const SecretMainPage = () => { tags: {}, searchFilter: "" }); - const [selectedSecret, setSelectedSecret] = useState>({}); const [snapshotId, setSnapshotId] = useState(null); const isRollbackMode = Boolean(snapshotId); @@ -73,17 +73,6 @@ export const SecretMainPage = () => { ProjectPermissionSub.SecretRollback ); - useEffect(() => { - const onRouteChangeStart = () => { - setSelectedSecret({}); - }; - - router.events.on("routeChangeStart", onRouteChangeStart); - return () => { - router.events.off("routeChangeStart", onRouteChangeStart); - }; - }, []); - const { data: decryptFileKey } = useGetUserWsKey(workspaceId); // fetch secrets @@ -187,20 +176,6 @@ export const SecretMainPage = () => { const handleToggleVisibility = useCallback(() => setIsVisible((state) => !state), []); - const handleToggleSecretSelect = useCallback( - (id: string) => - setSelectedSecret((state) => { - const isChecked = Boolean(state?.[id]); - const newChecks = { ...state }; - if (isChecked) delete newChecks[id]; - else newChecks[id] = true; - return newChecks; - }), - [] - ); - - const handleResetSecretSelect = useCallback(() => setSelectedSecret({}), []); - // snapshot functions const handleSelectSnapshot = useCallback((snapId: string) => { setSnapshotId(snapId); @@ -221,138 +196,137 @@ export const SecretMainPage = () => { } return ( -
-
- +
+
+ +
+ {!isRollbackMode ? ( + <> + handlePopUpToggle("snapshots", true)} + /> +
+
+ {isNotEmtpy && ( +
+
+
{ + if (evt.key === "Enter") handleSortToggle(); + }} + > + Key + +
+
Value
+
+ )} + {canReadSecret && ( + + )} + + {canReadSecret && ( + + )} + {!canReadSecret && folders?.length === 0 && } +
+
+ + + ) : ( + handlePopUpToggle("snapshots", true)} + /> + )} + handlePopUpToggle("snapshots", isOpen)} + hasNextPage={hasNextSnapshotListPage} + fetchNextPage={fetchNextSnapshotList} + onSelectSnapshot={handleSelectSnapshot} + isFetchingNextPage={isFetchingNextSnapshotList} />
- {!isRollbackMode ? ( - <> - handlePopUpToggle("snapshots", true)} - /> -
-
- {isNotEmtpy && ( -
-
-
{ - if (evt.key === "Enter") handleSortToggle(); - }} - > - Key - -
-
Value
-
- )} - {canReadSecret && ( - - )} - - {canReadSecret && ( - - )} - {!canReadSecret && folders?.length === 0 && } -
-
- - - ) : ( - handlePopUpToggle("snapshots", true)} - /> - )} - handlePopUpToggle("snapshots", isOpen)} - hasNextPage={hasNextSnapshotListPage} - fetchNextPage={fetchNextSnapshotList} - onSelectSnapshot={handleSelectSnapshot} - isFetchingNextPage={isFetchingNextSnapshotList} - /> -
+ ); }; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx index 7e5a2bdcb..5240c242f 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx @@ -45,6 +45,7 @@ import { usePopUp } from "@app/hooks"; import { useCreateFolder, useDeleteSecretBatch } from "@app/hooks/api"; import { DecryptedSecret, TImportedSecrets, UserWsKeyPair, WsTag } from "@app/hooks/api/types"; +import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store"; import { Filter, GroupBy } from "../../SecretMainPage.types"; import { CreateSecretForm } from "./CreateSecretForm"; import { CreateSecretImportForm } from "./CreateSecretImportForm"; @@ -61,14 +62,13 @@ type Props = { filter: Filter; tags?: WsTag[]; isVisible?: boolean; - selectedSecrets: Record; snapshotCount: number; isSnapshotCountLoading?: boolean; + autoCapitalization?: boolean; onGroupByChange: (opt?: GroupBy) => void; onSearchChange: (term: string) => void; onToggleTagFilter: (tagId: string) => void; onVisiblilityToggle: () => void; - onResetSelectedSecret: () => void; onClickRollbackMode: () => void; }; @@ -82,14 +82,13 @@ export const ActionBar = ({ filter, tags = [], isVisible, - selectedSecrets, snapshotCount, isSnapshotCountLoading, + autoCapitalization, onSearchChange, onToggleTagFilter, onGroupByChange, onVisiblilityToggle, - onResetSelectedSecret, onClickRollbackMode }: Props) => { const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ @@ -106,6 +105,8 @@ export const ActionBar = ({ const { mutateAsync: createFolder } = useCreateFolder(); const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch(); + const selectedSecrets = useSelectedSecrets(); + const { reset: resetSelectedSecret } = useSelectedSecretActions(); const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length); const handleFolderCreate = async (folderName: string) => { @@ -170,7 +171,7 @@ export const ActionBar = ({ environment, secrets: bulkDeletedSecrets.map(({ key }) => ({ secretName: key, type: "shared" })) }); - onResetSelectedSecret(); + resetSelectedSecret(); handlePopUpClose("bulkDeleteSecrets"); createNotification({ type: "success", @@ -376,7 +377,7 @@ export const ActionBar = ({ >
- + @@ -410,6 +411,7 @@ export const ActionBar = ({ secrets={secrets} environment={environment} workspaceId={workspaceId} + autoCapitalize={autoCapitalization} secretPath={secretPath} decryptFileKey={decryptFileKey!} isOpen={popUp.addSecret.isOpen} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateSecretForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateSecretForm.tsx index 1c7a179d9..aab4ba50c 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateSecretForm.tsx @@ -9,7 +9,7 @@ import { DecryptedSecret, UserWsKeyPair } from "@app/hooks/api/types"; const typeSchema = z.object({ key: z.string(), - value: z.string() + value: z.string().optional() }); type TFormSchema = z.infer; @@ -24,6 +24,7 @@ type Props = { isOpen?: boolean; onClose: () => void; onTogglePopUp: (isOpen: boolean) => void; + autoCapitalize?: boolean; }; export const CreateSecretForm = ({ @@ -33,7 +34,8 @@ export const CreateSecretForm = ({ secretPath = "/", isOpen, onClose, - onTogglePopUp + onTogglePopUp, + autoCapitalize = true }: Props) => { const { register, @@ -54,7 +56,7 @@ export const CreateSecretForm = ({ workspaceId, secretPath, secretName: key, - secretValue: value, + secretValue: value || "", secretComment: "", type: "shared", latestFileKey: decryptFileKey @@ -82,7 +84,11 @@ export const CreateSecretForm = ({ >
- + - ( - - - + + {(isAllowed) => ( + ( + + + + )} + /> )} - /> -
+ +
)} -
- ( - - {(isAllowed) => ( - - Disable multi line encoding - - - - - )} - - )} - /> -
{fields.map(({ tagColor, id: formId, name, _id }) => ( @@ -341,6 +320,34 @@ export const SecretDetailSidebar = ({ rows={5} /> +
+ ( + + {(isAllowed) => ( + + Disable multi line encoding + + + + + )} + + )} + /> +
Version History
diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx index 722033e96..98a18881e 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx @@ -13,6 +13,7 @@ import { UserWsKeyPair, WsTag } from "@app/hooks/api/types"; import { secretSnapshotKeys } from "~/hooks/api/secretSnapshots/queries"; +import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store"; import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types"; import { SecretDetailSidebar } from "./SecretDetaiSidebar"; import { SecretItem } from "./SecretItem"; @@ -27,8 +28,6 @@ type Props = { sortDir?: SortDir; tags?: WsTag[]; isVisible?: boolean; - selectedSecrets: Record; - onToggleSecretSelect: (id: string) => void; }; const reorderSecretGroupByUnderscore = (secrets: DecryptedSecret[], sortDir: SortDir) => { @@ -86,9 +85,7 @@ export const SecretListView = ({ filter, sortDir = SortDir.ASC, tags: wsTags = [], - isVisible, - selectedSecrets, - onToggleSecretSelect + isVisible }: Props) => { const { createNotification } = useNotificationContext(); const queryClient = useQueryClient(); @@ -114,6 +111,8 @@ export const SecretListView = ({ onSuccess: undefined } }); + const selectedSecrets = useSelectedSecrets(); + const { toggle: toggleSelectedSecret } = useSelectedSecretActions(); const handleSecretOperation = async ( operation: "create" | "update" | "delete", @@ -303,7 +302,7 @@ export const SecretListView = ({ secretPath={secretPath} tags={wsTags} isSelected={selectedSecrets?.[secret._id]} - onToggleSecretSelect={onToggleSecretSelect} + onToggleSecretSelect={toggleSelectedSecret} isVisible={isVisible} secret={secret} key={secret._id}