|
| 1 | +import { IconSettings } from "@tabler/icons-react"; |
| 2 | +import CodeEditor from "@uiw/react-textarea-code-editor"; |
| 3 | +import cn from "classnames"; |
| 4 | +import { useFormikContext } from "formik"; |
| 5 | +import { useState } from "react"; |
| 6 | +import type { ProxyLocation } from "src/api/backend"; |
| 7 | +import { intl, T } from "src/locale"; |
| 8 | +import styles from "./LocationsFields.module.css"; |
| 9 | + |
| 10 | +interface Props { |
| 11 | + initialValues: ProxyLocation[]; |
| 12 | + name?: string; |
| 13 | +} |
| 14 | +export function LocationsFields({ initialValues, name = "items" }: Props) { |
| 15 | + const [values, setValues] = useState<ProxyLocation[]>(initialValues || []); |
| 16 | + const { setFieldValue } = useFormikContext(); |
| 17 | + const [advVisible, setAdvVisible] = useState<number[]>([]); |
| 18 | + |
| 19 | + const blankItem: ProxyLocation = { |
| 20 | + path: "", |
| 21 | + advancedConfig: "", |
| 22 | + forwardScheme: "http", |
| 23 | + forwardHost: "", |
| 24 | + forwardPort: 80, |
| 25 | + }; |
| 26 | + |
| 27 | + const toggleAdvVisible = (idx: number) => { |
| 28 | + setAdvVisible(advVisible.includes(idx) ? advVisible.filter((i) => i !== idx) : [...advVisible, idx]); |
| 29 | + }; |
| 30 | + |
| 31 | + const handleAdd = () => { |
| 32 | + setValues([...values, blankItem]); |
| 33 | + }; |
| 34 | + |
| 35 | + const handleRemove = (idx: number) => { |
| 36 | + const newValues = values.filter((_: ProxyLocation, i: number) => i !== idx); |
| 37 | + setValues(newValues); |
| 38 | + setFormField(newValues); |
| 39 | + }; |
| 40 | + |
| 41 | + const handleChange = (idx: number, field: string, fieldValue: string) => { |
| 42 | + const newValues = values.map((v: ProxyLocation, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v)); |
| 43 | + setValues(newValues); |
| 44 | + setFormField(newValues); |
| 45 | + }; |
| 46 | + |
| 47 | + const setFormField = (newValues: ProxyLocation[]) => { |
| 48 | + const filtered = newValues.filter((v: ProxyLocation) => v?.path?.trim() !== ""); |
| 49 | + setFieldValue(name, filtered); |
| 50 | + }; |
| 51 | + |
| 52 | + if (values.length === 0) { |
| 53 | + return ( |
| 54 | + <div className="text-center"> |
| 55 | + <button type="button" className="btn my-3" onClick={handleAdd}> |
| 56 | + <T id="action.add-location" /> |
| 57 | + </button> |
| 58 | + </div> |
| 59 | + ); |
| 60 | + } |
| 61 | + |
| 62 | + return ( |
| 63 | + <> |
| 64 | + {values.map((item: ProxyLocation, idx: number) => ( |
| 65 | + <div key={idx} className={cn("card", "card-active", "mb-3", styles.locationCard)}> |
| 66 | + <div className="card-body"> |
| 67 | + <div className="row"> |
| 68 | + <div className="col-md-10"> |
| 69 | + <div className="input-group mb-3"> |
| 70 | + <span className="input-group-text">Location</span> |
| 71 | + <input |
| 72 | + type="text" |
| 73 | + className="form-control" |
| 74 | + placeholder="/path" |
| 75 | + autoComplete="off" |
| 76 | + value={item.path} |
| 77 | + onChange={(e) => handleChange(idx, "path", e.target.value)} |
| 78 | + /> |
| 79 | + </div> |
| 80 | + </div> |
| 81 | + <div className="col-md-2 text-end"> |
| 82 | + <button |
| 83 | + type="button" |
| 84 | + className="btn p-0" |
| 85 | + title="Advanced" |
| 86 | + onClick={() => toggleAdvVisible(idx)} |
| 87 | + > |
| 88 | + <IconSettings size={20} /> |
| 89 | + </button> |
| 90 | + </div> |
| 91 | + </div> |
| 92 | + <div className="row"> |
| 93 | + <div className="col-md-3"> |
| 94 | + <div className="mb-3"> |
| 95 | + <label className="form-label" htmlFor="forwardScheme"> |
| 96 | + <T id="host.forward-scheme" /> |
| 97 | + </label> |
| 98 | + <select |
| 99 | + id="forwardScheme" |
| 100 | + className="form-control" |
| 101 | + value={item.forwardScheme} |
| 102 | + onChange={(e) => handleChange(idx, "forwardScheme", e.target.value)} |
| 103 | + > |
| 104 | + <option value="http">http</option> |
| 105 | + <option value="https">https</option> |
| 106 | + </select> |
| 107 | + </div> |
| 108 | + </div> |
| 109 | + <div className="col-md-6"> |
| 110 | + <div className="mb-3"> |
| 111 | + <label className="form-label" htmlFor="forwardHost"> |
| 112 | + <T id="proxy-host.forward-host" /> |
| 113 | + </label> |
| 114 | + <input |
| 115 | + id="forwardHost" |
| 116 | + type="text" |
| 117 | + className="form-control" |
| 118 | + required |
| 119 | + placeholder="eg: 10.0.0.1/path/" |
| 120 | + value={item.forwardHost} |
| 121 | + onChange={(e) => handleChange(idx, "forwardHost", e.target.value)} |
| 122 | + /> |
| 123 | + </div> |
| 124 | + </div> |
| 125 | + <div className="col-md-3"> |
| 126 | + <div className="mb-3"> |
| 127 | + <label className="form-label" htmlFor="forwardPort"> |
| 128 | + <T id="host.forward-port" /> |
| 129 | + </label> |
| 130 | + <input |
| 131 | + id="forwardPort" |
| 132 | + type="number" |
| 133 | + min={1} |
| 134 | + max={65535} |
| 135 | + className="form-control" |
| 136 | + required |
| 137 | + placeholder="eg: 8081" |
| 138 | + value={item.forwardPort} |
| 139 | + onChange={(e) => handleChange(idx, "forwardPort", e.target.value)} |
| 140 | + /> |
| 141 | + </div> |
| 142 | + </div> |
| 143 | + </div> |
| 144 | + {advVisible.includes(idx) && ( |
| 145 | + <div className=""> |
| 146 | + <CodeEditor |
| 147 | + language="nginx" |
| 148 | + placeholder={intl.formatMessage({ id: "nginx-config.placeholder" })} |
| 149 | + padding={15} |
| 150 | + data-color-mode="dark" |
| 151 | + minHeight={170} |
| 152 | + indentWidth={2} |
| 153 | + value={item.advancedConfig} |
| 154 | + onChange={(e) => handleChange(idx, "advancedConfig", e.target.value)} |
| 155 | + style={{ |
| 156 | + fontFamily: |
| 157 | + "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace", |
| 158 | + borderRadius: "0.3rem", |
| 159 | + minHeight: "170px", |
| 160 | + }} |
| 161 | + /> |
| 162 | + </div> |
| 163 | + )} |
| 164 | + <div className="mt-1"> |
| 165 | + <a |
| 166 | + href="#" |
| 167 | + onClick={(e) => { |
| 168 | + e.preventDefault(); |
| 169 | + handleRemove(idx); |
| 170 | + }} |
| 171 | + > |
| 172 | + <T id="action.delete" /> |
| 173 | + </a> |
| 174 | + </div> |
| 175 | + </div> |
| 176 | + </div> |
| 177 | + ))} |
| 178 | + <div> |
| 179 | + <button type="button" className="btn btn-sm" onClick={handleAdd}> |
| 180 | + <T id="action.add-location" /> |
| 181 | + </button> |
| 182 | + </div> |
| 183 | + </> |
| 184 | + ); |
| 185 | +} |
0 commit comments