misc: migrated to react-state

This commit is contained in:
Sheen Capadngan
2024-05-08 01:03:41 +08:00
parent c08fcc6f5e
commit f957b9d970
4 changed files with 74 additions and 99 deletions

View File

@@ -2,7 +2,6 @@ import { useTranslation } from "react-i18next";
import Head from "next/head";
import { SecretOverviewPage } from "@app/views/SecretOverviewPage";
import { StoreProvider } from "@app/views/SecretOverviewPage/SecretOverviewPage.store";
const Dashboard = () => {
const { t } = useTranslation();
@@ -17,9 +16,7 @@ const Dashboard = () => {
<meta name="og:description" content={String(t("dashboard.og-description"))} />
</Head>
<div className="h-full">
<StoreProvider>
<SecretOverviewPage />
</StoreProvider>
<SecretOverviewPage />
</div>
</>
);

View File

@@ -1,78 +0,0 @@
import { createContext, ReactNode, useContext, useEffect, useRef } from "react";
import { useRouter } from "next/router";
import { createStore, StateCreator, StoreApi, useStore } from "zustand";
export enum EntryType {
FOLDER = "folder",
SECRET = "secret"
}
type SelectedEntriesState = {
selectedEntries: {
[EntryType.FOLDER]: Record<string, boolean>;
[EntryType.SECRET]: Record<string, boolean>;
};
action: {
toggle: (type: EntryType, key: string) => void;
reset: () => void;
};
};
const createSelectedSecretStore: StateCreator<SelectedEntriesState> = (set) => ({
selectedEntries: {
[EntryType.FOLDER]: {},
[EntryType.SECRET]: {}
},
action: {
toggle: (type: EntryType, key: string) =>
set((state) => {
const isChecked = Boolean(state.selectedEntries[type]?.[key]);
const newChecks = { ...state.selectedEntries };
// remove selection if its present else add it
if (isChecked) delete newChecks[type][key];
else newChecks[type][key] = true;
return { selectedEntries: newChecks };
}),
reset: () =>
set({
selectedEntries: {
[EntryType.FOLDER]: {},
[EntryType.SECRET]: {}
}
})
}
});
const StoreContext = createContext<StoreApi<SelectedEntriesState> | null>(null);
export const StoreProvider = ({ children }: { children: ReactNode }) => {
const storeRef = useRef<StoreApi<SelectedEntriesState>>();
const router = useRouter();
if (!storeRef.current) {
storeRef.current = createStore<SelectedEntriesState>((...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 <StoreContext.Provider value={storeRef.current}>{children}</StoreContext.Provider>;
};
const useStoreContext = <T extends unknown>(selector: (state: SelectedEntriesState) => T): T => {
const ctx = useContext(StoreContext);
if (!ctx) throw new Error("Missing context provider");
return useStore(ctx, selector);
};
export const useSelectedEntries = () => useStoreContext((state) => state.selectedEntries);
export const useSelectedEntryActions = () => useStoreContext((state) => state.action);

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -71,7 +71,11 @@ import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynam
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
import { SelectionPanel } from "./components/SelectionPanel/SelectionPanel";
import { EntryType, useSelectedEntries, useSelectedEntryActions } from "./SecretOverviewPage.store";
export enum EntryType {
FOLDER = "folder",
SECRET = "secret"
}
export const SecretOverviewPage = () => {
const { t } = useTranslation();
@@ -84,15 +88,6 @@ export const SecretOverviewPage = () => {
const [expandableTableWidth, setExpandableTableWidth] = useState(0);
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
useEffect(() => {
const handleParentTableWidthResize = () => {
setExpandableTableWidth(parentTableRef.current?.clientWidth || 0);
};
window.addEventListener("resize", handleParentTableWidthResize);
return () => window.removeEventListener("resize", handleParentTableWidthResize);
}, []);
useEffect(() => {
if (parentTableRef.current) {
setExpandableTableWidth(parentTableRef.current.clientWidth);
@@ -107,8 +102,55 @@ export const SecretOverviewPage = () => {
const [searchFilter, setSearchFilter] = useState("");
const secretPath = (router.query?.secretPath as string) || "/";
const selectedEntries = useSelectedEntries();
const { toggle: toggleSelectedEntry } = useSelectedEntryActions();
const [selectedEntries, setSelectedEntries] = useState<{
[EntryType.FOLDER]: Record<string, boolean>;
[EntryType.SECRET]: Record<string, boolean>;
}>({
[EntryType.FOLDER]: {},
[EntryType.SECRET]: {}
});
const toggleSelectedEntry = useCallback(
(type: EntryType, key: string) => {
const isChecked = Boolean(selectedEntries[type]?.[key]);
const newChecks = { ...selectedEntries };
// remove selection if its present else add it
if (isChecked) {
delete newChecks[type][key];
} else {
newChecks[type][key] = true;
}
setSelectedEntries(newChecks);
},
[selectedEntries]
);
const resetSelectedEntries = useCallback(() => {
setSelectedEntries({
[EntryType.FOLDER]: {},
[EntryType.SECRET]: {}
});
}, []);
useEffect(() => {
const handleParentTableWidthResize = () => {
setExpandableTableWidth(parentTableRef.current?.clientWidth || 0);
};
const onRouteChangeStart = () => {
resetSelectedEntries();
};
router.events.on("routeChangeStart", onRouteChangeStart);
window.addEventListener("resize", handleParentTableWidthResize);
return () => {
window.removeEventListener("resize", handleParentTableWidthResize);
router.events.off("routeChangeStart", onRouteChangeStart);
};
}, []);
useEffect(() => {
if (!isWorkspaceLoading && !workspaceId && router.isReady) {
@@ -553,6 +595,8 @@ export const SecretOverviewPage = () => {
secretPath={secretPath}
getSecretByKey={getSecretByKey}
getFolderByNameAndEnv={getFolderByNameAndEnv}
selectedEntries={selectedEntries}
resetSelectedEntries={resetSelectedEntries}
/>
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
<TableContainer className="max-h-[calc(100vh-250px)] overflow-y-auto">

View File

@@ -15,23 +15,35 @@ import { usePopUp } from "@app/hooks";
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
import { DecryptedSecret, TDeleteSecretBatchDTO, TSecretFolder } from "@app/hooks/api/types";
import { useSelectedEntries, useSelectedEntryActions } from "../../SecretOverviewPage.store";
export enum EntryType {
FOLDER = "folder",
SECRET = "secret"
}
type Props = {
secretPath: string;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
getFolderByNameAndEnv: (name: string, env: string) => TSecretFolder | undefined;
resetSelectedEntries: () => void;
selectedEntries: {
[EntryType.FOLDER]: Record<string, boolean>;
[EntryType.SECRET]: Record<string, boolean>;
};
};
export const SelectionPanel = ({ getFolderByNameAndEnv, getSecretByKey, secretPath }: Props) => {
export const SelectionPanel = ({
getFolderByNameAndEnv,
getSecretByKey,
secretPath,
resetSelectedEntries,
selectedEntries
}: Props) => {
const { permission } = useProjectPermission();
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"bulkDeleteEntries"
] as const);
const selectedEntries = useSelectedEntries();
const { reset: resetSelectedEntries } = useSelectedEntryActions();
const selectedCount =
Object.keys(selectedEntries.folder).length + Object.keys(selectedEntries.secret).length;