mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 07:55:25 +02:00
improved AliasNotesForm.tsx
This commit is contained in:
parent
b376177dc2
commit
f4f8eceabe
@ -21,6 +21,7 @@
|
||||
"camelcase-keys": "^8.0.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"deep-equal": "^2.0.5",
|
||||
"formik": "^2.2.9",
|
||||
"group-array": "^1.0.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
@ -41,6 +42,7 @@
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/date-fns": "^2.6.0",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/group-array": "^1.0.1",
|
||||
"@types/openpgp": "^4.4.18",
|
||||
"@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 * from "./DecryptionPasswordMissingAlert"
|
||||
export {default as DecryptionPasswordMissingAlert} from "./DecryptionPasswordMissingAlert"
|
||||
export * from "./BackupImage"
|
||||
export {default as BackupImage} from "./BackupImage"
|
||||
export * from "./FaviconImage"
|
||||
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
|
||||
}
|
||||
|
||||
const getDomain = (url: string): string => {
|
||||
const {hostname, port} = new URL(url)
|
||||
return `${hostname}${port ? `:${port}` : ""}`
|
||||
}
|
||||
|
||||
export default function AliasDetails({
|
||||
alias: aliasValue,
|
||||
}: AliasDetailsProps): ReactElement {
|
||||
@ -46,24 +51,15 @@ export default function AliasDetails({
|
||||
</Grid>
|
||||
</Grid>
|
||||
<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 ? (
|
||||
<AliasNotesForm
|
||||
id={aliasUIState.id}
|
||||
notes={(aliasUIState as DecryptedAlias).notes}
|
||||
onChanged={setAliasUIState}
|
||||
/>
|
||||
) : (
|
||||
<DecryptionPasswordMissingAlert />
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
{encryptionStatus === EncryptionStatus.Available ? (
|
||||
<AliasNotesForm
|
||||
id={aliasUIState.id}
|
||||
notes={(aliasUIState as DecryptedAlias).notes}
|
||||
onChanged={setAliasUIState}
|
||||
/>
|
||||
) : (
|
||||
<DecryptionPasswordMissingAlert />
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid container spacing={4}>
|
||||
@ -72,24 +68,23 @@ export default function AliasDetails({
|
||||
Settings
|
||||
</Typography>
|
||||
</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>
|
||||
<AliasPreferencesForm
|
||||
alias={aliasUIState}
|
||||
onChanged={setAliasUIState}
|
||||
/>
|
||||
</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>
|
||||
|
@ -1,18 +1,17 @@
|
||||
import * as yup from "yup"
|
||||
import {TiDelete} from "react-icons/ti"
|
||||
import {AxiosError} from "axios"
|
||||
import {ReactElement, useContext} from "react"
|
||||
import {MdEditCalendar} from "react-icons/md"
|
||||
import {RiLinkM, RiStickyNoteFill} from "react-icons/ri"
|
||||
import {ReactElement, useContext, useMemo, useState} from "react"
|
||||
import {MdCheckCircle, MdEditCalendar} from "react-icons/md"
|
||||
import {RiStickyNoteFill} from "react-icons/ri"
|
||||
import {FieldArray, FormikProvider, useFormik} from "formik"
|
||||
import {FaPen} from "react-icons/fa"
|
||||
import deepEqual from "deep-equal"
|
||||
import format from "date-fns/format"
|
||||
import update from "immutability-helper"
|
||||
|
||||
import {useMutation} from "@tanstack/react-query"
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
FormHelperText,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
@ -26,11 +25,11 @@ import {
|
||||
Typography,
|
||||
} from "@mui/material"
|
||||
|
||||
import {URL_REGEX} from "~/constants/values"
|
||||
import {parseFastAPIError, whenEnterPressed} from "~/utils"
|
||||
import {BackupImage, ErrorSnack, SuccessSnack} from "~/components"
|
||||
import {parseFastAPIError} from "~/utils"
|
||||
import {ErrorSnack, FaviconImage, SuccessSnack} from "~/components"
|
||||
import {Alias, AliasNote, DecryptedAlias} from "~/server-types"
|
||||
import {UpdateAliasData, updateAlias} from "~/apis"
|
||||
import AddWebsiteField from "~/route-widgets/AliasDetailRoute/AddWebsiteField"
|
||||
import AuthContext from "~/AuthContext/AuthContext"
|
||||
import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
|
||||
|
||||
@ -48,10 +47,6 @@ interface Form {
|
||||
detail?: string
|
||||
}
|
||||
|
||||
interface WebsiteForm {
|
||||
url: string
|
||||
}
|
||||
|
||||
const SCHEMA = yup.object().shape({
|
||||
personalNotes: yup.string(),
|
||||
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({
|
||||
id,
|
||||
@ -91,12 +78,16 @@ export default function AliasNotesForm({
|
||||
onChanged(newAlias as any as DecryptedAlias)
|
||||
},
|
||||
})
|
||||
const formik = useFormik<Form>({
|
||||
validationSchema: SCHEMA,
|
||||
initialValues: {
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
personalNotes: notes.data.personalNotes,
|
||||
websites: notes.data.websites,
|
||||
},
|
||||
}),
|
||||
[notes.data.personalNotes, notes.data.websites],
|
||||
)
|
||||
const formik = useFormik<Form>({
|
||||
validationSchema: SCHEMA,
|
||||
initialValues,
|
||||
onSubmit: async (values, {setErrors}) => {
|
||||
try {
|
||||
const newNotes = update(notes, {
|
||||
@ -121,187 +112,244 @@ 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 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,
|
||||
})
|
||||
const [isInEditMode, setIsInEditMode] = useState<boolean>(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Grid container spacing={4} direction="column">
|
||||
{notes.data.createdAt && (
|
||||
<Grid item>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
>
|
||||
<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">
|
||||
{notes.data.createdAt && (
|
||||
<Grid item>
|
||||
<MdEditCalendar />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
title={notes.data.createdAt.toISOString()}
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="body1">
|
||||
{format(notes.data.createdAt, "Pp")}
|
||||
<Grid item>
|
||||
<MdEditCalendar />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip
|
||||
title={notes.data.createdAt.toISOString()}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
{format(
|
||||
notes.data.createdAt,
|
||||
"Pp",
|
||||
)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item>
|
||||
<Grid container spacing={1} direction="column">
|
||||
<Grid item>
|
||||
<Typography variant="overline">
|
||||
Personal Notes
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
{isInEditMode ? (
|
||||
<TextField
|
||||
label="Personal Notes"
|
||||
multiline
|
||||
fullWidth
|
||||
key="personalNotes"
|
||||
id="personalNotes"
|
||||
name="personalNotes"
|
||||
value={
|
||||
formik.values.personalNotes
|
||||
}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
disabled={formik.isSubmitting}
|
||||
error={
|
||||
formik.touched
|
||||
.personalNotes &&
|
||||
Boolean(
|
||||
formik.errors
|
||||
.personalNotes,
|
||||
)
|
||||
}
|
||||
helperText={
|
||||
(formik.touched
|
||||
.personalNotes &&
|
||||
formik.errors
|
||||
.personalNotes) ||
|
||||
"You can enter personal notes for this alias here. Notes are encrypted."
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<RiStickyNoteFill />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Typography>
|
||||
{notes.data.personalNotes}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Grid container spacing={1} direction="column">
|
||||
<Grid item>
|
||||
<Typography variant="overline">
|
||||
Websites
|
||||
</Typography>
|
||||
</Grid>
|
||||
{isInEditMode ? (
|
||||
<Grid item>
|
||||
<AddWebsiteField
|
||||
onAdd={async website => {
|
||||
await formik.setFieldValue(
|
||||
"websites",
|
||||
[
|
||||
...formik.values
|
||||
.websites,
|
||||
{
|
||||
url: website,
|
||||
},
|
||||
],
|
||||
)
|
||||
}}
|
||||
isLoading={formik.isSubmitting}
|
||||
/>
|
||||
<FormikProvider value={formik}>
|
||||
<FieldArray
|
||||
name="websites"
|
||||
render={arrayHelpers => (
|
||||
<List>
|
||||
{formik.values.websites.map(
|
||||
(
|
||||
website,
|
||||
index,
|
||||
) => (
|
||||
<ListItem
|
||||
key={
|
||||
website.url
|
||||
}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<FaviconImage
|
||||
url={
|
||||
website.url
|
||||
}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{
|
||||
website.url
|
||||
}
|
||||
</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="delete"
|
||||
onClick={async () => {
|
||||
arrayHelpers.remove(
|
||||
index,
|
||||
)
|
||||
}}
|
||||
>
|
||||
<TiDelete />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
),
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
/>
|
||||
</FormikProvider>
|
||||
</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 item>
|
||||
<TextField
|
||||
label="Personal Notes"
|
||||
multiline
|
||||
fullWidth
|
||||
key="personalNotes"
|
||||
id="personalNotes"
|
||||
name="personalNotes"
|
||||
value={formik.values.personalNotes}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => formik.submitForm()}
|
||||
disabled={formik.isSubmitting}
|
||||
error={
|
||||
formik.touched.personalNotes &&
|
||||
Boolean(formik.errors.personalNotes)
|
||||
}
|
||||
helperText={
|
||||
(formik.touched.personalNotes &&
|
||||
formik.errors.personalNotes) ||
|
||||
"You can enter personal notes for this alias here. Notes are encrypted."
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<RiStickyNoteFill />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<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}
|
||||
error={
|
||||
websiteFormik.touched.url &&
|
||||
Boolean(websiteFormik.errors.url)
|
||||
}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<RiLinkM />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<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}>
|
||||
<FieldArray
|
||||
name="websites"
|
||||
render={arrayHelpers => (
|
||||
<List>
|
||||
{formik.values.websites.map(
|
||||
(website, index) => (
|
||||
<ListItem key={website.url}>
|
||||
<ListItemIcon>
|
||||
<BackupImage
|
||||
width={20}
|
||||
fallbackSrc={`https://external-content.duckduckgo.com/ip3/${getDomain(
|
||||
website.url,
|
||||
)}.ico`}
|
||||
src={`${website.url}/favicon.ico`}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{website.url}
|
||||
</ListItemText>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="delete"
|
||||
onClick={async () => {
|
||||
arrayHelpers.remove(
|
||||
index,
|
||||
)
|
||||
await formik.submitForm()
|
||||
}}
|
||||
>
|
||||
<TiDelete />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
),
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
/>
|
||||
</FormikProvider>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
|
@ -40,7 +40,7 @@ export default function AuthenticatedRoute(): ReactElement {
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item xs={12} sm={4} md={2}>
|
||||
<Grid item xs={12} sm={3} md={2}>
|
||||
<Box
|
||||
bgcolor={theme.palette.background.paper}
|
||||
component="nav"
|
||||
@ -56,7 +56,7 @@ export default function AuthenticatedRoute(): ReactElement {
|
||||
</List>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={8} md={10}>
|
||||
<Grid item xs={12} sm={9} md={10}>
|
||||
<Paper>
|
||||
<Box
|
||||
maxHeight="80vh"
|
||||
|
Loading…
x
Reference in New Issue
Block a user