diff --git a/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx b/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx new file mode 100644 index 000000000..cf2bfa54c --- /dev/null +++ b/apps/roam/src/components/settings/components/BlockPropSettingPanels.tsx @@ -0,0 +1,333 @@ +import React, { useState } from "react"; +import { + Checkbox, + InputGroup, + Label, + NumericInput, + HTMLSelect, + Button, + Tag, +} from "@blueprintjs/core"; +import Description from "roamjs-components/components/Description"; +import idToTitle from "roamjs-components/util/idToTitle"; +import { + getGlobalSetting, + setGlobalSetting, + getPersonalSetting, + setPersonalSetting, + getFeatureFlag, + setFeatureFlag, +} from "../utils/accessors"; +import type { json } from "~/utils/getBlockProps"; +import type { FeatureFlags } from "../utils/zodSchema"; + +type Getter = (keys: string[]) => T | undefined; +type Setter = (keys: string[], value: json) => void; + +type BaseProps = { + title: string; + description: string; + settingKeys: string[]; + getter: Getter; + setter: Setter; +}; + + +export const BaseTextPanel = ({ + title, + description, + settingKeys, + getter, + setter, + defaultValue = "", + placeholder, +}: BaseProps & { + defaultValue?: string; + placeholder?: string; +}) => { + const [value, setValue] = useState( + () => getter(settingKeys) ?? defaultValue, + ); + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setValue(newValue); + setter(settingKeys, newValue); + }; + + return ( + + ); +}; + +export const BaseFlagPanel = ({ + title, + description, + settingKeys, + getter, + setter, + defaultValue = false, + disabled = false, + onBeforeChange, + onChange, +}: BaseProps & { + defaultValue?: boolean; + disabled?: boolean; + onBeforeChange?: (checked: boolean) => Promise; + onChange?: (checked: boolean) => void; +}) => { + const [value, setValue] = useState( + () => getter(settingKeys) ?? defaultValue, + ); + + const handleChange = async (e: React.FormEvent) => { + const { checked } = e.target as HTMLInputElement; + + if (onBeforeChange) { + const shouldProceed = await onBeforeChange(checked); + if (!shouldProceed) return; + } + + setValue(checked); + setter(settingKeys, checked); + onChange?.(checked); + }; + + return ( + void handleChange(e)} + disabled={disabled} + labelElement={ + <> + {idToTitle(title)} + + + } + /> + ); +}; + +export const BaseNumberPanel = ({ + title, + description, + settingKeys, + getter, + setter, + defaultValue = 0, + min, + max, +}: BaseProps & { + defaultValue?: number; + min?: number; + max?: number; +}) => { + const [value, setValue] = useState( + () => getter(settingKeys) ?? defaultValue, + ); + + const handleChange = (valueAsNumber: number) => { + setValue(valueAsNumber); + setter(settingKeys, valueAsNumber); + }; + + return ( + + ); +}; + +export const BaseSelectPanel = ({ + title, + description, + settingKeys, + getter, + setter, + options, + defaultValue, +}: BaseProps & { + options: string[]; + defaultValue?: string; +}) => { + const [value, setValue] = useState( + () => getter(settingKeys) ?? defaultValue ?? options[0], + ); + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setValue(newValue); + setter(settingKeys, newValue); + }; + + return ( + + ); +}; + +export const BaseMultiTextPanel = ({ + title, + description, + settingKeys, + getter, + setter, + defaultValue = [], +}: BaseProps & { + defaultValue?: string[]; +}) => { + const [values, setValues] = useState( + () => getter(settingKeys) ?? defaultValue, + ); + const [inputValue, setInputValue] = useState(""); + + const handleAdd = () => { + if (inputValue.trim() && !values.includes(inputValue.trim())) { + const newValues = [...values, inputValue.trim()]; + setValues(newValues); + setter(settingKeys, newValues); + setInputValue(""); + } + }; + + const handleRemove = (index: number) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const newValues = values.filter((_, i) => i !== index); + setValues(newValues); + setter(settingKeys, newValues); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAdd(); + } + }; + + return ( + + ); +}; + +type WrapperProps = Omit; + +const featureFlagGetter = (keys: string[]): T | undefined => + getFeatureFlag(keys[0] as keyof FeatureFlags) as T | undefined; + +const featureFlagSetter = (keys: string[], value: json): void => + setFeatureFlag(keys[0] as keyof FeatureFlags, value as boolean); + +export const FeatureFlagPanel = ({ + title, + description, + featureKey, + onBeforeEnable, + onAfterChange, +}: { + title: string; + description: string; + featureKey: keyof FeatureFlags; + onBeforeEnable?: () => Promise; + onAfterChange?: (checked: boolean) => void; +}) => ( + (checked ? onBeforeEnable() : Promise.resolve(true)) : undefined} + onChange={onAfterChange} + /> +); + +export const GlobalTextPanel = ( + props: WrapperProps & { defaultValue?: string; placeholder?: string }, +) => ; + +export const GlobalFlagPanel = ( + props: WrapperProps & { + defaultValue?: boolean; + disabled?: boolean; + onBeforeChange?: (checked: boolean) => Promise; + onChange?: (checked: boolean) => void; + }, +) => ; + +export const GlobalNumberPanel = ( + props: WrapperProps & { defaultValue?: number; min?: number; max?: number }, +) => ; + +export const GlobalSelectPanel = ( + props: WrapperProps & { options: string[]; defaultValue?: string }, +) => ; + +export const GlobalMultiTextPanel = ( + props: WrapperProps & { defaultValue?: string[] }, +) => ; + +export const PersonalTextPanel = ( + props: WrapperProps & { defaultValue?: string; placeholder?: string }, +) => ; + +export const PersonalFlagPanel = ( + props: WrapperProps & { + defaultValue?: boolean; + disabled?: boolean; + onBeforeChange?: (checked: boolean) => Promise; + onChange?: (checked: boolean) => void; + }, +) => ; + +export const PersonalNumberPanel = ( + props: WrapperProps & { defaultValue?: number; min?: number; max?: number }, +) => ; + +export const PersonalSelectPanel = ( + props: WrapperProps & { options: string[]; defaultValue?: string }, +) => ; + +export const PersonalMultiTextPanel = ( + props: WrapperProps & { defaultValue?: string[] }, +) => ;