mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-24 16:32:20 -04:00
Update modals to use shared wrapper (#1404)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
|
||||
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/Forms/FormInputCopyToClipboard';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
@@ -56,10 +57,6 @@ const FieldHistoryModal: React.FC<FieldHistoryModalProps> = ({
|
||||
}
|
||||
}, [isOpen, dbContext?.sqliteClient, itemId, fieldKey]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date string to a human readable format.
|
||||
*/
|
||||
@@ -89,87 +86,52 @@ 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 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">
|
||||
<h3 className="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{fieldLabel} {t('credentials.history')}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
onClick={onClose}
|
||||
>
|
||||
<span className="sr-only">{t('common.close')}</span>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="overflow-y-auto flex-1">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" />
|
||||
</div>
|
||||
) : history.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
{t('credentials.noHistoryAvailable')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{history.map((record) => {
|
||||
const values = parseValueSnapshot(record.ValueSnapshot);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={record.Id}
|
||||
className="rounded-lg p-4 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<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="mt-2">
|
||||
<FormInputCopyToClipboard
|
||||
id={`history-${record.Id}-${idx}`}
|
||||
label=""
|
||||
value={value}
|
||||
type={shouldMaskByDefault ? 'password' : 'text'}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ModalWrapper
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={`${fieldLabel} ${t('credentials.history')}`}
|
||||
maxWidth="max-w-2xl"
|
||||
bodyClassName="px-4 pb-4 overflow-y-auto max-h-[60vh]"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : history.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
{t('credentials.noHistoryAvailable')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{history.map((record) => {
|
||||
const values = parseValueSnapshot(record.ValueSnapshot);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={record.Id}
|
||||
className="rounded-lg p-4 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<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="mt-2">
|
||||
<FormInputCopyToClipboard
|
||||
id={`history-${record.Id}-${idx}`}
|
||||
label=""
|
||||
value={value}
|
||||
type={shouldMaskByDefault ? 'password' : 'text'}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
|
||||
|
||||
type ConfirmDeleteModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reusable confirmation modal for delete actions.
|
||||
* Uses the danger styling to indicate a destructive action.
|
||||
*/
|
||||
const ConfirmDeleteModal: React.FC<ConfirmDeleteModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title,
|
||||
message,
|
||||
confirmText
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ModalWrapper
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={title}
|
||||
maxWidth="max-w-sm"
|
||||
footer={
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="px-4 py-2 text-sm bg-red-600 text-white rounded hover:bg-red-700"
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
{message}
|
||||
</p>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmDeleteModal;
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
|
||||
|
||||
type HelpModalProps = {
|
||||
title: string;
|
||||
content: string;
|
||||
@@ -38,45 +40,15 @@ const HelpModal: React.FC<HelpModalProps> = ({ title, content, className = '' })
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm w-full">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{title}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{content}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="mt-4 w-full px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('common.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ModalWrapper
|
||||
isOpen={showModal}
|
||||
onClose={() => setShowModal(false)}
|
||||
title={title}
|
||||
>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{content}
|
||||
</p>
|
||||
</ModalWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import QRCode from 'qrcode';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
|
||||
import { MobileLoginErrorCode } from '@/entrypoints/popup/types/MobileLoginErrorCode';
|
||||
import { MobileLoginUtility } from '@/entrypoints/popup/utils/MobileLoginUtility';
|
||||
|
||||
@@ -170,74 +171,50 @@ const MobileUnlockModal: React.FC<IMobileUnlockModalProps> = ({
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = mode === 'unlock' ? t('auth.unlockWithMobile') : t('auth.loginWithMobile');
|
||||
const description = t('auth.scanQrCode');
|
||||
|
||||
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={handleClose} />
|
||||
<ModalWrapper
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
title={title}
|
||||
showHeaderBorder={false}
|
||||
bodyClassName="px-6 pb-6"
|
||||
>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<div className="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-6 pb-6 pt-5 text-left shadow-xl transition-all w-full max-w-md">
|
||||
{/* Close button */}
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-4 top-4 text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<span className="sr-only">{t('common.close')}</span>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-700 rounded text-red-700 dark:text-red-400 text-sm">
|
||||
{getErrorMessage(error)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="mt-3">
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-700 rounded text-red-700 dark:text-red-400 text-sm">
|
||||
{getErrorMessage(error)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{qrCodeUrl && (
|
||||
<div className="flex flex-col items-center mb-4">
|
||||
<img src={qrCodeUrl} alt="QR Code" className="border-4 border-gray-200 dark:border-gray-600 rounded mb-3" />
|
||||
<div className="text-gray-700 dark:text-gray-300 text-sm font-medium">
|
||||
{formatTime(timeRemaining)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!qrCodeUrl && !error && (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="mt-4 w-full inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 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 focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
{qrCodeUrl && (
|
||||
<div className="flex flex-col items-center mb-4">
|
||||
<img src={qrCodeUrl} alt="QR Code" className="border-4 border-gray-200 dark:border-gray-600 rounded mb-3" />
|
||||
<div className="text-gray-700 dark:text-gray-300 text-sm font-medium">
|
||||
{formatTime(timeRemaining)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!qrCodeUrl && !error && (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="mt-4 w-full inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 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 focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
|
||||
|
||||
interface IModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -14,6 +15,7 @@ interface IModalProps {
|
||||
|
||||
/**
|
||||
* A reusable modal component for confirmations and alerts.
|
||||
* Built on top of ModalWrapper for consistent behavior.
|
||||
*/
|
||||
const Modal: React.FC<IModalProps> = ({
|
||||
isOpen,
|
||||
@@ -25,81 +27,62 @@ const Modal: React.FC<IModalProps> = ({
|
||||
cancelText = '',
|
||||
variant = 'default'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const confirmButtonClass = variant === 'danger'
|
||||
? 'bg-red-600 hover:bg-red-700 focus:ring-red-500'
|
||||
: 'bg-primary-600 hover:bg-primary-700 focus:ring-primary-500';
|
||||
|
||||
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">
|
||||
<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-lg">
|
||||
{/* Close button */}
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-4 top-4 text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
onClick={onClose}
|
||||
>
|
||||
<span className="sr-only">{t('common.close')}</span>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
<ModalWrapper
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
maxWidth="max-w-lg"
|
||||
showHeaderBorder={false}
|
||||
bodyClassName="px-4 pb-4 pt-5"
|
||||
>
|
||||
{/* Content */}
|
||||
<div className="sm:flex sm:items-start">
|
||||
{variant === 'danger' && (
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 dark:bg-red-900 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<svg className="h-6 w-6 text-red-600 dark:text-red-200" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
<div className="sm:flex sm:items-start">
|
||||
{variant === 'danger' && (
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 dark:bg-red-900 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<svg className="h-6 w-6 text-red-600 dark:text-red-200" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h3 className="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
{confirmText && (
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${confirmButtonClass}`}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
)}
|
||||
{cancelText && (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white dark:bg-gray-700 px-3 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 focus:ring-2 focus:ring-primary-500 sm:mt-0 sm:w-auto"
|
||||
onClick={onClose}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
)}
|
||||
)}
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h3 className="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
{confirmText && (
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${confirmButtonClass}`}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
)}
|
||||
{cancelText && (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white dark:bg-gray-700 px-3 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 focus:ring-2 focus:ring-primary-500 sm:mt-0 sm:w-auto"
|
||||
onClick={onClose}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
export default Modal;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type ModalWrapperProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
/** Modal title (optional - if not provided, no header is shown) */
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
/** Optional max-width class (default: 'max-w-md') */
|
||||
maxWidth?: string;
|
||||
/** Whether to show the close button in header (default: true) */
|
||||
showCloseButton?: boolean;
|
||||
/** Optional footer actions */
|
||||
footer?: React.ReactNode;
|
||||
/** Whether to show header border (default: true) */
|
||||
showHeaderBorder?: boolean;
|
||||
/** Custom body padding class (default: 'px-6 py-4') */
|
||||
bodyClassName?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A generic modal wrapper component that provides consistent behavior:
|
||||
* - Click outside to close (on backdrop)
|
||||
* - Escape key to close
|
||||
* - Dark overlay background
|
||||
* - Consistent styling and animations
|
||||
*/
|
||||
const ModalWrapper: React.FC<ModalWrapperProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
maxWidth = 'max-w-md',
|
||||
showCloseButton = true,
|
||||
footer,
|
||||
showHeaderBorder = true,
|
||||
bodyClassName = 'px-6 py-4'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
* Handle escape key press to close modal.
|
||||
*/
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent): void => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
}, [onClose]);
|
||||
|
||||
/**
|
||||
* Add/remove escape key listener when modal opens/closes.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return (): void => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}
|
||||
}, [isOpen, handleKeyDown]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click on the container (outside modal content) to close.
|
||||
*/
|
||||
const handleContainerClick = (e: React.MouseEvent<HTMLDivElement>): void => {
|
||||
// Only close if clicking directly on the container, not the modal content
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const hasHeader = title || showCloseButton;
|
||||
|
||||
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 container - clicking here (outside modal content) also closes */}
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center p-4"
|
||||
onClick={handleContainerClick}
|
||||
>
|
||||
<div className={`relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow-xl transition-all w-full ${maxWidth} mx-4`}>
|
||||
{/* Header */}
|
||||
{hasHeader && (
|
||||
<div className={`px-6 py-4 flex items-center justify-between ${showHeaderBorder ? 'border-b border-gray-200 dark:border-gray-700' : ''}`}>
|
||||
{title && (
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{!title && <div />}
|
||||
{showCloseButton && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
onClick={onClose}
|
||||
>
|
||||
<span className="sr-only">{t('common.close')}</span>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body */}
|
||||
<div className={bodyClassName}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{footer && (
|
||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalWrapper;
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Button from '../Button';
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
|
||||
|
||||
type PasskeyBypassDialogProps = {
|
||||
isOpen: boolean;
|
||||
origin: string;
|
||||
onChoice: (choice: 'once' | 'always') => void;
|
||||
onCancel: () => void;
|
||||
@@ -13,6 +15,7 @@ type PasskeyBypassDialogProps = {
|
||||
* Dialog for choosing how to bypass AliasVault passkey provider
|
||||
*/
|
||||
const PasskeyBypassDialog: React.FC<PasskeyBypassDialogProps> = ({
|
||||
isOpen,
|
||||
origin,
|
||||
onChoice,
|
||||
onCancel
|
||||
@@ -20,40 +23,39 @@ const PasskeyBypassDialog: React.FC<PasskeyBypassDialogProps> = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{t('passkeys.bypass.title')}
|
||||
</h2>
|
||||
<ModalWrapper
|
||||
isOpen={isOpen}
|
||||
onClose={onCancel}
|
||||
title={t('passkeys.bypass.title')}
|
||||
showCloseButton={true}
|
||||
>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
{t('passkeys.bypass.description', { origin })}
|
||||
</p>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
{t('passkeys.bypass.description', { origin })}
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => onChoice('once')}
|
||||
>
|
||||
{t('passkeys.bypass.thisTimeOnly')}
|
||||
</Button>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => onChoice('once')}
|
||||
>
|
||||
{t('passkeys.bypass.thisTimeOnly')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => onChoice('always')}
|
||||
>
|
||||
{t('passkeys.bypass.alwaysForSite')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => onChoice('always')}
|
||||
>
|
||||
{t('passkeys.bypass.alwaysForSite')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
|
||||
|
||||
import type { PasswordSettings } from '@/utils/dist/core/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/core/password-generator';
|
||||
|
||||
@@ -61,158 +63,127 @@ const PasswordConfigDialog: React.FC<IPasswordConfigDialogProps> = ({
|
||||
onClose();
|
||||
}, [previewPassword, onSave, onClose]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 bg-black bg-opacity-60 transition-opacity" onClick={handleCancel} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<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-lg">
|
||||
{/* Close button */}
|
||||
<ModalWrapper
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={t('credentials.changePasswordComplexity')}
|
||||
maxWidth="max-w-lg"
|
||||
footer={
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-4 top-4 text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
onClick={handleCancel}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-gray-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500"
|
||||
onClick={handleSave}
|
||||
>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 13l-3 3m0 0l-3-3m3 3V8m0 13a9 9 0 110-18 9 9 0 010 18z" />
|
||||
</svg>
|
||||
{t('common.use')}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Password Preview */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={previewPassword}
|
||||
readOnly
|
||||
className="flex-1 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:text-white font-mono"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefreshPreview}
|
||||
className="px-3 py-2 text-sm text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
|
||||
title={t('credentials.generateNewPreview')}
|
||||
>
|
||||
<svg className="w-4 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal content */}
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="w-full mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-white mb-4">
|
||||
{t('credentials.changePasswordComplexity')}
|
||||
</h3>
|
||||
{/* Character Type Toggle Buttons */}
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* Lowercase Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSettingChange('UseLowercase', !settings.UseLowercase)}
|
||||
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
settings.UseLowercase
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={t('credentials.includeLowercase')}
|
||||
>
|
||||
<span className="font-mono text-base">a-z</span>
|
||||
</button>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Password Preview */}
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={previewPassword}
|
||||
readOnly
|
||||
className="flex-1 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:text-white font-mono"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRefreshPreview}
|
||||
className="px-3 py-2 text-sm text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
|
||||
title={t('credentials.generateNewPreview')}
|
||||
>
|
||||
<svg className="w-4 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Uppercase Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSettingChange('UseUppercase', !settings.UseUppercase)}
|
||||
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
settings.UseUppercase
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={t('credentials.includeUppercase')}
|
||||
>
|
||||
<span className="font-mono text-base">A-Z</span>
|
||||
</button>
|
||||
|
||||
{/* Character Type Toggle Buttons */}
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* Lowercase Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSettingChange('UseLowercase', !settings.UseLowercase)}
|
||||
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
settings.UseLowercase
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={t('credentials.includeLowercase')}
|
||||
>
|
||||
<span className="font-mono text-base">a-z</span>
|
||||
</button>
|
||||
{/* Numbers Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSettingChange('UseNumbers', !settings.UseNumbers)}
|
||||
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
settings.UseNumbers
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={t('credentials.includeNumbers')}
|
||||
>
|
||||
<span className="font-mono text-base">0-9</span>
|
||||
</button>
|
||||
|
||||
{/* Uppercase Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSettingChange('UseUppercase', !settings.UseUppercase)}
|
||||
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
settings.UseUppercase
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={t('credentials.includeUppercase')}
|
||||
>
|
||||
<span className="font-mono text-base">A-Z</span>
|
||||
</button>
|
||||
{/* Special Characters Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSettingChange('UseSpecialChars', !settings.UseSpecialChars)}
|
||||
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
settings.UseSpecialChars
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={t('credentials.includeSpecialChars')}
|
||||
>
|
||||
<span className="font-mono text-base">!@#</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Numbers Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSettingChange('UseNumbers', !settings.UseNumbers)}
|
||||
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
settings.UseNumbers
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={t('credentials.includeNumbers')}
|
||||
>
|
||||
<span className="font-mono text-base">0-9</span>
|
||||
</button>
|
||||
|
||||
{/* Special Characters Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSettingChange('UseSpecialChars', !settings.UseSpecialChars)}
|
||||
className={`flex items-center justify-center px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
settings.UseSpecialChars
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title={t('credentials.includeSpecialChars')}
|
||||
>
|
||||
<span className="font-mono text-base">!@#</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Avoid Ambiguous Characters - Checkbox */}
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="use-non-ambiguous"
|
||||
type="checkbox"
|
||||
checked={settings.UseNonAmbiguousChars}
|
||||
onChange={(e) => handleSettingChange('UseNonAmbiguousChars', e.target.checked)}
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label htmlFor="use-non-ambiguous" className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('credentials.avoidAmbiguousChars')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="mt-5 sm:mt-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center items-center gap-1 rounded-md bg-gray-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 sm:ml-3 sm:w-auto"
|
||||
onClick={handleSave}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 13l-3 3m0 0l-3-3m3 3V8m0 13a9 9 0 110-18 9 9 0 010 18z" />
|
||||
</svg>
|
||||
{t('common.use')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Avoid Ambiguous Characters - Checkbox */}
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="use-non-ambiguous"
|
||||
type="checkbox"
|
||||
checked={settings.UseNonAmbiguousChars}
|
||||
onChange={(e) => handleSettingChange('UseNonAmbiguousChars', e.target.checked)}
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<label htmlFor="use-non-ambiguous" className="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('credentials.avoidAmbiguousChars')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordConfigDialog;
|
||||
export default PasswordConfigDialog;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
|
||||
|
||||
type DeleteFolderModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -55,112 +57,99 @@ const DeleteFolderModal: React.FC<DeleteFolderModalProps> = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle escape key press
|
||||
* Handle close - only allow if not submitting
|
||||
*/
|
||||
const handleKeyDown = (e: React.KeyboardEvent): void => {
|
||||
if (e.key === 'Escape' && !isSubmitting) {
|
||||
const handleClose = (): void => {
|
||||
if (!isSubmitting) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{t('items.deleteFolder')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
{t('items.deleteFolderConfirm', { folderName })}
|
||||
</p>
|
||||
|
||||
{itemCount > 0 && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('items.folderContainsItems', { count: itemCount })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Option buttons */}
|
||||
<div className="space-y-3 pt-2">
|
||||
{/* Delete folder only - move items to root */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteFolderOnly}
|
||||
disabled={isSubmitting}
|
||||
className="w-full p-3 text-left border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 text-orange-500">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 14l2 2 4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{t('items.deleteFolderKeepItems')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('items.deleteFolderKeepItemsDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Delete folder and contents */}
|
||||
{itemCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteFolderAndContents}
|
||||
disabled={isSubmitting}
|
||||
className="w-full p-3 text-left border border-red-300 dark:border-red-700 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 text-red-500">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-red-600 dark:text-red-400">
|
||||
{t('items.deleteFolderAndItems')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('items.deleteFolderAndItemsDescription', { count: itemCount })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end">
|
||||
<ModalWrapper
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
title={t('items.deleteFolder')}
|
||||
footer={
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
onClick={handleClose}
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
{t('items.deleteFolderConfirm', { folderName })}
|
||||
</p>
|
||||
|
||||
{itemCount > 0 && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('items.folderContainsItems', { count: itemCount })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Option buttons */}
|
||||
<div className="space-y-3 pt-2">
|
||||
{/* Delete folder only - move items to root */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteFolderOnly}
|
||||
disabled={isSubmitting}
|
||||
className="w-full p-3 text-left border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 text-orange-500">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 14l2 2 4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{t('items.deleteFolderKeepItems')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('items.deleteFolderKeepItemsDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Delete folder and contents */}
|
||||
{itemCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteFolderAndContents}
|
||||
disabled={isSubmitting}
|
||||
className="w-full p-3 text-left border border-red-300 dark:border-red-700 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 text-red-500">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-red-600 dark:text-red-400">
|
||||
{t('items.deleteFolderAndItems')}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('items.deleteFolderAndItemsDescription', { count: itemCount })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
|
||||
|
||||
type FolderModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -57,81 +59,58 @@ const FolderModal: React.FC<FolderModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the escape key press.
|
||||
*/
|
||||
const handleKeyDown = (e: React.KeyboardEvent): void => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||
<div
|
||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{mode === 'create' ? t('items.createFolder') : t('items.editFolder')}
|
||||
</h2>
|
||||
<ModalWrapper
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={mode === 'create' ? t('items.createFolder') : t('items.editFolder')}
|
||||
footer={
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="folder-form"
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-orange-600 hover:bg-orange-700 rounded focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting
|
||||
? t('common.saving')
|
||||
: mode === 'create'
|
||||
? t('common.create')
|
||||
: t('common.save')
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="px-6 py-4">
|
||||
<label
|
||||
htmlFor="folderName"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
>
|
||||
{t('items.folderName')}
|
||||
</label>
|
||||
<input
|
||||
id="folderName"
|
||||
type="text"
|
||||
value={folderName}
|
||||
onChange={(e) => setFolderName(e.target.value)}
|
||||
placeholder={t('items.folderNamePlaceholder')}
|
||||
autoFocus
|
||||
className="w-full p-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
{error && (
|
||||
<p className="mt-2 text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-orange-600 hover:bg-orange-700 rounded focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting
|
||||
? t('common.saving')
|
||||
: mode === 'create'
|
||||
? t('common.create')
|
||||
: t('common.save')
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<form id="folder-form" onSubmit={handleSubmit}>
|
||||
<label
|
||||
htmlFor="folderName"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||
>
|
||||
{t('items.folderName')}
|
||||
</label>
|
||||
<input
|
||||
id="folderName"
|
||||
type="text"
|
||||
value={folderName}
|
||||
onChange={(e) => setFolderName(e.target.value)}
|
||||
placeholder={t('items.folderNamePlaceholder')}
|
||||
autoFocus
|
||||
className="w-full p-2 border dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
{error && (
|
||||
<p className="mt-2 text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
)}
|
||||
</form>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ModalWrapper from '@/entrypoints/popup/components/Dialogs/ModalWrapper';
|
||||
|
||||
import type { FieldType } from '@/utils/dist/core/models/vault';
|
||||
|
||||
/**
|
||||
@@ -204,8 +206,9 @@ const AddFieldMenu: React.FC<AddFieldMenuProps> = ({
|
||||
{/* Dropdown Menu */}
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Dark overlay backdrop for better visibility */}
|
||||
<div
|
||||
className="fixed inset-0 z-10"
|
||||
className="fixed inset-0 z-10 bg-black bg-opacity-50"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<div className="absolute bottom-full left-0 right-0 mb-1 z-20 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg overflow-hidden">
|
||||
@@ -239,69 +242,66 @@ const AddFieldMenu: React.FC<AddFieldMenuProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Custom Field Modal */}
|
||||
{showCustomFieldModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-96 max-w-full mx-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{t('itemTypes.addCustomField')}
|
||||
</h3>
|
||||
<ModalWrapper
|
||||
isOpen={showCustomFieldModal}
|
||||
onClose={handleCloseCustomFieldModal}
|
||||
title={t('itemTypes.addCustomField')}
|
||||
footer={
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddCustomField}
|
||||
disabled={!customFieldLabel.trim()}
|
||||
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{t('common.add')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCloseCustomFieldModal}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('itemTypes.fieldLabel')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customFieldLabel}
|
||||
onChange={(e) => setCustomFieldLabel(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder={t('itemTypes.enterFieldName')}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('itemTypes.fieldLabel')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customFieldLabel}
|
||||
onChange={(e) => setCustomFieldLabel(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white"
|
||||
placeholder={t('itemTypes.enterFieldName')}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('itemTypes.fieldType')}
|
||||
</label>
|
||||
<select
|
||||
value={customFieldType}
|
||||
onChange={(e) => setCustomFieldType(e.target.value as FieldType)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="Text">{t('itemTypes.fieldTypes.text')}</option>
|
||||
<option value="Hidden">{t('itemTypes.fieldTypes.hidden')}</option>
|
||||
<option value="Email">{t('itemTypes.fieldTypes.email')}</option>
|
||||
<option value="URL">{t('itemTypes.fieldTypes.url')}</option>
|
||||
<option value="Phone">{t('itemTypes.fieldTypes.phone')}</option>
|
||||
<option value="Number">{t('itemTypes.fieldTypes.number')}</option>
|
||||
<option value="Date">{t('itemTypes.fieldTypes.date')}</option>
|
||||
<option value="TextArea">{t('itemTypes.fieldTypes.textArea')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddCustomField}
|
||||
disabled={!customFieldLabel.trim()}
|
||||
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{t('common.add')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCloseCustomFieldModal}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('itemTypes.fieldType')}
|
||||
</label>
|
||||
<select
|
||||
value={customFieldType}
|
||||
onChange={(e) => setCustomFieldType(e.target.value as FieldType)}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="Text">{t('itemTypes.fieldTypes.text')}</option>
|
||||
<option value="Hidden">{t('itemTypes.fieldTypes.hidden')}</option>
|
||||
<option value="Email">{t('itemTypes.fieldTypes.email')}</option>
|
||||
<option value="URL">{t('itemTypes.fieldTypes.url')}</option>
|
||||
<option value="Phone">{t('itemTypes.fieldTypes.phone')}</option>
|
||||
<option value="Number">{t('itemTypes.fieldTypes.number')}</option>
|
||||
<option value="Date">{t('itemTypes.fieldTypes.date')}</option>
|
||||
<option value="TextArea">{t('itemTypes.fieldTypes.textArea')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ModalWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -661,8 +661,8 @@ const CredentialAddEdit: React.FC = () => {
|
||||
setShowDeleteModal(false);
|
||||
void handleDelete();
|
||||
}}
|
||||
title={t('credentials.deleteCredentialTitle')}
|
||||
message={t('credentials.deleteCredentialConfirm')}
|
||||
title={t('credentials.deleteItemTitle')}
|
||||
message={t('credentials.deleteItemConfirm')}
|
||||
confirmText={t('common.delete')}
|
||||
cancelText={t('common.cancel')}
|
||||
variant="danger"
|
||||
|
||||
@@ -1387,8 +1387,8 @@ const ItemAddEdit: React.FC = () => {
|
||||
<Modal
|
||||
isOpen={showDeleteModal}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
title={t('credentials.deleteCredentialTitle')}
|
||||
message={t('credentials.deleteCredentialConfirmation')}
|
||||
title={t('credentials.deleteItemTitle')}
|
||||
message={t('credentials.deleteItemConfirm')}
|
||||
confirmText={t('common.delete')}
|
||||
cancelText={t('common.cancel')}
|
||||
onConfirm={handleDelete}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ConfirmDeleteModal from '@/entrypoints/popup/components/Dialogs/ConfirmDeleteModal';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
@@ -70,19 +71,19 @@ const RecentlyDeleted: React.FC = () => {
|
||||
/**
|
||||
* Permanently delete an item.
|
||||
*/
|
||||
const handlePermanentDelete = useCallback(async (itemId: string) => {
|
||||
if (!dbContext?.sqliteClient) {
|
||||
const handlePermanentDelete = useCallback(async () => {
|
||||
if (!dbContext?.sqliteClient || !selectedItemId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await executeVaultMutationAsync(async () => {
|
||||
await dbContext.sqliteClient!.permanentlyDeleteItem(itemId);
|
||||
await dbContext.sqliteClient!.permanentlyDeleteItem(selectedItemId);
|
||||
});
|
||||
|
||||
loadItems();
|
||||
setShowConfirmDelete(false);
|
||||
setSelectedItemId(null);
|
||||
}, [dbContext?.sqliteClient, executeVaultMutationAsync, loadItems]);
|
||||
}, [dbContext?.sqliteClient, executeVaultMutationAsync, loadItems, selectedItemId]);
|
||||
|
||||
/**
|
||||
* Empty all items from Recently Deleted (permanent delete all).
|
||||
@@ -124,6 +125,14 @@ const RecentlyDeleted: React.FC = () => {
|
||||
load();
|
||||
}, [dbContext?.sqliteClient, setIsLoading, loadItems]);
|
||||
|
||||
/**
|
||||
* Handle closing the delete confirmation modal.
|
||||
*/
|
||||
const handleCloseDeleteModal = useCallback(() => {
|
||||
setShowConfirmDelete(false);
|
||||
setSelectedItemId(null);
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center p-8">
|
||||
@@ -221,63 +230,24 @@ const RecentlyDeleted: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Confirm Delete Modal */}
|
||||
{showConfirmDelete && selectedItemId && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm mx-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
{t('recentlyDeleted.confirmDeleteTitle')}
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
{t('recentlyDeleted.confirmDeleteMessage')}
|
||||
</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowConfirmDelete(false);
|
||||
setSelectedItemId(null);
|
||||
}}
|
||||
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePermanentDelete(selectedItemId)}
|
||||
className="px-4 py-2 text-sm bg-red-600 text-white rounded hover:bg-red-700"
|
||||
>
|
||||
{t('recentlyDeleted.deletePermanently')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ConfirmDeleteModal
|
||||
isOpen={showConfirmDelete && !!selectedItemId}
|
||||
onClose={handleCloseDeleteModal}
|
||||
onConfirm={handlePermanentDelete}
|
||||
title={t('recentlyDeleted.confirmDeleteTitle')}
|
||||
message={t('recentlyDeleted.confirmDeleteMessage')}
|
||||
confirmText={t('recentlyDeleted.deletePermanently')}
|
||||
/>
|
||||
|
||||
{/* Confirm Empty All Modal */}
|
||||
{showConfirmEmptyAll && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm mx-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
{t('recentlyDeleted.confirmEmptyAllTitle')}
|
||||
</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
{t('recentlyDeleted.confirmEmptyAllMessage', { count: items.length })}
|
||||
</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setShowConfirmEmptyAll(false)}
|
||||
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleEmptyAll}
|
||||
className="px-4 py-2 text-sm bg-red-600 text-white rounded hover:bg-red-700"
|
||||
>
|
||||
{t('recentlyDeleted.emptyAll')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ConfirmDeleteModal
|
||||
isOpen={showConfirmEmptyAll}
|
||||
onClose={() => setShowConfirmEmptyAll(false)}
|
||||
onConfirm={handleEmptyAll}
|
||||
title={t('recentlyDeleted.confirmEmptyAllTitle')}
|
||||
message={t('recentlyDeleted.confirmEmptyAllMessage', { count: items.length })}
|
||||
confirmText={t('recentlyDeleted.emptyAll')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -365,13 +365,12 @@ const PasskeyAuthenticate: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{showBypassDialog && request && (
|
||||
<PasskeyBypassDialog
|
||||
origin={new URL(request.origin).hostname}
|
||||
onChoice={handleBypassChoice}
|
||||
onCancel={() => setShowBypassDialog(false)}
|
||||
/>
|
||||
)}
|
||||
<PasskeyBypassDialog
|
||||
isOpen={showBypassDialog && !!request}
|
||||
origin={request ? new URL(request.origin).hostname : ''}
|
||||
onChoice={handleBypassChoice}
|
||||
onCancel={() => setShowBypassDialog(false)}
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
|
||||
@@ -546,13 +546,12 @@ const PasskeyCreate: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{showBypassDialog && request && (
|
||||
<PasskeyBypassDialog
|
||||
origin={new URL(request.origin).hostname}
|
||||
onChoice={handleBypassChoice}
|
||||
onCancel={() => setShowBypassDialog(false)}
|
||||
/>
|
||||
)}
|
||||
<PasskeyBypassDialog
|
||||
isOpen={showBypassDialog && !!request}
|
||||
origin={request ? new URL(request.origin).hostname : ''}
|
||||
onChoice={handleBypassChoice}
|
||||
onCancel={() => setShowBypassDialog(false)}
|
||||
/>
|
||||
|
||||
{localLoading && (
|
||||
<div className="fixed inset-0 flex flex-col justify-center items-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90 z-50">
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
"title": "Credentials",
|
||||
"addCredential": "Add Credential",
|
||||
"editCredential": "Edit Credential",
|
||||
"deleteCredential": "Delete Credential",
|
||||
"deleteCredential": "Delete Item",
|
||||
"credentialDetails": "Credential Details",
|
||||
"serviceName": "Service Name",
|
||||
"itemName": "Name",
|
||||
@@ -208,9 +208,8 @@
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Last updated",
|
||||
"saveCredential": "Save credential",
|
||||
"deleteCredentialTitle": "Delete Credential",
|
||||
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
|
||||
"deleteCredentialConfirmation": "Are you sure you want to delete this item? This action cannot be undone.",
|
||||
"deleteItemTitle": "Delete Item",
|
||||
"deleteItemConfirm": "Are you sure you want to delete this item? This action cannot be undone.",
|
||||
"filters": {
|
||||
"all": "(All) Credentials",
|
||||
"passkeys": "Passkeys",
|
||||
|
||||
Reference in New Issue
Block a user