mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-24 16:32:20 -04:00
Update FieldHistoryModal and clipboard countdown bar (#1404)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user