Skip to content

Commit 0fb7fc7

Browse files
committed
common block-prop based components to replace roam-js components
1 parent bd6a3cf commit 0fb7fc7

File tree

1 file changed

+333
-0
lines changed

1 file changed

+333
-0
lines changed
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
import React, { useState } from "react";
2+
import {
3+
Checkbox,
4+
InputGroup,
5+
Label,
6+
NumericInput,
7+
HTMLSelect,
8+
Button,
9+
Tag,
10+
} from "@blueprintjs/core";
11+
import Description from "roamjs-components/components/Description";
12+
import idToTitle from "roamjs-components/util/idToTitle";
13+
import {
14+
getGlobalSetting,
15+
setGlobalSetting,
16+
getPersonalSetting,
17+
setPersonalSetting,
18+
getFeatureFlag,
19+
setFeatureFlag,
20+
} from "../utils/accessors";
21+
import type { json } from "~/utils/getBlockProps";
22+
import type { FeatureFlags } from "../utils/zodSchema";
23+
24+
type Getter = <T>(keys: string[]) => T | undefined;
25+
type Setter = (keys: string[], value: json) => void;
26+
27+
type BaseProps = {
28+
title: string;
29+
description: string;
30+
settingKeys: string[];
31+
getter: Getter;
32+
setter: Setter;
33+
};
34+
35+
36+
export const BaseTextPanel = ({
37+
title,
38+
description,
39+
settingKeys,
40+
getter,
41+
setter,
42+
defaultValue = "",
43+
placeholder,
44+
}: BaseProps & {
45+
defaultValue?: string;
46+
placeholder?: string;
47+
}) => {
48+
const [value, setValue] = useState(
49+
() => getter<string>(settingKeys) ?? defaultValue,
50+
);
51+
52+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
53+
const newValue = e.target.value;
54+
setValue(newValue);
55+
setter(settingKeys, newValue);
56+
};
57+
58+
return (
59+
<Label>
60+
{idToTitle(title)}
61+
<Description description={description} />
62+
<InputGroup
63+
value={value}
64+
onChange={handleChange}
65+
placeholder={placeholder || defaultValue}
66+
/>
67+
</Label>
68+
);
69+
};
70+
71+
export const BaseFlagPanel = ({
72+
title,
73+
description,
74+
settingKeys,
75+
getter,
76+
setter,
77+
defaultValue = false,
78+
disabled = false,
79+
onBeforeChange,
80+
onChange,
81+
}: BaseProps & {
82+
defaultValue?: boolean;
83+
disabled?: boolean;
84+
onBeforeChange?: (checked: boolean) => Promise<boolean>;
85+
onChange?: (checked: boolean) => void;
86+
}) => {
87+
const [value, setValue] = useState(
88+
() => getter<boolean>(settingKeys) ?? defaultValue,
89+
);
90+
91+
const handleChange = async (e: React.FormEvent<HTMLInputElement>) => {
92+
const { checked } = e.target as HTMLInputElement;
93+
94+
if (onBeforeChange) {
95+
const shouldProceed = await onBeforeChange(checked);
96+
if (!shouldProceed) return;
97+
}
98+
99+
setValue(checked);
100+
setter(settingKeys, checked);
101+
onChange?.(checked);
102+
};
103+
104+
return (
105+
<Checkbox
106+
checked={value}
107+
onChange={(e) => void handleChange(e)}
108+
disabled={disabled}
109+
labelElement={
110+
<>
111+
{idToTitle(title)}
112+
<Description description={description} />
113+
</>
114+
}
115+
/>
116+
);
117+
};
118+
119+
export const BaseNumberPanel = ({
120+
title,
121+
description,
122+
settingKeys,
123+
getter,
124+
setter,
125+
defaultValue = 0,
126+
min,
127+
max,
128+
}: BaseProps & {
129+
defaultValue?: number;
130+
min?: number;
131+
max?: number;
132+
}) => {
133+
const [value, setValue] = useState(
134+
() => getter<number>(settingKeys) ?? defaultValue,
135+
);
136+
137+
const handleChange = (valueAsNumber: number) => {
138+
setValue(valueAsNumber);
139+
setter(settingKeys, valueAsNumber);
140+
};
141+
142+
return (
143+
<Label>
144+
{idToTitle(title)}
145+
<Description description={description} />
146+
<NumericInput
147+
value={value}
148+
onValueChange={handleChange}
149+
min={min}
150+
max={max}
151+
fill
152+
/>
153+
</Label>
154+
);
155+
};
156+
157+
export const BaseSelectPanel = ({
158+
title,
159+
description,
160+
settingKeys,
161+
getter,
162+
setter,
163+
options,
164+
defaultValue,
165+
}: BaseProps & {
166+
options: string[];
167+
defaultValue?: string;
168+
}) => {
169+
const [value, setValue] = useState(
170+
() => getter<string>(settingKeys) ?? defaultValue ?? options[0],
171+
);
172+
173+
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
174+
const newValue = e.target.value;
175+
setValue(newValue);
176+
setter(settingKeys, newValue);
177+
};
178+
179+
return (
180+
<Label>
181+
{idToTitle(title)}
182+
<Description description={description} />
183+
<HTMLSelect value={value} onChange={handleChange} fill options={options} />
184+
</Label>
185+
);
186+
};
187+
188+
export const BaseMultiTextPanel = ({
189+
title,
190+
description,
191+
settingKeys,
192+
getter,
193+
setter,
194+
defaultValue = [],
195+
}: BaseProps & {
196+
defaultValue?: string[];
197+
}) => {
198+
const [values, setValues] = useState<string[]>(
199+
() => getter<string[]>(settingKeys) ?? defaultValue,
200+
);
201+
const [inputValue, setInputValue] = useState("");
202+
203+
const handleAdd = () => {
204+
if (inputValue.trim() && !values.includes(inputValue.trim())) {
205+
const newValues = [...values, inputValue.trim()];
206+
setValues(newValues);
207+
setter(settingKeys, newValues);
208+
setInputValue("");
209+
}
210+
};
211+
212+
const handleRemove = (index: number) => {
213+
// eslint-disable-next-line @typescript-eslint/naming-convention
214+
const newValues = values.filter((_, i) => i !== index);
215+
setValues(newValues);
216+
setter(settingKeys, newValues);
217+
};
218+
219+
const handleKeyDown = (e: React.KeyboardEvent) => {
220+
if (e.key === "Enter") {
221+
e.preventDefault();
222+
handleAdd();
223+
}
224+
};
225+
226+
return (
227+
<Label>
228+
{idToTitle(title)}
229+
<Description description={description} />
230+
<div className="flex gap-2">
231+
<InputGroup
232+
value={inputValue}
233+
onChange={(e) => setInputValue(e.target.value)}
234+
onKeyDown={handleKeyDown}
235+
placeholder="Add new item..."
236+
className="flex-grow"
237+
/>
238+
<Button icon="plus" onClick={handleAdd} disabled={!inputValue.trim()} />
239+
</div>
240+
{values.length > 0 && (
241+
<div className="mt-2 flex flex-wrap gap-1">
242+
{values.map((v, i) => (
243+
<Tag key={i} onRemove={() => handleRemove(i)} minimal>
244+
{v}
245+
</Tag>
246+
))}
247+
</div>
248+
)}
249+
</Label>
250+
);
251+
};
252+
253+
type WrapperProps = Omit<BaseProps, "getter" | "setter">;
254+
255+
const featureFlagGetter = <T,>(keys: string[]): T | undefined =>
256+
getFeatureFlag(keys[0] as keyof FeatureFlags) as T | undefined;
257+
258+
const featureFlagSetter = (keys: string[], value: json): void =>
259+
setFeatureFlag(keys[0] as keyof FeatureFlags, value as boolean);
260+
261+
export const FeatureFlagPanel = ({
262+
title,
263+
description,
264+
featureKey,
265+
onBeforeEnable,
266+
onAfterChange,
267+
}: {
268+
title: string;
269+
description: string;
270+
featureKey: keyof FeatureFlags;
271+
onBeforeEnable?: () => Promise<boolean>;
272+
onAfterChange?: (checked: boolean) => void;
273+
}) => (
274+
<BaseFlagPanel
275+
title={title}
276+
description={description}
277+
settingKeys={[featureKey]}
278+
getter={featureFlagGetter}
279+
setter={featureFlagSetter}
280+
onBeforeChange={onBeforeEnable ? (checked) => (checked ? onBeforeEnable() : Promise.resolve(true)) : undefined}
281+
onChange={onAfterChange}
282+
/>
283+
);
284+
285+
export const GlobalTextPanel = (
286+
props: WrapperProps & { defaultValue?: string; placeholder?: string },
287+
) => <BaseTextPanel {...props} getter={getGlobalSetting} setter={setGlobalSetting} />;
288+
289+
export const GlobalFlagPanel = (
290+
props: WrapperProps & {
291+
defaultValue?: boolean;
292+
disabled?: boolean;
293+
onBeforeChange?: (checked: boolean) => Promise<boolean>;
294+
onChange?: (checked: boolean) => void;
295+
},
296+
) => <BaseFlagPanel {...props} getter={getGlobalSetting} setter={setGlobalSetting} />;
297+
298+
export const GlobalNumberPanel = (
299+
props: WrapperProps & { defaultValue?: number; min?: number; max?: number },
300+
) => <BaseNumberPanel {...props} getter={getGlobalSetting} setter={setGlobalSetting} />;
301+
302+
export const GlobalSelectPanel = (
303+
props: WrapperProps & { options: string[]; defaultValue?: string },
304+
) => <BaseSelectPanel {...props} getter={getGlobalSetting} setter={setGlobalSetting} />;
305+
306+
export const GlobalMultiTextPanel = (
307+
props: WrapperProps & { defaultValue?: string[] },
308+
) => <BaseMultiTextPanel {...props} getter={getGlobalSetting} setter={setGlobalSetting} />;
309+
310+
export const PersonalTextPanel = (
311+
props: WrapperProps & { defaultValue?: string; placeholder?: string },
312+
) => <BaseTextPanel {...props} getter={getPersonalSetting} setter={setPersonalSetting} />;
313+
314+
export const PersonalFlagPanel = (
315+
props: WrapperProps & {
316+
defaultValue?: boolean;
317+
disabled?: boolean;
318+
onBeforeChange?: (checked: boolean) => Promise<boolean>;
319+
onChange?: (checked: boolean) => void;
320+
},
321+
) => <BaseFlagPanel {...props} getter={getPersonalSetting} setter={setPersonalSetting} />;
322+
323+
export const PersonalNumberPanel = (
324+
props: WrapperProps & { defaultValue?: number; min?: number; max?: number },
325+
) => <BaseNumberPanel {...props} getter={getPersonalSetting} setter={setPersonalSetting} />;
326+
327+
export const PersonalSelectPanel = (
328+
props: WrapperProps & { options: string[]; defaultValue?: string },
329+
) => <BaseSelectPanel {...props} getter={getPersonalSetting} setter={setPersonalSetting} />;
330+
331+
export const PersonalMultiTextPanel = (
332+
props: WrapperProps & { defaultValue?: string[] },
333+
) => <BaseMultiTextPanel {...props} getter={getPersonalSetting} setter={setPersonalSetting} />;

0 commit comments

Comments
 (0)