Addressed PR suggestions

This commit is contained in:
Carlos Monastyrski
2025-07-18 15:32:22 -03:00
parent cf6b9d8905
commit a064fae94e
5 changed files with 142 additions and 57 deletions
+16 -6
View File
@@ -1,24 +1,34 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useBlocker } from "@tanstack/react-router"; import { useBlocker } from "@tanstack/react-router";
import { BatchContext, useBatchModeActions } from "@app/pages/secret-manager/SecretDashboardPage/SecretMainPage.store";
type TNavigationBlockerReturn = { type TNavigationBlockerReturn = {
isBlocked: boolean; isBlocked: boolean;
}; };
export const useNavigationBlocker = ( export const useNavigationBlocker = ({
shouldBlock: boolean, shouldBlock = false,
message: string = "Are you sure you want to leave? You may have unsaved changes." message = "Are you sure you want to leave? You may have unsaved changes.",
): TNavigationBlockerReturn => { context
}: {
shouldBlock: boolean;
message: string;
context: BatchContext;
}): TNavigationBlockerReturn => {
const { clearAllPendingChanges } = useBatchModeActions();
const blockerFn = useCallback(() => { const blockerFn = useCallback(() => {
if (!shouldBlock) return false; if (!shouldBlock) return false;
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
const confirmed = window.confirm(message); const confirmed = window.confirm(message);
if (confirmed) {
clearAllPendingChanges(context);
}
return !confirmed; return !confirmed;
}, [shouldBlock, message]); }, [shouldBlock, message, context]);
useBlocker(blockerFn); useBlocker(blockerFn, shouldBlock);
return { return {
isBlocked: shouldBlock isBlocked: shouldBlock
@@ -346,7 +346,7 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
if (change.resourceType === "secret") { if (change.resourceType === "secret") {
const existingSecret = const existingSecret =
state.existingSecretKeys.has(change.secretKey) || state.existingSecretKeys.has(change.secretKey) ||
newChanges.secrets.some((s) => s.secretKey === 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) { if (change.type === PendingAction.Create && existingSecret) {
return { pendingChanges: newChanges }; return { pendingChanges: newChanges };
@@ -200,21 +200,9 @@ export const SecretItem = memo(
isAutoSavingRef.current = true; isAutoSavingRef.current = true;
try { try {
const hasDirectReferences = importedBy?.some(({ folders }) => await onSaveSecret(secret, { ...secret, ...data }, () => {
folders?.some(({ secrets }) => reset();
secrets?.some(({ referencedSecretKey }) => referencedSecretKey === secret.key) });
)
);
if (hasDirectReferences) {
await onSaveSecret(secret, { ...secret, ...data }, () => {
reset();
});
} else {
await onSaveSecret(secret, { ...secret, ...data }, () => {
reset();
});
}
} catch (error) { } catch (error) {
console.error("Auto-save failed:", error); console.error("Auto-save failed:", error);
} finally { } finally {
@@ -268,7 +256,7 @@ export const SecretItem = memo(
); );
const isReadOnlySecret = const isReadOnlySecret =
isReadOnly || isRotatedSecret || (isPending && pendingAction !== PendingAction.Update); isReadOnly || isRotatedSecret || (isPending && pendingAction === PendingAction.Delete);
const { secretValueHidden } = secret; const { secretValueHidden } = secret;
@@ -355,7 +343,6 @@ export const SecretItem = memo(
isDirty && "border-primary-400/50", isDirty && "border-primary-400/50",
isRotatedSecret && "bg-mineshaft-700/60", isRotatedSecret && "bg-mineshaft-700/60",
isPending && "bg-mineshaft-700/60", isPending && "bg-mineshaft-700/60",
isInAutoSaveMode && "border-primary-400/75 bg-primary-900/20",
pendingAction === PendingAction.Delete && "border-l-2 border-l-red-600/75", pendingAction === PendingAction.Delete && "border-l-2 border-l-red-600/75",
pendingAction === PendingAction.Update && "border-l-2 border-l-yellow-600/75", pendingAction === PendingAction.Update && "border-l-2 border-l-yellow-600/75",
pendingAction === PendingAction.Create && "border-l-2 border-l-green-600/75" pendingAction === PendingAction.Create && "border-l-2 border-l-green-600/75"
@@ -421,14 +408,6 @@ export const SecretItem = memo(
tabIndex={0} tabIndex={0}
role="button" role="button"
> >
{isInAutoSaveMode && (
<Tooltip content="Auto-saving changes">
<div className="mr-2 flex items-center">
<Spinner className="h-3 w-3 text-primary" />
</div>
</Tooltip>
)}
{secretValueHidden && !isOverriden && ( {secretValueHidden && !isOverriden && (
<Tooltip <Tooltip
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`} content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
@@ -716,6 +695,94 @@ export const SecretItem = memo(
</Popover> </Popover>
</div> </div>
)} )}
{pendingAction === PendingAction.Create && (
<div
key="actions"
className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2"
>
<DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild disabled={!isAllowed}>
<IconButton
ariaLabel="tags"
variant="plain"
size="sm"
className={twMerge(
"w-0 overflow-hidden p-0 group-hover:w-5 data-[state=open]:w-5",
hasTagsApplied && "w-5 text-primary"
)}
isDisabled={!isAllowed}
>
<Tooltip content="Tags">
<FontAwesomeSymbol
className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.Tags}
/>
</Tooltip>
</IconButton>
</DropdownMenuTrigger>
)}
</ProjectPermissionCan>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
{tags.map((tag) => {
const { id: tagId, slug, color } = tag;
const isTagSelected = selectedTagsGroupById?.[tagId];
return (
<DropdownMenuItem
onClick={() => handleTagSelect(tag)}
key={`${secret.id}-${tagId}`}
icon={
isTagSelected && (
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.CheckedCircle}
className="h-3 w-3"
/>
)
}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }}
/>
{slug}
</div>
</DropdownMenuItem>
);
})}
<DropdownMenuItem className="px-1.5" asChild>
<Button
size="xs"
className="w-full"
colorSchema="primary"
variant="outline_bg"
leftIcon={
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Tags}
className="h-3 w-3"
/>
}
onClick={onCreateTag}
>
Create a tag
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</div> </div>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{isInAutoSaveMode ? ( {isInAutoSaveMode ? (
@@ -726,11 +793,7 @@ export const SecretItem = memo(
animate={{ x: 0, opacity: 1 }} animate={{ x: 0, opacity: 1 }}
exit={{ x: -10, opacity: 0 }} exit={{ x: -10, opacity: 0 }}
> >
<Tooltip content="Auto-saving changes..."> <div className="h-12 w-12" />
<div className="flex items-center text-primary">
<Spinner className="h-4 w-4" />
</div>
</Tooltip>
</motion.div> </motion.div>
) : !isDirty ? ( ) : !isDirty ? (
isPending ? ( isPending ? (
@@ -757,19 +820,21 @@ export const SecretItem = memo(
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<IconButton <Tooltip content="Discard">
ariaLabel="delete-value" <IconButton
variant="plain" ariaLabel="delete-value"
colorSchema="danger" variant="plain"
size="md" colorSchema="danger"
className="p-0 opacity-0 group-hover:opacity-100" size="md"
onClick={() => handleDeletePending(secret)} className="p-0 opacity-0 group-hover:opacity-100"
> onClick={() => handleDeletePending(secret)}
<FontAwesomeSymbol >
symbolName={FontAwesomeSpriteName.Close} <FontAwesomeSymbol
className="h-5 w-4" symbolName={FontAwesomeSpriteName.Close}
/> className="h-5 w-4"
</IconButton> />
</IconButton>
</Tooltip>
</motion.div> </motion.div>
) : ( ) : (
<motion.div <motion.div
@@ -815,7 +880,7 @@ export const SecretItem = memo(
isDisabled={!isAllowed || isRotatedSecret} isDisabled={!isAllowed || isRotatedSecret}
> >
<FontAwesomeSymbol <FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Close} symbolName={FontAwesomeSpriteName.Trash}
className="h-5 w-4" className="h-5 w-4"
/> />
</IconButton> </IconButton>
@@ -87,8 +87,15 @@ export const SecretListView = ({
const { toggle: toggleSelectedSecret } = useSelectedSecretActions(); const { toggle: toggleSelectedSecret } = useSelectedSecretActions();
const { isBatchMode, pendingChanges } = useBatchMode(); const { isBatchMode, pendingChanges } = useBatchMode();
useNavigationBlocker( useNavigationBlocker(
pendingChanges.secrets.length > 0 || pendingChanges.folders.length > 0, {
"You have unsaved work. Are you sure you want to leave?" shouldBlock: pendingChanges.secrets.length > 0 || pendingChanges.folders.length > 0,
message: "You have unsaved changes. If you leave now, your work will be lost. Do you want to continue?",
context: {
workspaceId,
environment,
secretPath
}
}
); );
const { addPendingChange } = useBatchModeActions(); const { addPendingChange } = useBatchModeActions();
@@ -316,7 +323,7 @@ export const SecretListView = ({
secretValue: value || "", secretValue: value || "",
secretComment: comment || "", secretComment: comment || "",
skipMultilineEncoding: modSecret.skipMultilineEncoding || false, skipMultilineEncoding: modSecret.skipMultilineEncoding || false,
tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || "" })) || [], tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || tag.slug || "" })) || [],
secretMetadata: secretMetadata || [], secretMetadata: secretMetadata || [],
timestamp: Date.now(), timestamp: Date.now(),
resourceType: "secret", resourceType: "secret",
@@ -13,7 +13,8 @@ import {
faLock, faLock,
faProjectDiagram, faProjectDiagram,
faShare, faShare,
faTags faTags,
faTrash
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { z } from "zod"; import { z } from "zod";
@@ -82,7 +83,8 @@ export enum FontAwesomeSpriteName {
ReplicatedSecretKey = "secret-replicated", ReplicatedSecretKey = "secret-replicated",
ShareSecret = "share-secret", ShareSecret = "share-secret",
KeyLock = "key-lock", KeyLock = "key-lock",
SecretReferenceTree = "secret-reference-tree" SecretReferenceTree = "secret-reference-tree",
Trash = "trash"
} }
// this is an optimization technique // this is an optimization technique
@@ -101,5 +103,6 @@ export const FontAwesomeSpriteSymbols = [
{ icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey }, { icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey },
{ icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret }, { icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret },
{ icon: faLock, symbol: FontAwesomeSpriteName.KeyLock }, { icon: faLock, symbol: FontAwesomeSpriteName.KeyLock },
{ icon: faProjectDiagram, symbol: FontAwesomeSpriteName.SecretReferenceTree } { icon: faProjectDiagram, symbol: FontAwesomeSpriteName.SecretReferenceTree },
{ icon: faTrash, symbol: FontAwesomeSpriteName.Trash }
]; ];