import * as yup from "yup" import {TiDelete} from "react-icons/ti" import {AxiosError} from "axios" import {ReactElement, useContext} from "react" import {RiLinkM, RiStickyNoteFill} from "react-icons/ri" import {FieldArray, FormikProvider, useFormik} from "formik" import update from "immutability-helper" import {useMutation} from "@tanstack/react-query" import { Button, FormGroup, FormHelperText, Grid, IconButton, InputAdornment, List, ListItem, ListItemIcon, ListItemSecondaryAction, ListItemText, TextField, } from "@mui/material" import {URL_REGEX} from "~/constants/values" import {decryptAliasNotes, parseFastAPIError, whenEnterPressed} from "~/utils" import {BackupImage, ErrorSnack, SuccessSnack} from "~/components" import {Alias, AliasNote, DecryptedAlias} from "~/server-types" import {UpdateAliasData, updateAlias} from "~/apis" import AuthContext from "~/AuthContext/AuthContext" export interface AliasNotesFormProps { id: string notes: AliasNote onChanged: (alias: DecryptedAlias) => void } interface Form { personalNotes: string websites: AliasNote["data"]["websites"] detail?: string } interface WebsiteForm { url: string } const SCHEMA = yup.object().shape({ personalNotes: yup.string(), websites: yup.array().of( yup.object().shape({ url: yup.string().url(), createdAt: yup.date(), }), ), }) 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, notes, onChanged, }: AliasNotesFormProps): ReactElement { const {_encryptUsingMasterPassword, _decryptUsingMasterPassword} = useContext(AuthContext) const {mutateAsync, isSuccess} = useMutation< Alias, AxiosError, UpdateAliasData >(values => updateAlias(id, values), { onSuccess: newAlias => { onChanged(decryptAliasNotes(newAlias, _decryptUsingMasterPassword)) }, }) const formik = useFormik
({ validationSchema: SCHEMA, initialValues: { personalNotes: notes.data.personalNotes, websites: notes.data.websites, }, onSubmit: async (values, {setErrors}) => { try { const newNotes = update(notes, { data: { personalNotes: { $set: values.personalNotes, }, websites: { $set: values.websites, }, }, }) const data = _encryptUsingMasterPassword( JSON.stringify(newNotes), ) await mutateAsync({ encryptedNotes: data, }) } catch (error) { setErrors(parseFastAPIError(error as AxiosError)) } }, }) const websiteFormik = useFormik({ 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, }) return ( <> 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: ( ), }} /> websiteFormik.handleSubmit(), )} disabled={websiteFormik.isSubmitting} error={ websiteFormik.touched.url && Boolean(websiteFormik.errors.url) } InputProps={{ startAdornment: ( ), }} /> {(websiteFormik.touched.url && websiteFormik.errors.url) || "Add a website to this alias. Used to autofill."} ( {formik.values.websites.map( (website, index) => ( {website.url} { arrayHelpers.remove( index, ) await formik.submitForm() }} > ), )} )} /> ) }