mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 07:55:25 +02:00
feat: Add StringPoolField
This commit is contained in:
parent
6eacf14bae
commit
82a3641a6d
@ -33,6 +33,7 @@
|
||||
"immutability-helper": "^3.1.1",
|
||||
"in-milliseconds": "^1.2.0",
|
||||
"in-seconds": "^1.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"notistack": "^2.0.8",
|
||||
"openpgp": "^5.5.0",
|
||||
"react": "^18.2.0",
|
||||
@ -56,6 +57,7 @@
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/group-array": "^1.0.1",
|
||||
"@types/jest": "^29.2.4",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/openpgp": "^4.4.18",
|
||||
"@types/react-icons": "^3.0.0",
|
||||
|
@ -3,6 +3,7 @@
|
||||
"cancelLabel": "Cancel",
|
||||
"emptyValue": "-",
|
||||
"emptyUnavailableValue": "Unavailable",
|
||||
"saveLabel": "Save",
|
||||
|
||||
"defaultValueSelection": "Default <{{value}}>",
|
||||
"defaultValueSelectionRaw": "<{{value}}>",
|
||||
@ -345,7 +346,7 @@
|
||||
"label": "Random alias character pool",
|
||||
"description": "Characters that are used to generate random emails."
|
||||
},
|
||||
"randomEmailIdLengthIncreaseOnPercentage": {
|
||||
"randomEmailLengthIncreaseOnPercentage": {
|
||||
"label": "Percentage of used aliases",
|
||||
"description": "If the percentage of used random email IDs is higher than this value, the length of the random email ID will be increased. This is used to prevent spammers from guessing the email ID."
|
||||
},
|
||||
@ -378,6 +379,10 @@
|
||||
"description": "If enabled, your instance will collect anonymous statistics and share them. They will only be stored locally on this instance but made public."
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Global Settings",
|
||||
"description": "Configure global settings for your instance."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -458,6 +463,19 @@
|
||||
"doNotShare": "Do not share",
|
||||
"decideLater": "Decide later",
|
||||
"doNotAskAgain": "Do not ask again"
|
||||
},
|
||||
"StringPoolField": {
|
||||
"addCustom": {
|
||||
"label": "Add custom"
|
||||
},
|
||||
"forms": {
|
||||
"addNew": {
|
||||
"title": "Add new value",
|
||||
"description": "Enter your characters you would like to include",
|
||||
"label": "Characters",
|
||||
"submit": "Add"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {client} from "~/constants/axios-client"
|
||||
import {AdminSettings} from "~/server-types"
|
||||
|
||||
export default async function getAdminSettings(): Promise<AdminSettings> {
|
||||
export default async function getAdminSettings(): Promise<Partial<AdminSettings>> {
|
||||
const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/settings`, {
|
||||
withCredentials: true,
|
||||
})
|
||||
|
68
src/components/widgets/StringPoolField/AddNewDialog.tsx
Normal file
68
src/components/widgets/StringPoolField/AddNewDialog.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import {ReactElement, useState} from "react"
|
||||
import {useTranslation} from "react-i18next"
|
||||
import {MdCheck} from "react-icons/md"
|
||||
import {TiCancel} from "react-icons/ti"
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
} from "@mui/material"
|
||||
|
||||
import {whenEnterPressed} from "~/utils"
|
||||
|
||||
export interface StringPoolFieldProps {
|
||||
onCreated: (value: string) => void
|
||||
onClose: () => void
|
||||
|
||||
open?: boolean
|
||||
}
|
||||
|
||||
export default function AddNewDialog({
|
||||
onCreated,
|
||||
open = false,
|
||||
onClose,
|
||||
}: StringPoolFieldProps): ReactElement {
|
||||
const {t} = useTranslation()
|
||||
|
||||
const [value, setValue] = useState<string>("")
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>{t("components.StringPoolField.forms.addNew.title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t("components.StringPoolField.forms.addNew.description")}
|
||||
</DialogContentText>
|
||||
<Box my={2}>
|
||||
<TextField
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
label={t("components.StringPoolField.forms.addNew.label")}
|
||||
name="addNew"
|
||||
fullWidth
|
||||
autoFocus
|
||||
onKeyUp={whenEnterPressed(() => onCreated(value))}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} startIcon={<TiCancel />} variant="text">
|
||||
{t("general.cancelLabel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onCreated(value)}
|
||||
variant="contained"
|
||||
startIcon={<MdCheck />}
|
||||
>
|
||||
{t("components.StringPoolField.forms.addNew.submit")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
183
src/components/widgets/StringPoolField/StringPoolField.tsx
Normal file
183
src/components/widgets/StringPoolField/StringPoolField.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
import {useTranslation} from "react-i18next"
|
||||
import {MdAdd} from "react-icons/md"
|
||||
import React, {ReactElement, useLayoutEffect, useMemo, useState} from "react"
|
||||
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
Chip,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
Select,
|
||||
SelectProps,
|
||||
} from "@mui/material"
|
||||
|
||||
import AddNewDialog from "./AddNewDialog"
|
||||
|
||||
export interface StringPoolFieldProps
|
||||
extends Omit<SelectProps<string[]>, "onChange" | "value" | "multiple" | "labelId" | "label"> {
|
||||
pools: Record<string, string>
|
||||
label: string
|
||||
value: string
|
||||
onChange: SelectProps<string>["onChange"]
|
||||
id: string
|
||||
|
||||
allowCustom?: boolean
|
||||
helperText?: string | string[]
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
export function createPool(pools: Record<string, string>): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(pools).map(([key, value]) => [key.split("").sort().join(""), value]),
|
||||
)
|
||||
}
|
||||
|
||||
export default function StringPoolField({
|
||||
pools,
|
||||
value,
|
||||
helperText,
|
||||
id,
|
||||
error,
|
||||
label,
|
||||
onChange,
|
||||
onOpen,
|
||||
allowCustom,
|
||||
name,
|
||||
fullWidth,
|
||||
...props
|
||||
}: StringPoolFieldProps): ReactElement {
|
||||
const {t} = useTranslation()
|
||||
|
||||
const reversedPoolsMap = useMemo(
|
||||
() => Object.fromEntries(Object.entries(pools).map(([key, value]) => [value, key])),
|
||||
[pools],
|
||||
)
|
||||
const [isInAddMode, setIsInAddMode] = useState<boolean>(false)
|
||||
const [uiRemainingValue, setUiRemainingValue] = useState<string>("")
|
||||
|
||||
const selectedValueMaps = Object.entries(pools)
|
||||
.filter(([key]) => value.includes(key))
|
||||
.map(([, value]) => value)
|
||||
const remainingValue = (() => {
|
||||
// List of all characters inside the pools
|
||||
const charactersInPools = Object.keys(pools).join("")
|
||||
|
||||
return value
|
||||
.split("")
|
||||
.filter(char => !charactersInPools.includes(char))
|
||||
.join("")
|
||||
})()
|
||||
const selectValue = [...selectedValueMaps, remainingValue].filter(Boolean)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (remainingValue) {
|
||||
setUiRemainingValue(remainingValue)
|
||||
}
|
||||
}, [remainingValue])
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControl sx={{minWidth: 180}} fullWidth={fullWidth} error={error}>
|
||||
<InputLabel id={id} error={error}>
|
||||
{label}
|
||||
</InputLabel>
|
||||
<Select<string[]>
|
||||
multiple
|
||||
name={name}
|
||||
labelId={id}
|
||||
renderValue={(selected: string[]) => (
|
||||
<Box sx={{display: "flex", flexWrap: "wrap", gap: 0.5}}>
|
||||
{selected.map(value => (
|
||||
<Chip key={value} label={value} />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
value={selectValue}
|
||||
label={label}
|
||||
onOpen={event => {
|
||||
if (!remainingValue) {
|
||||
setUiRemainingValue("")
|
||||
}
|
||||
|
||||
onOpen?.(event)
|
||||
}}
|
||||
onChange={(event, child) => {
|
||||
if (!Array.isArray(event.target.value)) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = event.target.value.reduce((acc, value) => {
|
||||
if (reversedPoolsMap[value]) {
|
||||
return acc + reversedPoolsMap[value]
|
||||
}
|
||||
|
||||
return acc + value
|
||||
}, "")
|
||||
|
||||
onChange!(
|
||||
// @ts-ignore
|
||||
{
|
||||
...event,
|
||||
target: {
|
||||
...event.target,
|
||||
value: value as string,
|
||||
},
|
||||
},
|
||||
child,
|
||||
)
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{Object.entries(pools).map(([poolValue, label]) => (
|
||||
<MenuItem key={poolValue} value={label}>
|
||||
<Checkbox checked={value.includes(poolValue)} />
|
||||
<ListItemText primary={label} />
|
||||
</MenuItem>
|
||||
))}
|
||||
{uiRemainingValue && (
|
||||
<MenuItem value={uiRemainingValue}>
|
||||
<Checkbox checked={uiRemainingValue === remainingValue} />
|
||||
<ListItemText primary={uiRemainingValue} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{allowCustom && (
|
||||
<MenuItem
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
setIsInAddMode(true)
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<MdAdd />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t("components.StringPoolField.addCustom.label")}
|
||||
/>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Select>
|
||||
{helperText ? <FormHelperText error={error}>{helperText}</FormHelperText> : null}
|
||||
</FormControl>
|
||||
<AddNewDialog
|
||||
onCreated={newValue => {
|
||||
setIsInAddMode(false)
|
||||
|
||||
// @ts-ignore: This is enough for formik.
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: value + newValue,
|
||||
},
|
||||
})
|
||||
}}
|
||||
onClose={() => setIsInAddMode(false)}
|
||||
open={isInAddMode}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
2
src/components/widgets/StringPoolField/index.ts
Normal file
2
src/components/widgets/StringPoolField/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./StringPoolField"
|
||||
export {default as StringPoolField} from "./StringPoolField"
|
@ -45,5 +45,6 @@ export {default as LoadingData} from "./LoadingData"
|
||||
export * from "./ExternalLinkIndication"
|
||||
export {default as ExternalLinkIndication} from "./ExternalLinkIndication"
|
||||
export {default as ExtensionSignalHandler} from "./ExtensionalSignalHandler"
|
||||
export * from "./StringPoolField"
|
||||
|
||||
export * as SimplePageBuilder from "./simple-page-builder"
|
||||
|
Loading…
x
Reference in New Issue
Block a user