/* 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; action: { toggle: (secret: SecretV3RawSanitized) => void; reset: () => void; set: (secrets: Record) => void; }; }; const createSelectedSecretStore: StateCreator = ( 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; existingSecretKeys: Set; existingFolderNames: Set; 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 = (set, get) => ({ isBatchMode: true, // Always enabled by default pendingChanges: { secrets: [], folders: [] }, pendingChangesByContext: new Map(), currentContext: null, existingSecretKeys: new Set(), existingFolderNames: new Set(), 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; popUpActions: { togglePopUp: (id: PopUpNames, isOpen?: boolean) => void; closePopUp: (id: PopUpNames) => void; openPopUp: (id: PopUpNames, data?: any) => void; }; }; const createPopUpStore: StateCreator = (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 | null>(null); export const StoreProvider = ({ children }: { children: ReactNode }) => { const storeRef = useRef>( createStore((...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 {children}; }; const useStoreContext = (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));