env import and single secret creation conflict resolution - #956
Conversation
…dialogs and secret rows
…detection functions
There was a problem hiding this comment.
@teyim this is great work! Apologies for the delay in reviewing this. I have a few suggestions on the UX and code conventions to address:
UX
Proposed changes to the conflict dialog:
- Remove the two summary cards at the top. Stack the elements: "x conflicts remaining", then the progress bar, then "Resolved x of y" in small gray text.
- Show all values in plain text. Remove the "Reveal all" button. Keep
ph-no-captureon the container. - Add a small gray "Select a value" label above each value list.
- Move the "Resolve all conflicts" menu up, next to the "Conflicts to resolve" heading. Open the menu downward.
- Move the "Back" button to the left side of the footer.
- Animate the accordion with real heights, from
grid-rows-[0fr]togrid-rows-[1fr]. Then the dialog height does not jump when the next conflict opens.
Code conventions
- Use the
iconprop onButtonfor all button icons. Do not pass icons as children. See the inline suggestion. - Use our native
ProgressBarcomponent (components/common/ProgressBar.tsx) instead of a raw<progress>element. See the inline suggestion. - Extract a shared
useEnvImportConflictshook for the parse, conflict, and selection state. Both import dialogs duplicate this flow, and their reset logic already differs.
Happy to push these changes directly to the branch or create a separate PR for you. Let me know!
| <progress | ||
| className="mt-2 h-1.5 w-full accent-emerald-500" | ||
| max={conflicts.length} | ||
| value={resolvedCount} | ||
| aria-label={`${resolvedCount} of ${conflicts.length} conflicts resolved`} | ||
| /> |
There was a problem hiding this comment.
Use the native ProgressBar component here. Add the import: import ProgressBar from '@/components/common/ProgressBar'
| <progress | |
| className="mt-2 h-1.5 w-full accent-emerald-500" | |
| max={conflicts.length} | |
| value={resolvedCount} | |
| aria-label={`${resolvedCount} of ${conflicts.length} conflicts resolved`} | |
| /> | |
| <ProgressBar | |
| percentage={(resolvedCount / conflicts.length) * 100} | |
| color="bg-emerald-500" | |
| size="sm" | |
| /> |
| <Button | ||
| variant="outline" | ||
| onClick={() => toggleRevealed(conflict.key)} | ||
| aria-label={`${isRevealed ? 'Hide' : 'Reveal'} all values for ${conflict.key}`} | ||
| > | ||
| {isRevealed ? <FaEyeSlash /> : <FaEye />} | ||
| {isRevealed ? 'Hide all' : 'Reveal all'} | ||
| </Button> |
There was a problem hiding this comment.
Use the icon prop for Button icons. Do not pass icons as children. The prop keeps the icon size stable and swaps it for the spinner during loading states.
| <Button | |
| variant="outline" | |
| onClick={() => toggleRevealed(conflict.key)} | |
| aria-label={`${isRevealed ? 'Hide' : 'Reveal'} all values for ${conflict.key}`} | |
| > | |
| {isRevealed ? <FaEyeSlash /> : <FaEye />} | |
| {isRevealed ? 'Hide all' : 'Reveal all'} | |
| </Button> | |
| <Button | |
| variant="outline" | |
| onClick={() => toggleRevealed(conflict.key)} | |
| aria-label={`${isRevealed ? 'Hide' : 'Reveal'} all values for ${conflict.key}`} | |
| icon={isRevealed ? FaEyeSlash : FaEye} | |
| > | |
| {isRevealed ? 'Hide all' : 'Reveal all'} | |
| </Button> |
Note: if we adopt the plain-text values layout, this button goes away. The same rule applies to the chevron on "Resolve all conflicts".

🔍 Overview
This PR improves duplicate secret-key handling across environment imports and inline secret editing.
Previously, importing
.envcontent containing repeated keys could produce ambiguous results because users could not explicitly choose which value to retain. Duplicate keys created or edited directly in secret rows also lacked clear, actionable feedback. (Related to #946 )This change introduces a conflict-resolution workflow for imports and inline duplicate-key validation for environment and app secret rows.
💡 Proposed Changes
Import conflict resolution
Inline duplicate-key validation
Internal improvements
.envparsing, grouping, and conflict-resolution utilities.ph-no-captureprotection for sensitive values and validation content.🖼️ Screenshots or Demo
Development.Example.App.testing.Phase.Console.July.mp4
📝 Release Notes
Added an explicit conflict-resolution workflow for duplicate keys found during
.envimports. Users can now review conflicting values and select which occurrence to retain before importing.Duplicate keys entered directly in environment or app secret rows are now highlighted with actionable validation feedback.
There are no breaking changes or migration requirements.
❓ Open Questions
If there are aspects of the changes that you're unsure about or would like feedback on, list them here.
🧪 Testing
Added and updated tests covering:
.envcontent without collapsing duplicate keys.🎯 Reviewer Focus
Suggested review order:
frontend/utils/secrets.ts.envparsing and conflict-resolution behavior.frontend/components/environments/secrets/import/EnvConflictResolution.tsxImport dialogs
SingleEnvImportDialog.tsxMultiEnvImportDialog.tsxInline secret rows
SecretRow.tsxAppSecretRow.tsxEnvironment and app secret lists
Component and utility tests
➕ Additional Context
✨ How to Test the Changes Locally
Start the development stack and open https://localhost/. Import .env content containing duplicate keys with different values, confirm the conflict-resolution step appears, and verify individual selection, reveal/hide, bulk resolution, reset, and continue behavior. Also create duplicate keys directly in environment and app secret rows to confirm inline validation appears and is removed when a conflicting secret is renamed or staged for deletion.
💚 Did You...