From 87b1d495443ef8fd49947be95b30374cf3e6a342 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 18 Apr 2025 13:49:10 +0200 Subject: [PATCH] Refactor credential detail component structure (#771) --- .../CredentialDetails/AliasBlock.tsx | 65 +++++ .../CredentialDetails/EmailBlock.tsx | 18 ++ .../CredentialDetails/HeaderBlock.tsx | 60 +++++ .../LoginCredentialsBlock.tsx | 50 ++++ .../CredentialDetails/NotesBlock.tsx | 42 ++++ .../CredentialDetails/TotpBlock.tsx | 17 ++ .../components/CredentialDetails/index.tsx | 15 ++ .../popup/pages/CredentialDetails.tsx | 238 ++---------------- 8 files changed, 285 insertions(+), 220 deletions(-) create mode 100644 browser-extension/src/entrypoints/popup/components/CredentialDetails/AliasBlock.tsx create mode 100644 browser-extension/src/entrypoints/popup/components/CredentialDetails/EmailBlock.tsx create mode 100644 browser-extension/src/entrypoints/popup/components/CredentialDetails/HeaderBlock.tsx create mode 100644 browser-extension/src/entrypoints/popup/components/CredentialDetails/LoginCredentialsBlock.tsx create mode 100644 browser-extension/src/entrypoints/popup/components/CredentialDetails/NotesBlock.tsx create mode 100644 browser-extension/src/entrypoints/popup/components/CredentialDetails/TotpBlock.tsx create mode 100644 browser-extension/src/entrypoints/popup/components/CredentialDetails/index.tsx diff --git a/browser-extension/src/entrypoints/popup/components/CredentialDetails/AliasBlock.tsx b/browser-extension/src/entrypoints/popup/components/CredentialDetails/AliasBlock.tsx new file mode 100644 index 000000000..2b65c2365 --- /dev/null +++ b/browser-extension/src/entrypoints/popup/components/CredentialDetails/AliasBlock.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Credential } from '../../../../utils/types/Credential'; +import { FormInputCopyToClipboard } from '../../components/FormInputCopyToClipboard'; + +type AliasBlockProps = { + credential: Credential; + isValidDate: (date: string | null | undefined) => boolean; +} + +/** + * Render the alias block. + */ +const AliasBlock: React.FC = ({ credential, isValidDate }) => { + const hasFirstName = Boolean(credential.Alias?.FirstName?.trim()); + const hasLastName = Boolean(credential.Alias?.LastName?.trim()); + const hasNickName = Boolean(credential.Alias?.NickName?.trim()); + const hasBirthDate = isValidDate(credential.Alias?.BirthDate); + + if (!hasFirstName && !hasLastName && !hasNickName && !hasBirthDate) { + return null; + } + + return ( +
+

Alias

+ {(hasFirstName || hasLastName) && ( + + )} + {hasFirstName && ( + + )} + {hasLastName && ( + + )} + {hasBirthDate && ( + + )} + {hasNickName && ( + + )} +
+ ); +}; + +export default AliasBlock; \ No newline at end of file diff --git a/browser-extension/src/entrypoints/popup/components/CredentialDetails/EmailBlock.tsx b/browser-extension/src/entrypoints/popup/components/CredentialDetails/EmailBlock.tsx new file mode 100644 index 000000000..3c7d7b320 --- /dev/null +++ b/browser-extension/src/entrypoints/popup/components/CredentialDetails/EmailBlock.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { EmailPreview } from '../../components/EmailPreview'; + +type EmailBlockProps = { + email: string; + isSupported: boolean; +} + +/** + * Render the email block. + */ +const EmailBlock: React.FC = ({ email, isSupported }) => ( + <> + {isSupported && } + +); + +export default EmailBlock; \ No newline at end of file diff --git a/browser-extension/src/entrypoints/popup/components/CredentialDetails/HeaderBlock.tsx b/browser-extension/src/entrypoints/popup/components/CredentialDetails/HeaderBlock.tsx new file mode 100644 index 000000000..1e23a3eb7 --- /dev/null +++ b/browser-extension/src/entrypoints/popup/components/CredentialDetails/HeaderBlock.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { Credential } from '../../../../utils/types/Credential'; +import SqliteClient from '../../../../utils/SqliteClient'; + +type HeaderBlockProps = { + credential: Credential; + onOpenNewPopup: () => void; +} + +/** + * Render the header block. + */ +const HeaderBlock: React.FC = ({ credential, onOpenNewPopup }) => ( +
+
+
+ {credential.ServiceName} +
+

{credential.ServiceName}

+ {credential.ServiceUrl && ( + + {credential.ServiceUrl} + + )} +
+
+ +
+
+); + +export default HeaderBlock; \ No newline at end of file diff --git a/browser-extension/src/entrypoints/popup/components/CredentialDetails/LoginCredentialsBlock.tsx b/browser-extension/src/entrypoints/popup/components/CredentialDetails/LoginCredentialsBlock.tsx new file mode 100644 index 000000000..598538a81 --- /dev/null +++ b/browser-extension/src/entrypoints/popup/components/CredentialDetails/LoginCredentialsBlock.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Credential } from '../../../../utils/types/Credential'; +import { FormInputCopyToClipboard } from '../../components/FormInputCopyToClipboard'; + +type LoginCredentialsBlockProps = { + credential: Credential; +} + +/** + * Render the login credentials block. + */ +const LoginCredentialsBlock: React.FC = ({ credential }) => { + const email = credential.Alias?.Email?.trim(); + const username = credential.Username?.trim(); + const password = credential.Password?.trim(); + + if (!email && !username && !password) { + return null; + } + + return ( +
+

Login credentials

+ {email && ( + + )} + {username && ( + + )} + {password && ( + + )} +
+ ); +}; + +export default LoginCredentialsBlock; \ No newline at end of file diff --git a/browser-extension/src/entrypoints/popup/components/CredentialDetails/NotesBlock.tsx b/browser-extension/src/entrypoints/popup/components/CredentialDetails/NotesBlock.tsx new file mode 100644 index 000000000..9b51d712c --- /dev/null +++ b/browser-extension/src/entrypoints/popup/components/CredentialDetails/NotesBlock.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +type NotesBlockProps = { + notes: string | undefined; +} + +/** + * Convert URLs in text to clickable links. + */ +const convertUrlsToLinks = (text: string): string => { + const urlPattern = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/g; + + return text.replace(urlPattern, (url) => { + const href = url.startsWith('http') ? url : `http://${url}`; + return `${url}`; + }); +}; + +/** + * Render the notes block. + */ +const NotesBlock: React.FC = ({ notes }) => { + if (!notes) { + return null; + } + + const formattedNotes = convertUrlsToLinks(notes); + + return ( +
+

Notes

+
+

+

+
+ ); +}; + +export default NotesBlock; \ No newline at end of file diff --git a/browser-extension/src/entrypoints/popup/components/CredentialDetails/TotpBlock.tsx b/browser-extension/src/entrypoints/popup/components/CredentialDetails/TotpBlock.tsx new file mode 100644 index 000000000..4baedc9e5 --- /dev/null +++ b/browser-extension/src/entrypoints/popup/components/CredentialDetails/TotpBlock.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { TotpViewer } from '../../components/TotpViewer'; + +type TotpBlockProps = { + credentialId: string; +} + +/** + * Render the TOTP viewer block. + */ +const TotpBlock: React.FC = ({ credentialId }) => ( + <> + + +); + +export default TotpBlock; \ No newline at end of file diff --git a/browser-extension/src/entrypoints/popup/components/CredentialDetails/index.tsx b/browser-extension/src/entrypoints/popup/components/CredentialDetails/index.tsx new file mode 100644 index 000000000..32ed2cc8d --- /dev/null +++ b/browser-extension/src/entrypoints/popup/components/CredentialDetails/index.tsx @@ -0,0 +1,15 @@ +import HeaderBlock from './HeaderBlock'; +import EmailBlock from './EmailBlock'; +import TotpBlock from './TotpBlock'; +import LoginCredentialsBlock from './LoginCredentialsBlock'; +import AliasBlock from './AliasBlock'; +import NotesBlock from './NotesBlock'; + +export { + HeaderBlock, + EmailBlock, + TotpBlock, + LoginCredentialsBlock, + AliasBlock, + NotesBlock +}; \ No newline at end of file diff --git a/browser-extension/src/entrypoints/popup/pages/CredentialDetails.tsx b/browser-extension/src/entrypoints/popup/pages/CredentialDetails.tsx index 9f32a1749..4d2d6ac6a 100644 --- a/browser-extension/src/entrypoints/popup/pages/CredentialDetails.tsx +++ b/browser-extension/src/entrypoints/popup/pages/CredentialDetails.tsx @@ -2,212 +2,15 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useDb } from '../context/DbContext'; import { Credential } from '../../../utils/types/Credential'; -import { FormInputCopyToClipboard } from '../components/FormInputCopyToClipboard'; -import { EmailPreview } from '../components/EmailPreview'; -import { TotpViewer } from '../components/TotpViewer'; import { useLoading } from '../context/LoadingContext'; -import SqliteClient from '../../../utils/SqliteClient'; - -type BlockProps = { - children: React.ReactNode; - className?: string; -} - -/** - * Render a block. - */ -const Block: React.FC = ({ children, className = '' }) => ( -
- {children} -
-); - -/** - * Render the header block. - */ -const HeaderBlock: React.FC<{ credential: Credential; onOpenNewPopup: () => void }> = ({ credential, onOpenNewPopup }) => ( - -
-
- {credential.ServiceName} -
-

{credential.ServiceName}

- {credential.ServiceUrl && ( - - {credential.ServiceUrl} - - )} -
-
- -
-
-); - -/** - * Render the email block. - */ -const EmailBlock: React.FC<{ email: string; isSupported: boolean }> = ({ email, isSupported }) => ( - - {isSupported && } - -); - -/** - * Render the TOTP viewer block. - */ -const TotpBlock: React.FC<{ credentialId: string }> = ({ credentialId }) => ( - - - -); - -/** - * Render the login credentials block. - */ -const LoginCredentialsBlock: React.FC<{ credential: Credential }> = ({ credential }) => { - const email = credential.Alias?.Email?.trim(); - const username = credential.Username?.trim(); - const password = credential.Password?.trim(); - - if (!email && !username && !password) { - return null; - } - - return ( - -

Login credentials

- {email && ( - - )} - {username && ( - - )} - {password && ( - - )} -
- ); -}; - -/** - * Render the alias block. - */ -const AliasBlock: React.FC<{ credential: Credential; isValidDate: (date: string | null | undefined) => boolean }> = ({ - credential, - isValidDate -}) => { - const hasFirstName = Boolean(credential.Alias?.FirstName?.trim()); - const hasLastName = Boolean(credential.Alias?.LastName?.trim()); - const hasNickName = Boolean(credential.Alias?.NickName?.trim()); - const hasBirthDate = isValidDate(credential.Alias?.BirthDate); - - if (!hasFirstName && !hasLastName && !hasNickName && !hasBirthDate) { - return null; - } - - return ( - -

Alias

- {(hasFirstName || hasLastName) && ( - - )} - {hasFirstName && ( - - )} - {hasLastName && ( - - )} - {hasBirthDate && ( - - )} - {hasNickName && ( - - )} -
- ); -}; - -/** - * Render the notes block. - */ -const NotesBlock: React.FC<{ notes: string | undefined }> = ({ notes }) => { - if (!notes) { - return null; - } - - return ( - -

Notes

-
-

- {notes} -

-
-
- ); -}; +import { + HeaderBlock, + EmailBlock, + TotpBlock, + LoginCredentialsBlock, + AliasBlock, + NotesBlock +} from '../components/CredentialDetails'; /** * Credential details page. @@ -302,26 +105,21 @@ const CredentialDetails: React.FC = () => { } return ( -
+
- {credential.Alias?.Email && ( - )} - - - - - - - + + +
); };