mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 15:55:26 +02:00
improved AliasNotesForm.tsx
This commit is contained in:
parent
b376177dc2
commit
f4f8eceabe
@ -21,6 +21,7 @@
|
|||||||
"camelcase-keys": "^8.0.2",
|
"camelcase-keys": "^8.0.2",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"deep-equal": "^2.0.5",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"group-array": "^1.0.0",
|
"group-array": "^1.0.0",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
@ -41,6 +42,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/date-fns": "^2.6.0",
|
"@types/date-fns": "^2.6.0",
|
||||||
|
"@types/deep-equal": "^1.0.1",
|
||||||
"@types/group-array": "^1.0.1",
|
"@types/group-array": "^1.0.1",
|
||||||
"@types/openpgp": "^4.4.18",
|
"@types/openpgp": "^4.4.18",
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
import React, {ReactElement, useState} from "react"
|
|
||||||
|
|
||||||
export interface BackupImageProps
|
|
||||||
extends Omit<
|
|
||||||
React.DetailedHTMLProps<
|
|
||||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
|
||||||
HTMLImageElement
|
|
||||||
>,
|
|
||||||
"src"
|
|
||||||
> {
|
|
||||||
fallbackSrc: string
|
|
||||||
src: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BackupImage({
|
|
||||||
fallbackSrc,
|
|
||||||
src,
|
|
||||||
...props
|
|
||||||
}: BackupImageProps): ReactElement {
|
|
||||||
const [source, setSource] = useState<string>(src)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img {...props} src={source} onError={() => setSource(fallbackSrc)} />
|
|
||||||
)
|
|
||||||
}
|
|
38
src/components/FaviconImage.tsx
Normal file
38
src/components/FaviconImage.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React, {ReactElement, useState} from "react"
|
||||||
|
|
||||||
|
export interface FaviconImageProps
|
||||||
|
extends Omit<
|
||||||
|
React.DetailedHTMLProps<
|
||||||
|
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||||
|
HTMLImageElement
|
||||||
|
>,
|
||||||
|
"src"
|
||||||
|
> {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDomain = (url: string): string => {
|
||||||
|
const {hostname, port} = new URL(url)
|
||||||
|
return `${hostname}${port ? `:${port}` : ""}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FaviconImage({
|
||||||
|
url,
|
||||||
|
...props
|
||||||
|
}: FaviconImageProps): ReactElement {
|
||||||
|
const [source, setSource] = useState<string>(`${url}/favicon.ico`)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
{...props}
|
||||||
|
src={source}
|
||||||
|
onError={() =>
|
||||||
|
setSource(
|
||||||
|
`https://external-content.duckduckgo.com/ip3/${getDomain(
|
||||||
|
url,
|
||||||
|
)}.ico`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -28,5 +28,5 @@ export * from "./AliasTypeIndicator"
|
|||||||
export {default as AliasTypeIndicator} from "./AliasTypeIndicator"
|
export {default as AliasTypeIndicator} from "./AliasTypeIndicator"
|
||||||
export * from "./DecryptionPasswordMissingAlert"
|
export * from "./DecryptionPasswordMissingAlert"
|
||||||
export {default as DecryptionPasswordMissingAlert} from "./DecryptionPasswordMissingAlert"
|
export {default as DecryptionPasswordMissingAlert} from "./DecryptionPasswordMissingAlert"
|
||||||
export * from "./BackupImage"
|
export * from "./FaviconImage"
|
||||||
export {default as BackupImage} from "./BackupImage"
|
export {default as FaviconImage} from "./FaviconImage"
|
||||||
|
114
src/route-widgets/AliasDetailRoute/AddWebsiteField.tsx
Normal file
114
src/route-widgets/AliasDetailRoute/AddWebsiteField.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import * as yup from "yup"
|
||||||
|
import {useFormik} from "formik"
|
||||||
|
import {ReactElement} from "react"
|
||||||
|
import {RiLinkM} from "react-icons/ri"
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormGroup,
|
||||||
|
FormHelperText,
|
||||||
|
Grid,
|
||||||
|
InputAdornment,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material"
|
||||||
|
|
||||||
|
import {URL_REGEX} from "~/constants/values"
|
||||||
|
import {whenEnterPressed} from "~/utils"
|
||||||
|
|
||||||
|
export interface AddWebsiteFieldProps {
|
||||||
|
onAdd: (website: string) => Promise<void>
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebsiteForm {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const WEBSITE_SCHEMA = yup.object().shape({
|
||||||
|
url: yup.string().matches(URL_REGEX, "This URL is invalid."),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function AddWebsiteField({
|
||||||
|
onAdd,
|
||||||
|
isLoading,
|
||||||
|
}: AddWebsiteFieldProps): ReactElement {
|
||||||
|
const websiteFormik = useFormik<WebsiteForm>({
|
||||||
|
validationSchema: WEBSITE_SCHEMA,
|
||||||
|
initialValues: {
|
||||||
|
url: "",
|
||||||
|
},
|
||||||
|
onSubmit: async values => {
|
||||||
|
const url = (() => {
|
||||||
|
// Make sure url starts with `http://` or `https://`
|
||||||
|
if (values.url.startsWith("http://")) {
|
||||||
|
return values.url
|
||||||
|
}
|
||||||
|
if (values.url.startsWith("https://")) {
|
||||||
|
return values.url
|
||||||
|
}
|
||||||
|
return `https://${values.url}`
|
||||||
|
})()
|
||||||
|
|
||||||
|
const {hostname, protocol, port} = new URL(url)
|
||||||
|
const baseUrl = `${protocol}//${hostname}${port ? `:${port}` : ""}`
|
||||||
|
|
||||||
|
await onAdd(baseUrl)
|
||||||
|
websiteFormik.resetForm()
|
||||||
|
},
|
||||||
|
validateOnChange: true,
|
||||||
|
validateOnBlur: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={2} direction="column">
|
||||||
|
<Grid item>
|
||||||
|
<FormGroup row>
|
||||||
|
<TextField
|
||||||
|
name="url"
|
||||||
|
id="url"
|
||||||
|
label="Website"
|
||||||
|
variant="outlined"
|
||||||
|
value={websiteFormik.values.url}
|
||||||
|
onChange={websiteFormik.handleChange}
|
||||||
|
onBlur={websiteFormik.handleBlur}
|
||||||
|
onKeyDown={whenEnterPressed(() =>
|
||||||
|
websiteFormik.handleSubmit(),
|
||||||
|
)}
|
||||||
|
disabled={websiteFormik.isSubmitting || isLoading}
|
||||||
|
error={
|
||||||
|
websiteFormik.touched.url &&
|
||||||
|
Boolean(websiteFormik.errors.url)
|
||||||
|
}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<RiLinkM />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
disableElevation
|
||||||
|
disabled={websiteFormik.isSubmitting || isLoading}
|
||||||
|
onClick={() => websiteFormik.handleSubmit()}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</FormGroup>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<FormHelperText
|
||||||
|
error={
|
||||||
|
websiteFormik.touched.url &&
|
||||||
|
Boolean(websiteFormik.errors.url)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(websiteFormik.touched.url && websiteFormik.errors.url) ||
|
||||||
|
"Add a website to this alias. Used to autofill."}
|
||||||
|
</FormHelperText>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
@ -15,6 +15,11 @@ export interface AliasDetailsProps {
|
|||||||
alias: Alias | DecryptedAlias
|
alias: Alias | DecryptedAlias
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDomain = (url: string): string => {
|
||||||
|
const {hostname, port} = new URL(url)
|
||||||
|
return `${hostname}${port ? `:${port}` : ""}`
|
||||||
|
}
|
||||||
|
|
||||||
export default function AliasDetails({
|
export default function AliasDetails({
|
||||||
alias: aliasValue,
|
alias: aliasValue,
|
||||||
}: AliasDetailsProps): ReactElement {
|
}: AliasDetailsProps): ReactElement {
|
||||||
@ -46,13 +51,6 @@ export default function AliasDetails({
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item width="100%">
|
<Grid item width="100%">
|
||||||
<Grid container direction="column" spacing={4}>
|
|
||||||
<Grid item>
|
|
||||||
<Typography variant="h6" component="h3">
|
|
||||||
Notes
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
{encryptionStatus === EncryptionStatus.Available ? (
|
{encryptionStatus === EncryptionStatus.Available ? (
|
||||||
<AliasNotesForm
|
<AliasNotesForm
|
||||||
id={aliasUIState.id}
|
id={aliasUIState.id}
|
||||||
@ -63,8 +61,6 @@ export default function AliasDetails({
|
|||||||
<DecryptionPasswordMissingAlert />
|
<DecryptionPasswordMissingAlert />
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Grid container spacing={4}>
|
<Grid container spacing={4}>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -72,24 +68,23 @@ export default function AliasDetails({
|
|||||||
Settings
|
Settings
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
|
||||||
<Typography variant="body1">
|
|
||||||
These settings apply to this alias only. You can
|
|
||||||
either set a value manually or refer to your
|
|
||||||
defaults settings. Note that this does change in
|
|
||||||
behavior. When you set a value to refer to your
|
|
||||||
default setting, the alias will always use the
|
|
||||||
latest value. So when you change your default
|
|
||||||
setting, the alias will automatically use the new
|
|
||||||
value.
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<AliasPreferencesForm
|
<AliasPreferencesForm
|
||||||
alias={aliasUIState}
|
alias={aliasUIState}
|
||||||
onChanged={setAliasUIState}
|
onChanged={setAliasUIState}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="body2">
|
||||||
|
These settings apply to this alias only. You can
|
||||||
|
either set a value manually or refer to your default
|
||||||
|
settings. Note that this does change in behavior.
|
||||||
|
When you set a value to refer to your default
|
||||||
|
setting, the alias will always use the latest value.
|
||||||
|
So when you change your default setting, the alias
|
||||||
|
will automatically use the new value.
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import * as yup from "yup"
|
import * as yup from "yup"
|
||||||
import {TiDelete} from "react-icons/ti"
|
import {TiDelete} from "react-icons/ti"
|
||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
import {ReactElement, useContext} from "react"
|
import {ReactElement, useContext, useMemo, useState} from "react"
|
||||||
import {MdEditCalendar} from "react-icons/md"
|
import {MdCheckCircle, MdEditCalendar} from "react-icons/md"
|
||||||
import {RiLinkM, RiStickyNoteFill} from "react-icons/ri"
|
import {RiStickyNoteFill} from "react-icons/ri"
|
||||||
import {FieldArray, FormikProvider, useFormik} from "formik"
|
import {FieldArray, FormikProvider, useFormik} from "formik"
|
||||||
|
import {FaPen} from "react-icons/fa"
|
||||||
|
import deepEqual from "deep-equal"
|
||||||
import format from "date-fns/format"
|
import format from "date-fns/format"
|
||||||
import update from "immutability-helper"
|
import update from "immutability-helper"
|
||||||
|
|
||||||
import {useMutation} from "@tanstack/react-query"
|
import {useMutation} from "@tanstack/react-query"
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
FormGroup,
|
|
||||||
FormHelperText,
|
|
||||||
Grid,
|
Grid,
|
||||||
IconButton,
|
IconButton,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
@ -26,11 +25,11 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material"
|
} from "@mui/material"
|
||||||
|
|
||||||
import {URL_REGEX} from "~/constants/values"
|
import {parseFastAPIError} from "~/utils"
|
||||||
import {parseFastAPIError, whenEnterPressed} from "~/utils"
|
import {ErrorSnack, FaviconImage, SuccessSnack} from "~/components"
|
||||||
import {BackupImage, ErrorSnack, SuccessSnack} from "~/components"
|
|
||||||
import {Alias, AliasNote, DecryptedAlias} from "~/server-types"
|
import {Alias, AliasNote, DecryptedAlias} from "~/server-types"
|
||||||
import {UpdateAliasData, updateAlias} from "~/apis"
|
import {UpdateAliasData, updateAlias} from "~/apis"
|
||||||
|
import AddWebsiteField from "~/route-widgets/AliasDetailRoute/AddWebsiteField"
|
||||||
import AuthContext from "~/AuthContext/AuthContext"
|
import AuthContext from "~/AuthContext/AuthContext"
|
||||||
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
|
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
|
||||||
|
|
||||||
@ -48,10 +47,6 @@ interface Form {
|
|||||||
detail?: string
|
detail?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebsiteForm {
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const SCHEMA = yup.object().shape({
|
const SCHEMA = yup.object().shape({
|
||||||
personalNotes: yup.string(),
|
personalNotes: yup.string(),
|
||||||
websites: yup.array().of(
|
websites: yup.array().of(
|
||||||
@ -61,14 +56,6 @@ const SCHEMA = yup.object().shape({
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
const WEBSITE_SCHEMA = yup.object().shape({
|
|
||||||
url: yup.string().matches(URL_REGEX, "This URL is invalid."),
|
|
||||||
})
|
|
||||||
|
|
||||||
const getDomain = (url: string): string => {
|
|
||||||
const {hostname, port} = new URL(url)
|
|
||||||
return `${hostname}${port ? `:${port}` : ""}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AliasNotesForm({
|
export default function AliasNotesForm({
|
||||||
id,
|
id,
|
||||||
@ -91,12 +78,16 @@ export default function AliasNotesForm({
|
|||||||
onChanged(newAlias as any as DecryptedAlias)
|
onChanged(newAlias as any as DecryptedAlias)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const formik = useFormik<Form>({
|
const initialValues = useMemo(
|
||||||
validationSchema: SCHEMA,
|
() => ({
|
||||||
initialValues: {
|
|
||||||
personalNotes: notes.data.personalNotes,
|
personalNotes: notes.data.personalNotes,
|
||||||
websites: notes.data.websites,
|
websites: notes.data.websites,
|
||||||
},
|
}),
|
||||||
|
[notes.data.personalNotes, notes.data.websites],
|
||||||
|
)
|
||||||
|
const formik = useFormik<Form>({
|
||||||
|
validationSchema: SCHEMA,
|
||||||
|
initialValues,
|
||||||
onSubmit: async (values, {setErrors}) => {
|
onSubmit: async (values, {setErrors}) => {
|
||||||
try {
|
try {
|
||||||
const newNotes = update(notes, {
|
const newNotes = update(notes, {
|
||||||
@ -121,47 +112,51 @@ export default function AliasNotesForm({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const websiteFormik = useFormik<WebsiteForm>({
|
|
||||||
validationSchema: WEBSITE_SCHEMA,
|
|
||||||
initialValues: {
|
|
||||||
url: "",
|
|
||||||
},
|
|
||||||
onSubmit: async values => {
|
|
||||||
const url = (() => {
|
|
||||||
// Make sure url starts with `http://` or `https://`
|
|
||||||
if (values.url.startsWith("http://")) {
|
|
||||||
return values.url
|
|
||||||
}
|
|
||||||
if (values.url.startsWith("https://")) {
|
|
||||||
return values.url
|
|
||||||
}
|
|
||||||
return `https://${values.url}`
|
|
||||||
})()
|
|
||||||
|
|
||||||
const {hostname, protocol, port} = new URL(url)
|
const [isInEditMode, setIsInEditMode] = useState<boolean>(false)
|
||||||
const baseUrl = `${protocol}//${hostname}${port ? `:${port}` : ""}`
|
|
||||||
|
|
||||||
websiteFormik.resetForm()
|
|
||||||
await formik.setFieldValue(
|
|
||||||
"websites",
|
|
||||||
[
|
|
||||||
...formik.values.websites,
|
|
||||||
{
|
|
||||||
url: baseUrl,
|
|
||||||
createdAt: new Date(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
await formik.submitForm()
|
|
||||||
},
|
|
||||||
validateOnChange: true,
|
|
||||||
validateOnBlur: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
|
<Grid container direction="column" spacing={4}>
|
||||||
|
<Grid item>
|
||||||
|
<Grid container spacing={1} direction="row">
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="h6" component="h3">
|
||||||
|
Notes
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
onClick={async () => {
|
||||||
|
if (
|
||||||
|
isInEditMode &&
|
||||||
|
!deepEqual(
|
||||||
|
initialValues,
|
||||||
|
formik.values,
|
||||||
|
{
|
||||||
|
strict: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await formik.submitForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsInEditMode(!isInEditMode)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isInEditMode ? (
|
||||||
|
<MdCheckCircle />
|
||||||
|
) : (
|
||||||
|
<FaPen />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
<Grid container spacing={4} direction="column">
|
<Grid container spacing={4} direction="column">
|
||||||
{notes.data.createdAt && (
|
{notes.data.createdAt && (
|
||||||
<Grid item>
|
<Grid item>
|
||||||
@ -179,7 +174,10 @@ export default function AliasNotesForm({
|
|||||||
title={notes.data.createdAt.toISOString()}
|
title={notes.data.createdAt.toISOString()}
|
||||||
>
|
>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
{format(notes.data.createdAt, "Pp")}
|
{format(
|
||||||
|
notes.data.createdAt,
|
||||||
|
"Pp",
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -187,6 +185,14 @@ export default function AliasNotesForm({
|
|||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
<Grid item>
|
<Grid item>
|
||||||
|
<Grid container spacing={1} direction="column">
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="overline">
|
||||||
|
Personal Notes
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
{isInEditMode ? (
|
||||||
<TextField
|
<TextField
|
||||||
label="Personal Notes"
|
label="Personal Notes"
|
||||||
multiline
|
multiline
|
||||||
@ -194,17 +200,25 @@ export default function AliasNotesForm({
|
|||||||
key="personalNotes"
|
key="personalNotes"
|
||||||
id="personalNotes"
|
id="personalNotes"
|
||||||
name="personalNotes"
|
name="personalNotes"
|
||||||
value={formik.values.personalNotes}
|
value={
|
||||||
|
formik.values.personalNotes
|
||||||
|
}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={() => formik.submitForm()}
|
onBlur={formik.handleBlur}
|
||||||
disabled={formik.isSubmitting}
|
disabled={formik.isSubmitting}
|
||||||
error={
|
error={
|
||||||
formik.touched.personalNotes &&
|
formik.touched
|
||||||
Boolean(formik.errors.personalNotes)
|
.personalNotes &&
|
||||||
|
Boolean(
|
||||||
|
formik.errors
|
||||||
|
.personalNotes,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
helperText={
|
helperText={
|
||||||
(formik.touched.personalNotes &&
|
(formik.touched
|
||||||
formik.errors.personalNotes) ||
|
.personalNotes &&
|
||||||
|
formik.errors
|
||||||
|
.personalNotes) ||
|
||||||
"You can enter personal notes for this alias here. Notes are encrypted."
|
"You can enter personal notes for this alias here. Notes are encrypted."
|
||||||
}
|
}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -215,71 +229,64 @@ export default function AliasNotesForm({
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography>
|
||||||
|
{notes.data.personalNotes}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<FormGroup row>
|
<Grid container spacing={1} direction="column">
|
||||||
<TextField
|
<Grid item>
|
||||||
name="url"
|
<Typography variant="overline">
|
||||||
id="url"
|
Websites
|
||||||
label="Website"
|
</Typography>
|
||||||
variant="outlined"
|
</Grid>
|
||||||
value={websiteFormik.values.url}
|
{isInEditMode ? (
|
||||||
onChange={websiteFormik.handleChange}
|
<Grid item>
|
||||||
onBlur={websiteFormik.handleBlur}
|
<AddWebsiteField
|
||||||
onKeyDown={whenEnterPressed(() =>
|
onAdd={async website => {
|
||||||
websiteFormik.handleSubmit(),
|
await formik.setFieldValue(
|
||||||
)}
|
"websites",
|
||||||
disabled={websiteFormik.isSubmitting}
|
[
|
||||||
error={
|
...formik.values
|
||||||
websiteFormik.touched.url &&
|
.websites,
|
||||||
Boolean(websiteFormik.errors.url)
|
{
|
||||||
}
|
url: website,
|
||||||
InputProps={{
|
},
|
||||||
startAdornment: (
|
],
|
||||||
<InputAdornment position="start">
|
)
|
||||||
<RiLinkM />
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
|
isLoading={formik.isSubmitting}
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="contained"
|
|
||||||
disableElevation
|
|
||||||
onClick={() => websiteFormik.handleSubmit()}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
<FormHelperText
|
|
||||||
error={
|
|
||||||
websiteFormik.touched.url &&
|
|
||||||
Boolean(websiteFormik.errors.url)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(websiteFormik.touched.url &&
|
|
||||||
websiteFormik.errors.url) ||
|
|
||||||
"Add a website to this alias. Used to autofill."}
|
|
||||||
</FormHelperText>
|
|
||||||
</FormGroup>
|
|
||||||
<FormikProvider value={formik}>
|
<FormikProvider value={formik}>
|
||||||
<FieldArray
|
<FieldArray
|
||||||
name="websites"
|
name="websites"
|
||||||
render={arrayHelpers => (
|
render={arrayHelpers => (
|
||||||
<List>
|
<List>
|
||||||
{formik.values.websites.map(
|
{formik.values.websites.map(
|
||||||
(website, index) => (
|
(
|
||||||
<ListItem key={website.url}>
|
website,
|
||||||
|
index,
|
||||||
|
) => (
|
||||||
|
<ListItem
|
||||||
|
key={
|
||||||
|
website.url
|
||||||
|
}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<BackupImage
|
<FaviconImage
|
||||||
width={20}
|
url={
|
||||||
fallbackSrc={`https://external-content.duckduckgo.com/ip3/${getDomain(
|
website.url
|
||||||
website.url,
|
}
|
||||||
)}.ico`}
|
|
||||||
src={`${website.url}/favicon.ico`}
|
|
||||||
/>
|
/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>
|
||||||
{website.url}
|
{
|
||||||
|
website.url
|
||||||
|
}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -289,7 +296,6 @@ export default function AliasNotesForm({
|
|||||||
arrayHelpers.remove(
|
arrayHelpers.remove(
|
||||||
index,
|
index,
|
||||||
)
|
)
|
||||||
await formik.submitForm()
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TiDelete />
|
<TiDelete />
|
||||||
@ -303,6 +309,48 @@ export default function AliasNotesForm({
|
|||||||
/>
|
/>
|
||||||
</FormikProvider>
|
</FormikProvider>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
) : (
|
||||||
|
<Grid item>
|
||||||
|
{notes.data.websites.length ? (
|
||||||
|
<List>
|
||||||
|
{notes.data.websites.map(
|
||||||
|
website => (
|
||||||
|
<ListItem
|
||||||
|
key={
|
||||||
|
website.url
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<FaviconImage
|
||||||
|
width={
|
||||||
|
20
|
||||||
|
}
|
||||||
|
url={
|
||||||
|
website.url
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>
|
||||||
|
{
|
||||||
|
website.url
|
||||||
|
}
|
||||||
|
</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2">
|
||||||
|
You haven't used this
|
||||||
|
alias on any website yet.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</form>
|
</form>
|
||||||
<ErrorSnack message={formik.errors.detail} />
|
<ErrorSnack message={formik.errors.detail} />
|
||||||
|
@ -40,7 +40,7 @@ export default function AuthenticatedRoute(): ReactElement {
|
|||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Grid item xs={12} sm={4} md={2}>
|
<Grid item xs={12} sm={3} md={2}>
|
||||||
<Box
|
<Box
|
||||||
bgcolor={theme.palette.background.paper}
|
bgcolor={theme.palette.background.paper}
|
||||||
component="nav"
|
component="nav"
|
||||||
@ -56,7 +56,7 @@ export default function AuthenticatedRoute(): ReactElement {
|
|||||||
</List>
|
</List>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={8} md={10}>
|
<Grid item xs={12} sm={9} md={10}>
|
||||||
<Paper>
|
<Paper>
|
||||||
<Box
|
<Box
|
||||||
maxHeight="80vh"
|
maxHeight="80vh"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user