mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 12:28:38 +00:00
751 lines
26 KiB
TypeScript
751 lines
26 KiB
TypeScript
/* eslint-disable no-nested-ternary */
|
|
import { createContext, ReactNode, useContext, useEffect, useRef } from "react";
|
|
import { useRouter } from "@tanstack/react-router";
|
|
import { createStore, StateCreator, StoreApi, useStore } from "zustand";
|
|
import { useShallow } from "zustand/react/shallow";
|
|
|
|
import { createNotification } from "@app/components/notifications";
|
|
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
|
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
|
|
|
|
// akhilmhdh: Don't remove this file if ur thinking why use zustand just for selected selects state
|
|
// This is first step and the whole secret crud will be moved to this global page scope state
|
|
// this will allow more stuff like undo grouping stuffs etc
|
|
|
|
// Base interface for all pending changes
|
|
export interface BasePendingChange {
|
|
id: string;
|
|
timestamp: number;
|
|
}
|
|
|
|
// Secret-related change types
|
|
export interface PendingSecretCreate extends BasePendingChange {
|
|
resourceType: "secret";
|
|
type: PendingAction.Create;
|
|
secretKey: string;
|
|
secretValue: string;
|
|
secretComment?: string;
|
|
skipMultilineEncoding?: boolean;
|
|
tags?: { id: string; slug: string }[];
|
|
secretMetadata?: { key: string; value: string }[];
|
|
originalKey?: string;
|
|
}
|
|
|
|
export interface PendingSecretUpdate extends BasePendingChange {
|
|
resourceType: "secret";
|
|
type: PendingAction.Update;
|
|
secretKey: string;
|
|
newSecretName?: string;
|
|
originalValue?: string;
|
|
secretValue?: string;
|
|
originalComment?: string;
|
|
secretComment?: string;
|
|
originalSkipMultilineEncoding?: boolean;
|
|
skipMultilineEncoding?: boolean;
|
|
originalTags?: { id: string; slug: string }[];
|
|
tags?: { id: string; slug: string }[];
|
|
originalSecretMetadata?: { key: string; value: string }[];
|
|
secretMetadata?: { key: string; value: string }[];
|
|
existingSecret: SecretV3RawSanitized;
|
|
}
|
|
|
|
export interface PendingSecretDelete extends BasePendingChange {
|
|
resourceType: "secret";
|
|
type: PendingAction.Delete;
|
|
secretKey: string;
|
|
secretValue: string;
|
|
secretValueHidden: boolean;
|
|
}
|
|
|
|
// Folder-related change types
|
|
export interface PendingFolderCreate extends BasePendingChange {
|
|
resourceType: "folder";
|
|
type: PendingAction.Create;
|
|
id: string;
|
|
folderName: string;
|
|
description?: string;
|
|
parentPath: string;
|
|
}
|
|
|
|
export interface PendingFolderUpdate extends BasePendingChange {
|
|
resourceType: "folder";
|
|
type: PendingAction.Update;
|
|
originalFolderName: string;
|
|
folderName: string;
|
|
id: string;
|
|
originalDescription?: string;
|
|
description?: string;
|
|
}
|
|
|
|
export interface PendingFolderDelete extends BasePendingChange {
|
|
resourceType: "folder";
|
|
type: PendingAction.Delete;
|
|
id: string;
|
|
folderName: string;
|
|
folderPath: string;
|
|
}
|
|
|
|
// Union types for each resource
|
|
export type PendingSecretChange = PendingSecretCreate | PendingSecretUpdate | PendingSecretDelete;
|
|
export type PendingFolderChange = PendingFolderCreate | PendingFolderUpdate | PendingFolderDelete;
|
|
export type PendingChange = PendingSecretChange | PendingFolderChange;
|
|
|
|
// Grouped changes for better processing
|
|
export interface PendingChanges {
|
|
secrets: PendingSecretChange[];
|
|
folders: PendingFolderChange[];
|
|
}
|
|
|
|
// Context interface for batch operations
|
|
export interface BatchContext {
|
|
projectId: string;
|
|
environment: string;
|
|
secretPath: string;
|
|
}
|
|
|
|
const normalizeValue = (value: any): string | boolean | undefined => {
|
|
if (value === null || value === undefined || value === "") {
|
|
return undefined;
|
|
}
|
|
return value;
|
|
};
|
|
|
|
const areValuesEqual = (value1: any, value2: any): boolean => {
|
|
const normalized1 = normalizeValue(value1);
|
|
const normalized2 = normalizeValue(value2);
|
|
|
|
if (normalized1 === undefined && normalized2 === undefined) {
|
|
return true;
|
|
}
|
|
|
|
return normalized1 === normalized2;
|
|
};
|
|
|
|
const areArraysEqual = (arr1: any[] | undefined, arr2: any[] | undefined): boolean => {
|
|
// Handle undefined/null arrays
|
|
if (!arr1 && !arr2) return true;
|
|
if (!arr1 || !arr2) return false;
|
|
|
|
// Compare lengths
|
|
if (arr1.length !== arr2.length) return false;
|
|
|
|
// Deep comparison using JSON stringify (for simple objects)
|
|
return JSON.stringify(arr1.sort()) === JSON.stringify(arr2.sort());
|
|
};
|
|
|
|
const cleanupRevertedSecretFields = (update: PendingSecretUpdate): PendingSecretUpdate | null => {
|
|
const cleaned = { ...update };
|
|
let hasChanges = false;
|
|
|
|
if (
|
|
cleaned.secretValue !== undefined &&
|
|
!areValuesEqual(cleaned.secretValue, cleaned.originalValue)
|
|
) {
|
|
hasChanges = true;
|
|
} else {
|
|
cleaned.secretValue = undefined;
|
|
}
|
|
|
|
if (
|
|
cleaned.secretComment !== undefined &&
|
|
(!areValuesEqual(cleaned.secretComment, cleaned.originalComment) ||
|
|
!areValuesEqual(cleaned.secretComment, cleaned.existingSecret.comment))
|
|
) {
|
|
hasChanges = true;
|
|
} else {
|
|
cleaned.secretComment = undefined;
|
|
}
|
|
|
|
if (
|
|
cleaned.skipMultilineEncoding !== undefined &&
|
|
cleaned.skipMultilineEncoding !== cleaned.originalSkipMultilineEncoding
|
|
) {
|
|
hasChanges = true;
|
|
} else {
|
|
cleaned.skipMultilineEncoding = undefined;
|
|
}
|
|
|
|
if (cleaned.tags !== undefined && !areArraysEqual(cleaned.tags, cleaned.originalTags)) {
|
|
hasChanges = true;
|
|
} else {
|
|
cleaned.tags = undefined;
|
|
}
|
|
|
|
if (
|
|
cleaned.secretMetadata !== undefined &&
|
|
!areArraysEqual(cleaned.secretMetadata, cleaned.originalSecretMetadata)
|
|
) {
|
|
hasChanges = true;
|
|
} else {
|
|
cleaned.secretMetadata = undefined;
|
|
}
|
|
|
|
if (cleaned.newSecretName !== undefined && cleaned.newSecretName !== cleaned.secretKey) {
|
|
hasChanges = true;
|
|
} else {
|
|
cleaned.newSecretName = undefined;
|
|
}
|
|
|
|
// If no changes remain, return null to indicate this update should be removed
|
|
return hasChanges ? cleaned : null;
|
|
};
|
|
|
|
const cleanupRevertedFolderFields = (update: PendingFolderUpdate): PendingFolderUpdate | null => {
|
|
const cleaned = { ...update };
|
|
let hasChanges = false;
|
|
|
|
if (
|
|
cleaned.folderName !== undefined &&
|
|
!areValuesEqual(cleaned.folderName, cleaned.originalFolderName)
|
|
) {
|
|
hasChanges = true;
|
|
}
|
|
|
|
if (
|
|
cleaned.description !== undefined &&
|
|
!areValuesEqual(cleaned.description, cleaned.originalDescription)
|
|
) {
|
|
hasChanges = true;
|
|
} else {
|
|
cleaned.description = undefined;
|
|
}
|
|
|
|
// If no changes remain, return null to indicate this update should be removed
|
|
return hasChanges ? cleaned : null;
|
|
};
|
|
|
|
type SelectedSecretState = {
|
|
selectedSecret: Record<string, SecretV3RawSanitized>;
|
|
action: {
|
|
toggle: (secret: SecretV3RawSanitized) => void;
|
|
reset: () => void;
|
|
set: (secrets: Record<string, SecretV3RawSanitized>) => void;
|
|
};
|
|
};
|
|
|
|
const createSelectedSecretStore: StateCreator<CombinedState, [], [], SelectedSecretState> = (
|
|
set
|
|
) => ({
|
|
selectedSecret: {},
|
|
action: {
|
|
toggle: (secret) =>
|
|
set((state) => {
|
|
const isChecked = Boolean(state.selectedSecret?.[secret.id]);
|
|
const newChecks = { ...state.selectedSecret };
|
|
if (isChecked) delete newChecks[secret.id];
|
|
else newChecks[secret.id] = secret;
|
|
return { selectedSecret: newChecks };
|
|
}),
|
|
reset: () => set({ selectedSecret: {} }),
|
|
set: (secrets) => set({ selectedSecret: secrets })
|
|
}
|
|
});
|
|
|
|
type BatchModeState = {
|
|
isBatchMode: boolean;
|
|
pendingChanges: PendingChanges;
|
|
pendingChangesByContext: Map<string, PendingChanges>;
|
|
existingSecretKeys: Set<string>;
|
|
existingFolderNames: Set<string>;
|
|
currentContext: BatchContext | null;
|
|
batchActions: {
|
|
addPendingChange: (change: PendingChange, context: BatchContext) => void;
|
|
loadPendingChanges: (context: BatchContext) => void;
|
|
clearAllPendingChanges: (context: BatchContext) => void;
|
|
setExistingKeys: (secretKeys: string[], folderNames: string[]) => void;
|
|
getTotalPendingChangesCount: () => number;
|
|
removePendingChange: (changeId: string, resourceType: string, context: BatchContext) => void;
|
|
};
|
|
};
|
|
|
|
const generateContextKey = (projectId: string, environment: string, secretPath: string) => {
|
|
return `${projectId}_${environment}_${secretPath}`;
|
|
};
|
|
|
|
const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState> = (set, get) => ({
|
|
isBatchMode: true, // Always enabled by default
|
|
pendingChanges: { secrets: [], folders: [] },
|
|
pendingChangesByContext: new Map<string, PendingChanges>(),
|
|
currentContext: null,
|
|
existingSecretKeys: new Set<string>(),
|
|
existingFolderNames: new Set<string>(),
|
|
batchActions: {
|
|
addPendingChange: (change: PendingChange, context: BatchContext) =>
|
|
set((state) => {
|
|
const contextKey = generateContextKey(
|
|
context.projectId,
|
|
context.environment,
|
|
context.secretPath
|
|
);
|
|
|
|
// Get existing changes for this context or create new empty state
|
|
const existingChanges = state.pendingChangesByContext.get(contextKey) || {
|
|
secrets: [],
|
|
folders: []
|
|
};
|
|
const newChanges = { ...existingChanges };
|
|
|
|
if (change.resourceType === "folder") {
|
|
const existingFolder =
|
|
state.existingFolderNames.has(change.folderName) ||
|
|
newChanges.folders.some((f) => f.folderName === change.folderName);
|
|
|
|
if (change.type === PendingAction.Create && existingFolder) {
|
|
createNotification({
|
|
text: "A folder with this name already exists",
|
|
type: "error"
|
|
});
|
|
return { pendingChanges: newChanges };
|
|
}
|
|
if (
|
|
change.type === PendingAction.Update &&
|
|
change.folderName !== change.originalFolderName &&
|
|
existingFolder
|
|
) {
|
|
createNotification({
|
|
text: "A folder with this name already exists",
|
|
type: "error"
|
|
});
|
|
return { pendingChanges: newChanges };
|
|
}
|
|
}
|
|
|
|
if (change.resourceType === "secret") {
|
|
const existingSecret =
|
|
state.existingSecretKeys.has(change.secretKey) ||
|
|
newChanges.secrets.some(
|
|
(s) =>
|
|
(s.secretKey === change.secretKey && s.type !== PendingAction.Create) ||
|
|
(change.type === PendingAction.Create &&
|
|
change.originalKey !== change.secretKey &&
|
|
s.secretKey === change.secretKey)
|
|
);
|
|
|
|
if (change.type === PendingAction.Create && existingSecret) {
|
|
createNotification({
|
|
text: "A secret with this name already exists",
|
|
type: "error"
|
|
});
|
|
return { pendingChanges: newChanges };
|
|
}
|
|
|
|
const existingNewSecretName =
|
|
change.type === PendingAction.Update &&
|
|
change.newSecretName &&
|
|
change.newSecretName !== change.secretKey &&
|
|
(state.existingSecretKeys.has(change.newSecretName) ||
|
|
newChanges.secrets.some(
|
|
(s) =>
|
|
(s.secretKey === change.newSecretName ||
|
|
(s.type === PendingAction.Update &&
|
|
s.newSecretName === change.newSecretName)) &&
|
|
s.id !== change.id
|
|
));
|
|
|
|
if (existingNewSecretName) {
|
|
createNotification({
|
|
text: "A secret with this name already exists",
|
|
type: "error"
|
|
});
|
|
return { pendingChanges: newChanges };
|
|
}
|
|
}
|
|
|
|
if (change.resourceType === "secret") {
|
|
const secretChanges = [...newChanges.secrets];
|
|
|
|
if (change.type === PendingAction.Create) {
|
|
const existingCreateIndex = secretChanges.findIndex(
|
|
(c) =>
|
|
c.type === PendingAction.Create &&
|
|
(c.secretKey === change.secretKey || c.secretKey === change.originalKey)
|
|
);
|
|
|
|
if (existingCreateIndex >= 0) {
|
|
secretChanges[existingCreateIndex] = {
|
|
...secretChanges[existingCreateIndex],
|
|
...change,
|
|
timestamp: Date.now()
|
|
};
|
|
} else {
|
|
secretChanges.push(change);
|
|
}
|
|
} else if (change.type === PendingAction.Update) {
|
|
const existingCreateIndex = secretChanges.findIndex(
|
|
(c) => c.type === PendingAction.Create && c.id === change.id
|
|
);
|
|
|
|
if (existingCreateIndex >= 0) {
|
|
const existingCreate = secretChanges[existingCreateIndex] as PendingSecretCreate;
|
|
secretChanges[existingCreateIndex] = {
|
|
...existingCreate,
|
|
secretKey: change.newSecretName || change.secretKey || existingCreate.secretKey,
|
|
secretValue:
|
|
change.secretValue !== undefined
|
|
? change.secretValue
|
|
: existingCreate.secretValue,
|
|
secretComment:
|
|
change.secretComment !== undefined
|
|
? change.secretComment
|
|
: existingCreate.secretComment,
|
|
skipMultilineEncoding:
|
|
change.skipMultilineEncoding !== undefined
|
|
? change.skipMultilineEncoding
|
|
: existingCreate.skipMultilineEncoding,
|
|
tags: change.tags !== undefined ? change.tags : existingCreate.tags,
|
|
secretMetadata:
|
|
change.secretMetadata !== undefined
|
|
? change.secretMetadata
|
|
: existingCreate.secretMetadata,
|
|
timestamp: Date.now()
|
|
};
|
|
} else {
|
|
const existingUpdateIndex = secretChanges.findIndex(
|
|
(c) => c.type === PendingAction.Update && c.id === change.id
|
|
);
|
|
|
|
if (existingUpdateIndex >= 0) {
|
|
const existingUpdate = secretChanges[existingUpdateIndex] as PendingSecretUpdate;
|
|
const mergedUpdate: PendingSecretUpdate = {
|
|
...existingUpdate,
|
|
secretKey: existingUpdate.secretKey,
|
|
originalValue: change.originalValue,
|
|
originalComment: existingUpdate.originalComment,
|
|
originalSkipMultilineEncoding: existingUpdate.originalSkipMultilineEncoding,
|
|
originalTags: existingUpdate.originalTags,
|
|
originalSecretMetadata: existingUpdate.originalSecretMetadata,
|
|
|
|
newSecretName:
|
|
change.newSecretName !== undefined
|
|
? change.newSecretName
|
|
: existingUpdate.newSecretName,
|
|
secretValue:
|
|
change.secretValue !== undefined
|
|
? change.secretValue
|
|
: existingUpdate.secretValue,
|
|
secretComment:
|
|
change.secretComment !== undefined
|
|
? change.secretComment
|
|
: existingUpdate.secretComment,
|
|
skipMultilineEncoding:
|
|
change.skipMultilineEncoding !== undefined
|
|
? change.skipMultilineEncoding
|
|
: existingUpdate.skipMultilineEncoding,
|
|
tags: change.tags !== undefined ? change.tags : existingUpdate.tags,
|
|
secretMetadata:
|
|
change.secretMetadata !== undefined
|
|
? change.secretMetadata
|
|
: existingUpdate.secretMetadata,
|
|
existingSecret: existingUpdate.existingSecret,
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
// Clean up reverted fields and check if any changes remain
|
|
const cleanedUpdate = cleanupRevertedSecretFields(mergedUpdate);
|
|
|
|
if (cleanedUpdate) {
|
|
// Still has changes, keep the update
|
|
secretChanges[existingUpdateIndex] = cleanedUpdate;
|
|
} else {
|
|
// No changes remain, remove the pending update
|
|
secretChanges.splice(existingUpdateIndex, 1);
|
|
}
|
|
} else {
|
|
// New update - clean it up before adding
|
|
const cleanedUpdate = cleanupRevertedSecretFields(change);
|
|
if (cleanedUpdate) {
|
|
secretChanges.push(cleanedUpdate);
|
|
}
|
|
// If cleanedUpdate is null, don't add it (no actual changes)
|
|
}
|
|
}
|
|
} else {
|
|
secretChanges.push(change);
|
|
}
|
|
|
|
newChanges.secrets = secretChanges;
|
|
} else if (change.resourceType === "folder") {
|
|
const folderChanges = [...newChanges.folders];
|
|
|
|
if (change.type === PendingAction.Create) {
|
|
const existingCreateIndex = folderChanges.findIndex(
|
|
(c) => c.type === PendingAction.Create && c.folderName === change.folderName
|
|
);
|
|
|
|
if (existingCreateIndex >= 0) {
|
|
folderChanges[existingCreateIndex] = {
|
|
...folderChanges[existingCreateIndex],
|
|
...change,
|
|
timestamp: Date.now()
|
|
};
|
|
} else {
|
|
folderChanges.push(change);
|
|
}
|
|
} else if (change.type === PendingAction.Update) {
|
|
const existingCreateIndex = folderChanges.findIndex(
|
|
(c) => c.type === PendingAction.Create && c.folderName === change.originalFolderName
|
|
);
|
|
|
|
if (existingCreateIndex >= 0) {
|
|
const existingCreate = folderChanges[existingCreateIndex] as PendingFolderCreate;
|
|
folderChanges[existingCreateIndex] = {
|
|
...existingCreate,
|
|
folderName: change.folderName || existingCreate.folderName,
|
|
description:
|
|
change.description !== undefined
|
|
? change.description
|
|
: existingCreate.description,
|
|
timestamp: Date.now()
|
|
};
|
|
} else {
|
|
const existingUpdateIndex = folderChanges.findIndex(
|
|
(c) => c.type === PendingAction.Update && c.id === change.id
|
|
);
|
|
|
|
if (existingUpdateIndex >= 0) {
|
|
const existingUpdate = folderChanges[existingUpdateIndex] as PendingFolderUpdate;
|
|
|
|
const mergedUpdate: PendingFolderUpdate = {
|
|
...existingUpdate,
|
|
originalFolderName: existingUpdate.originalFolderName,
|
|
originalDescription: existingUpdate.originalDescription,
|
|
|
|
folderName:
|
|
change.folderName !== undefined ? change.folderName : existingUpdate.folderName,
|
|
description:
|
|
change.description !== undefined
|
|
? change.description
|
|
: existingUpdate.description,
|
|
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
// Clean up reverted fields and check if any changes remain
|
|
const cleanedUpdate = cleanupRevertedFolderFields(mergedUpdate);
|
|
|
|
if (cleanedUpdate) {
|
|
// Still has changes, keep the update
|
|
folderChanges[existingUpdateIndex] = cleanedUpdate;
|
|
} else {
|
|
// No changes remain, remove the pending update
|
|
folderChanges.splice(existingUpdateIndex, 1);
|
|
}
|
|
} else {
|
|
// New update - clean it up before adding
|
|
const cleanedUpdate = cleanupRevertedFolderFields(change);
|
|
if (cleanedUpdate) {
|
|
folderChanges.push(cleanedUpdate);
|
|
}
|
|
// If cleanedUpdate is null, don't add it (no actual changes)
|
|
}
|
|
}
|
|
} else {
|
|
folderChanges.push(change);
|
|
}
|
|
|
|
newChanges.folders = folderChanges;
|
|
}
|
|
|
|
const updatedContextMap = new Map(state.pendingChangesByContext);
|
|
updatedContextMap.set(contextKey, newChanges);
|
|
|
|
const currentChanges =
|
|
contextKey ===
|
|
generateContextKey(
|
|
state.currentContext?.projectId || context.projectId,
|
|
state.currentContext?.environment || context.environment,
|
|
state.currentContext?.secretPath || context.secretPath
|
|
)
|
|
? newChanges
|
|
: state.pendingChanges;
|
|
|
|
return {
|
|
pendingChangesByContext: updatedContextMap,
|
|
pendingChanges: currentChanges,
|
|
currentContext: context
|
|
};
|
|
}),
|
|
|
|
removePendingChange: (changeId: string, resourceType: string, context: BatchContext) =>
|
|
set((state) => {
|
|
const contextKey = generateContextKey(
|
|
context.projectId,
|
|
context.environment,
|
|
context.secretPath
|
|
);
|
|
|
|
// Get existing changes for this context
|
|
const existingChanges = state.pendingChangesByContext.get(contextKey) || {
|
|
secrets: [],
|
|
folders: []
|
|
};
|
|
const newChanges = { ...existingChanges };
|
|
|
|
if (resourceType === "secret") {
|
|
newChanges.secrets = newChanges.secrets.filter((c) => c.id !== changeId);
|
|
} else if (resourceType === "folder") {
|
|
newChanges.folders = newChanges.folders.filter((c) => c.id !== changeId);
|
|
}
|
|
|
|
const updatedContextMap = new Map(state.pendingChangesByContext);
|
|
updatedContextMap.set(contextKey, newChanges);
|
|
|
|
const isCurrentContext =
|
|
state.currentContext &&
|
|
contextKey ===
|
|
generateContextKey(
|
|
state.currentContext.projectId,
|
|
state.currentContext.environment,
|
|
state.currentContext.secretPath
|
|
);
|
|
|
|
return {
|
|
pendingChangesByContext: updatedContextMap,
|
|
pendingChanges: isCurrentContext ? newChanges : state.pendingChanges
|
|
};
|
|
}),
|
|
|
|
loadPendingChanges: (context) => {
|
|
const contextKey = generateContextKey(
|
|
context.projectId,
|
|
context.environment,
|
|
context.secretPath
|
|
);
|
|
|
|
set((state) => {
|
|
const contextChanges = state.pendingChangesByContext.get(contextKey) || {
|
|
secrets: [],
|
|
folders: []
|
|
};
|
|
|
|
return {
|
|
pendingChanges: contextChanges,
|
|
currentContext: context
|
|
};
|
|
});
|
|
},
|
|
|
|
clearAllPendingChanges: (context) => {
|
|
const contextKey = generateContextKey(
|
|
context.projectId,
|
|
context.environment,
|
|
context.secretPath
|
|
);
|
|
|
|
set((state) => {
|
|
// Clear changes for this specific context
|
|
const updatedContextMap = new Map(state.pendingChangesByContext);
|
|
updatedContextMap.delete(contextKey);
|
|
|
|
// If this is the current context, also clear the active pending changes
|
|
const isCurrentContext =
|
|
state.currentContext &&
|
|
contextKey ===
|
|
generateContextKey(
|
|
state.currentContext.projectId,
|
|
state.currentContext.environment,
|
|
state.currentContext.secretPath
|
|
);
|
|
|
|
return {
|
|
pendingChangesByContext: updatedContextMap,
|
|
pendingChanges: isCurrentContext ? { secrets: [], folders: [] } : state.pendingChanges
|
|
};
|
|
});
|
|
},
|
|
|
|
setExistingKeys: (secretKeys, folderNames) =>
|
|
set({
|
|
existingSecretKeys: new Set(secretKeys),
|
|
existingFolderNames: new Set(folderNames)
|
|
}),
|
|
|
|
getTotalPendingChangesCount: () => {
|
|
const state = get();
|
|
return state.pendingChanges.secrets.length + state.pendingChanges.folders.length;
|
|
}
|
|
}
|
|
});
|
|
|
|
export enum PopUpNames {
|
|
CreateSecretForm = "create-secret-form"
|
|
}
|
|
|
|
type PopUpState = {
|
|
popUp: Record<string, { isOpen: boolean; data?: any }>;
|
|
popUpActions: {
|
|
togglePopUp: (id: PopUpNames, isOpen?: boolean) => void;
|
|
closePopUp: (id: PopUpNames) => void;
|
|
openPopUp: (id: PopUpNames, data?: any) => void;
|
|
};
|
|
};
|
|
const createPopUpStore: StateCreator<PopUpState> = (set) => ({
|
|
popUp: {},
|
|
popUpActions: {
|
|
closePopUp: (id) => set((state) => ({ popUp: { ...state.popUp, [id]: { isOpen: false } } })),
|
|
openPopUp: (id, data) =>
|
|
set((state) => ({ popUp: { ...state.popUp, [id]: { isOpen: true, data } } })),
|
|
togglePopUp: (id, isOpen) =>
|
|
set((state) => ({
|
|
popUp: { ...state.popUp, [id]: { isOpen: isOpen ?? !state.popUp[id].isOpen } }
|
|
}))
|
|
}
|
|
});
|
|
|
|
type CombinedState = SelectedSecretState & PopUpState & BatchModeState;
|
|
const StoreContext = createContext<StoreApi<CombinedState> | null>(null);
|
|
export const StoreProvider = ({ children }: { children: ReactNode }) => {
|
|
const storeRef = useRef<StoreApi<CombinedState>>(
|
|
createStore<CombinedState>((...a) => ({
|
|
...createSelectedSecretStore(...a),
|
|
...createPopUpStore(...a),
|
|
...createBatchModeStore(...a)
|
|
}))
|
|
);
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
const onRouteChangeStart = () => {
|
|
const state = storeRef.current?.getState();
|
|
state?.action.reset();
|
|
};
|
|
|
|
const unsubscribe = router.subscribe("onBeforeLoad", onRouteChangeStart);
|
|
return () => {
|
|
unsubscribe();
|
|
};
|
|
}, []);
|
|
|
|
return <StoreContext.Provider value={storeRef.current}>{children}</StoreContext.Provider>;
|
|
};
|
|
|
|
const useStoreContext = <T extends object>(selector: (state: CombinedState) => T): T => {
|
|
const ctx = useContext(StoreContext);
|
|
if (!ctx) throw new Error("Missing ");
|
|
return useStore(ctx, selector);
|
|
};
|
|
|
|
// selected secret context
|
|
export const useSelectedSecrets = () =>
|
|
useStoreContext(useShallow((state) => state.selectedSecret));
|
|
export const useSelectedSecretActions = () => useStoreContext(useShallow((state) => state.action));
|
|
|
|
// popup context
|
|
export const usePopUpState = (id: PopUpNames) =>
|
|
useStoreContext(useShallow((state) => state.popUp?.[id] || { isOpen: false }));
|
|
export const usePopUpAction = () => useStoreContext(useShallow((state) => state.popUpActions));
|
|
|
|
export const useBatchMode = () =>
|
|
useStoreContext(
|
|
useShallow((state) => ({
|
|
isBatchMode: state.isBatchMode,
|
|
pendingChanges: state.pendingChanges,
|
|
currentContext: state.currentContext,
|
|
totalChangesCount: state.batchActions.getTotalPendingChangesCount(),
|
|
secretChangesCount: state.pendingChanges.secrets.length,
|
|
folderChangesCount: state.pendingChanges.folders.length
|
|
}))
|
|
);
|
|
|
|
export const useBatchModeActions = () => useStoreContext(useShallow((state) => state.batchActions));
|