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

View File

@@ -1,24 +1,34 @@
import { useCallback } from "react";
import { useBlocker } from "@tanstack/react-router";
import { BatchContext, useBatchModeActions } from "@app/pages/secret-manager/SecretDashboardPage/SecretMainPage.store";
type TNavigationBlockerReturn = {
isBlocked: boolean;
};
export const useNavigationBlocker = (
shouldBlock: boolean,
message: string = "Are you sure you want to leave? You may have unsaved changes."
): TNavigationBlockerReturn => {
export const useNavigationBlocker = ({
shouldBlock = false,
message = "Are you sure you want to leave? You may have unsaved changes.",
context
}: {
shouldBlock: boolean;
message: string;
context: BatchContext;
}): TNavigationBlockerReturn => {
const { clearAllPendingChanges } = useBatchModeActions();
const blockerFn = useCallback(() => {
if (!shouldBlock) return false;
// eslint-disable-next-line no-alert
const confirmed = window.confirm(message);
if (confirmed) {
clearAllPendingChanges(context);
}
return !confirmed;
}, [shouldBlock, message]);
}, [shouldBlock, message, context]);
useBlocker(blockerFn);
useBlocker(blockerFn, shouldBlock);
return {
isBlocked: shouldBlock

View File

@@ -346,7 +346,7 @@ const createBatchModeStore: StateCreator<CombinedState, [], [], BatchModeState>
if (change.resourceType === "secret") {
const existingSecret =
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) {
return { pendingChanges: newChanges };

View File

@@ -200,21 +200,9 @@ export const SecretItem = memo(
isAutoSavingRef.current = true;
try {
const hasDirectReferences = importedBy?.some(({ folders }) =>
folders?.some(({ secrets }) =>
secrets?.some(({ referencedSecretKey }) => referencedSecretKey === secret.key)
)
);
if (hasDirectReferences) {
await onSaveSecret(secret, { ...secret, ...data }, () => {
reset();
});
} else {
await onSaveSecret(secret, { ...secret, ...data }, () => {
reset();
});
}
await onSaveSecret(secret, { ...secret, ...data }, () => {
reset();
});
} catch (error) {
console.error("Auto-save failed:", error);
} finally {
@@ -268,7 +256,7 @@ export const SecretItem = memo(
);
const isReadOnlySecret =
isReadOnly || isRotatedSecret || (isPending && pendingAction !== PendingAction.Update);
isReadOnly || isRotatedSecret || (isPending && pendingAction === PendingAction.Delete);
const { secretValueHidden } = secret;
@@ -355,7 +343,6 @@ export const SecretItem = memo(
isDirty && "border-primary-400/50",
isRotatedSecret && "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.Update && "border-l-2 border-l-yellow-600/75",
pendingAction === PendingAction.Create && "border-l-2 border-l-green-600/75"
@@ -421,14 +408,6 @@ export const SecretItem = memo(
tabIndex={0}
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 && (
<Tooltip
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>
</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>
<AnimatePresence mode="wait">
{isInAutoSaveMode ? (
@@ -726,11 +793,7 @@ export const SecretItem = memo(
animate={{ x: 0, opacity: 1 }}
exit={{ x: -10, opacity: 0 }}
>
<Tooltip content="Auto-saving changes...">
<div className="flex items-center text-primary">
<Spinner className="h-4 w-4" />
</div>
</Tooltip>
<div className="h-12 w-12" />
</motion.div>
) : !isDirty ? (
isPending ? (
@@ -757,19 +820,21 @@ export const SecretItem = memo(
</IconButton>
</Tooltip>
<IconButton
ariaLabel="delete-value"
variant="plain"
colorSchema="danger"
size="md"
className="p-0 opacity-0 group-hover:opacity-100"
onClick={() => handleDeletePending(secret)}
>
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Close}
className="h-5 w-4"
/>
</IconButton>
<Tooltip content="Discard">
<IconButton
ariaLabel="delete-value"
variant="plain"
colorSchema="danger"
size="md"
className="p-0 opacity-0 group-hover:opacity-100"
onClick={() => handleDeletePending(secret)}
>
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Close}
className="h-5 w-4"
/>
</IconButton>
</Tooltip>
</motion.div>
) : (
<motion.div
@@ -815,7 +880,7 @@ export const SecretItem = memo(
isDisabled={!isAllowed || isRotatedSecret}
>
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Close}
symbolName={FontAwesomeSpriteName.Trash}
className="h-5 w-4"
/>
</IconButton>

View File

@@ -87,8 +87,15 @@ export const SecretListView = ({
const { toggle: toggleSelectedSecret } = useSelectedSecretActions();
const { isBatchMode, pendingChanges } = useBatchMode();
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();
@@ -316,7 +323,7 @@ export const SecretListView = ({
secretValue: value || "",
secretComment: comment || "",
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 || [],
timestamp: Date.now(),
resourceType: "secret",

View File

@@ -13,7 +13,8 @@ import {
faLock,
faProjectDiagram,
faShare,
faTags
faTags,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { z } from "zod";
@@ -82,7 +83,8 @@ export enum FontAwesomeSpriteName {
ReplicatedSecretKey = "secret-replicated",
ShareSecret = "share-secret",
KeyLock = "key-lock",
SecretReferenceTree = "secret-reference-tree"
SecretReferenceTree = "secret-reference-tree",
Trash = "trash"
}
// this is an optimization technique
@@ -101,5 +103,6 @@ export const FontAwesomeSpriteSymbols = [
{ icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey },
{ icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret },
{ icon: faLock, symbol: FontAwesomeSpriteName.KeyLock },
{ icon: faProjectDiagram, symbol: FontAwesomeSpriteName.SecretReferenceTree }
{ icon: faProjectDiagram, symbol: FontAwesomeSpriteName.SecretReferenceTree },
{ icon: faTrash, symbol: FontAwesomeSpriteName.Trash }
];