Update FieldHistoryModal and clipboard countdown bar (#1404)

This commit is contained in:
Leendert de Borst
2025-12-13 12:07:08 +01:00
parent 9d4a0d45f7
commit 7bd8694595
4 changed files with 45 additions and 146 deletions

View File

@@ -97,7 +97,7 @@ export const ClipboardCountdownBar: React.FC = () => {
}
return (
<div className="fixed top-0 left-0 right-0 z-50 h-1 bg-gray-200 dark:bg-gray-700">
<div className="fixed top-0 left-0 right-0 z-[60] h-1 bg-gray-200 dark:bg-gray-700">
<div
ref={animationRef}
className="h-full bg-orange-500"

View File

@@ -116,40 +116,13 @@ const FieldBlock: React.FC<FieldBlockProps> = ({ field, itemId }) => {
case 'Hidden':
return (
<>
<div>
<label htmlFor={field.FieldKey} className="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
{label}
{HistoryButton}
</label>
<div className="relative">
<input
type="password"
id={field.FieldKey}
readOnly
value={value}
className="w-full px-3 py-2.5 bg-white border border-gray-300 text-gray-900 text-sm rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400"
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
<button
type="button"
onClick={async () => {
try {
await navigator.clipboard.writeText(value);
} catch (err) {
console.error('Failed to copy:', err);
}
}}
className="p-1 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
title={t('common.copyToClipboard')}
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
</button>
</div>
</div>
</div>
<FormInputCopyToClipboard
id={field.FieldKey}
label={label}
value={value}
type="password"
labelSuffix={HistoryButton}
/>
{HistoryModal}
</>
);
@@ -179,40 +152,13 @@ const FieldBlock: React.FC<FieldBlockProps> = ({ field, itemId }) => {
default:
return (
<>
<div>
<label htmlFor={field.FieldKey} className="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
{label}
{HistoryButton}
</label>
<div className="relative">
<input
type="text"
id={field.FieldKey}
readOnly
value={value}
className="w-full px-3 py-2.5 bg-white border border-gray-300 text-gray-900 text-sm rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400"
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
<button
type="button"
onClick={async () => {
try {
await navigator.clipboard.writeText(value);
} catch (err) {
console.error('Failed to copy:', err);
}
}}
className="p-1 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
title={t('common.copyToClipboard')}
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
</button>
</div>
</div>
</div>
<FormInputCopyToClipboard
id={field.FieldKey}
label={label}
value={value}
type="text"
labelSuffix={HistoryButton}
/>
{HistoryModal}
</>
);

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/Forms/FormInputCopyToClipboard';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import type { FieldHistory, FieldType } from '@/utils/dist/core/models/vault';
@@ -33,7 +34,6 @@ const FieldHistoryModal: React.FC<FieldHistoryModalProps> = ({
const { t } = useTranslation();
const dbContext = useDb();
const [history, setHistory] = useState<FieldHistory[]>([]);
const [visibleValues, setVisibleValues] = useState<Set<string>>(new Set());
const [loading, setLoading] = useState(true);
// For non-hidden fields, show values by default
@@ -59,32 +59,6 @@ const FieldHistoryModal: React.FC<FieldHistoryModalProps> = ({
return null;
}
/**
* Toggle the visibility of a field value in the history modal.
*/
const toggleValueVisibility = (historyId: string): void => {
setVisibleValues(prev => {
const newSet = new Set(prev);
if (newSet.has(historyId)) {
newSet.delete(historyId);
} else {
newSet.add(historyId);
}
return newSet;
});
};
/**
* Copy a field value to the clipboard.
*/
const copyToClipboard = async (value: string): Promise<void> => {
try {
await navigator.clipboard.writeText(value);
} catch (error) {
console.error('Failed to copy to clipboard:', error);
}
};
/**
* Format a date string to a human readable format.
*/
@@ -114,13 +88,26 @@ const FieldHistoryModal: React.FC<FieldHistoryModalProps> = ({
}
};
/**
* Handle click on backdrop to close modal.
*/
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>): void => {
// Only close if clicking directly on the backdrop/container, not the modal content
if (e.target === e.currentTarget) {
onClose();
}
};
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div className="fixed inset-0 bg-black bg-opacity-80 transition-opacity" onClick={onClose} />
{/* Modal */}
<div className="fixed inset-0 flex items-center justify-center p-4">
{/* Modal container - clicking here (outside modal content) closes */}
<div
className="fixed inset-0 flex items-center justify-center p-4"
onClick={handleBackdropClick}
>
<div className="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all w-full max-w-2xl max-h-[80vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between mb-4">
@@ -153,50 +140,24 @@ const FieldHistoryModal: React.FC<FieldHistoryModalProps> = ({
<div className="space-y-3">
{history.map((record) => {
const values = parseValueSnapshot(record.ValueSnapshot);
/**
* For hidden fields, check if this record is explicitly set to visible
* For non-hidden fields, always show values (no toggle needed)
*/
const isVisible = shouldMaskByDefault ? visibleValues.has(record.Id) : true;
return (
<div
key={record.Id}
className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 border border-gray-200 dark:border-gray-600"
className="rounded-lg p-4 border border-gray-200 dark:border-gray-700"
>
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-gray-600 dark:text-gray-400">
{formatDate(record.ChangedAt)}
</div>
{shouldMaskByDefault && (
<button
type="button"
onClick={() => toggleValueVisibility(record.Id)}
className="text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 focus:outline-none"
>
{isVisible ? t('common.hide') : t('common.show')}
</button>
)}
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
{formatDate(record.ChangedAt)}
</div>
{values.map((value, idx) => (
<div
key={idx}
className="flex items-center gap-2 mt-2"
>
<div className="flex-1 font-mono text-sm text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 rounded px-3 py-2 border border-gray-200 dark:border-gray-600 break-all">
{shouldMaskByDefault && !isVisible ? '\u2022'.repeat(12) : value}
</div>
<button
type="button"
onClick={() => copyToClipboard(value)}
className="p-2 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 focus:outline-none"
title={t('common.copyToClipboard')}
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" />
</svg>
</button>
<div key={idx} className="mt-2">
<FormInputCopyToClipboard
id={`history-${record.Id}-${idx}`}
label=""
value={value}
type={shouldMaskByDefault ? 'password' : 'text'}
/>
</div>
))}
</div>
@@ -205,17 +166,6 @@ const FieldHistoryModal: React.FC<FieldHistoryModalProps> = ({
</div>
)}
</div>
{/* Footer */}
<div className="mt-4 flex justify-end">
<button
type="button"
className="inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none"
onClick={onClose}
>
{t('common.close')}
</button>
</div>
</div>
</div>
</div>

View File

@@ -12,6 +12,7 @@ type FormInputCopyToClipboardProps = {
label: string;
value: string;
type?: 'text' | 'password';
labelSuffix?: React.ReactNode;
}
const clipboardService = new ClipboardCopyService();
@@ -60,7 +61,8 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
id,
label,
value,
type = 'text'
type = 'text',
labelSuffix
}) => {
const { t } = useTranslation();
const [showPassword, setShowPassword] = useState(false);
@@ -101,6 +103,7 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
<div>
<label htmlFor={id} className="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
{label}
{labelSuffix}
</label>
<div className="relative">
<input