kopia lustrzana https://github.com/zhengkyl/qrframe
array params
rodzic
a702380045
commit
3046a2d103
|
@ -17,7 +17,7 @@ import { debounce } from "~/lib/util";
|
|||
import { Switch } from "../Switch";
|
||||
|
||||
type Props = {
|
||||
onSave: (s: string) => void;
|
||||
onSave: (s: string, thumbnail: boolean) => void;
|
||||
initialValue: string;
|
||||
};
|
||||
|
||||
|
@ -37,6 +37,8 @@ export function CodeEditor(props: Props) {
|
|||
localStorage.setItem(VIM_MODE_KEY, v ? "true" : "false");
|
||||
};
|
||||
|
||||
const [updateThumbnail, setUpdateThumbnail] = createSignal(true);
|
||||
|
||||
const [dirty, setDirty] = createSignal(false);
|
||||
|
||||
const extensions = [
|
||||
|
@ -54,7 +56,7 @@ export function CodeEditor(props: Props) {
|
|||
key: "Mod-s",
|
||||
linux: "Ctrl-s", // untested, but might be necessary
|
||||
run: (view) => {
|
||||
props.onSave(view.state.doc.toString());
|
||||
props.onSave(view.state.doc.toString(), updateThumbnail());
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
@ -134,9 +136,20 @@ export function CodeEditor(props: Props) {
|
|||
onChange={(e) => setVimMode(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
<label class="flex items-center gap-1 text-sm">
|
||||
Update thumbnail
|
||||
<input
|
||||
class="h-4 w-4"
|
||||
type="checkbox"
|
||||
checked={updateThumbnail()}
|
||||
onChange={(e) => setUpdateThumbnail(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
<Button
|
||||
disabled={!dirty()}
|
||||
onMouseDown={() => props.onSave(view.state.doc.toString())}
|
||||
onMouseDown={() =>
|
||||
props.onSave(view.state.doc.toString(), updateThumbnail())
|
||||
}
|
||||
class="bg-green-700 border rounded-md hover:bg-green-700/90 focus-visible:(outline-none ring-2 ring-fore-base ring-offset-2 ring-offset-back-base) disabled:(bg-transparent text-fore-base pointer-events-none opacity-50) transition-colors px-3 min-w-150px"
|
||||
>
|
||||
{dirty() ? "Save" : "No changes"}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Dynamic } from "solid-js/web";
|
|||
import {
|
||||
PARAM_COMPONENTS,
|
||||
defaultParams,
|
||||
paramsEqual,
|
||||
deepEqualObj,
|
||||
parseParamsSchema,
|
||||
type ParamsSchema,
|
||||
} from "~/lib/params";
|
||||
|
@ -19,6 +19,8 @@ import { TextInput, TextareaInput } from "../TextInput";
|
|||
import { CodeEditor } from "./CodeEditor";
|
||||
import { Settings } from "./Settings";
|
||||
import { clearToasts, toastError } from "../ErrorToasts";
|
||||
import Minus from "lucide-solid/icons/minus";
|
||||
import Plus from "lucide-solid/icons/plus";
|
||||
|
||||
type Props = {
|
||||
class?: string;
|
||||
|
@ -129,13 +131,13 @@ export function Editor(props: Props) {
|
|||
const setExistingKey = (key: string) => {
|
||||
setRenderKey(key);
|
||||
if (isPreset(key)) {
|
||||
saveAndRun(PRESET_CODE[key], false);
|
||||
saveAndRun(PRESET_CODE[key], false, false);
|
||||
} else {
|
||||
let storedCode = localStorage.getItem(key);
|
||||
if (storedCode == null) {
|
||||
storedCode = `Failed to load ${key}`;
|
||||
}
|
||||
saveAndRun(storedCode, false);
|
||||
saveAndRun(storedCode, false, false);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -170,19 +172,23 @@ export function Editor(props: Props) {
|
|||
return { type, url, parsedParamsSchema };
|
||||
};
|
||||
|
||||
const saveAndRun = async (code: string, changed: boolean) => {
|
||||
const saveAndRun = async (
|
||||
code: string,
|
||||
save: boolean,
|
||||
thumbnail: boolean
|
||||
) => {
|
||||
try {
|
||||
setCode(code);
|
||||
if (changed) {
|
||||
if (save) {
|
||||
localStorage.setItem(renderKey(), code);
|
||||
}
|
||||
|
||||
const { type, url, parsedParamsSchema } = await importCode(code);
|
||||
clearToasts()
|
||||
clearToasts();
|
||||
|
||||
// batched b/c trigger rendering effect
|
||||
batch(() => {
|
||||
if (!paramsEqual(parsedParamsSchema, paramsSchema())) {
|
||||
if (!deepEqualObj(parsedParamsSchema, paramsSchema())) {
|
||||
setParams(defaultParams(parsedParamsSchema));
|
||||
}
|
||||
setParamsSchema(parsedParamsSchema); // always update in case different property order
|
||||
|
@ -195,12 +201,12 @@ export function Editor(props: Props) {
|
|||
});
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
if (thumbnail) {
|
||||
asyncUpdateThumbnail(renderKey(), type, url, parsedParamsSchema);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("e", e!.toString());
|
||||
toastError("Invalid code", e!.toString())
|
||||
toastError("Invalid code", e!.toString());
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -280,7 +286,7 @@ export function Editor(props: Props) {
|
|||
|
||||
setThumbs(key, LOADING_THUMB);
|
||||
setRenderKey(key);
|
||||
saveAndRun(code, true);
|
||||
saveAndRun(code, true, true);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -431,11 +437,64 @@ export function Editor(props: Props) {
|
|||
<div class="flex flex-col gap-2 mb-4">
|
||||
<For each={Object.entries(paramsSchema())}>
|
||||
{([label, { type, ...props }]) => {
|
||||
if (type === "Array") {
|
||||
return (
|
||||
<div>
|
||||
<div class="grid grid-cols-[144px_1fr] justify-items-end gap-y-2">
|
||||
<div class="text-sm py-2 w-36">{label}</div>
|
||||
<div class="flex gap-1">
|
||||
<Show when={props.resizable}>
|
||||
<FlatButton
|
||||
class="p-1.5"
|
||||
onClick={() =>
|
||||
setParams(label, (prev: any[]) =>
|
||||
prev.slice(0, -1)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Minus />
|
||||
</FlatButton>
|
||||
<FlatButton
|
||||
class="p-1.5"
|
||||
onClick={() =>
|
||||
setParams(label, (prev: any[]) => [
|
||||
...prev,
|
||||
props.props.default,
|
||||
])
|
||||
}
|
||||
>
|
||||
<Plus />
|
||||
</FlatButton>
|
||||
</Show>
|
||||
</div>
|
||||
<For each={params[label]}>
|
||||
{(v, i) => (
|
||||
<>
|
||||
<div class="text-sm py-2 pl-4 w-full text-left">
|
||||
{i()}
|
||||
</div>
|
||||
<Dynamic
|
||||
component={
|
||||
PARAM_COMPONENTS[
|
||||
props.props
|
||||
.type as keyof typeof PARAM_COMPONENTS
|
||||
]
|
||||
}
|
||||
{...props.props}
|
||||
value={v}
|
||||
setValue={(v: any) => setParams(label, i(), v)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div class="flex justify-between">
|
||||
<div class="text-sm py-2 w-48">{label}</div>
|
||||
{/* @ts-expect-error lose type b/c type and props destructured */}
|
||||
<div class="text-sm py-2 w-36 shrink-0">{label}</div>
|
||||
<Dynamic
|
||||
component={PARAM_COMPONENTS[type]}
|
||||
{...props}
|
||||
|
@ -450,11 +509,11 @@ export function Editor(props: Props) {
|
|||
</div>
|
||||
<CodeEditor
|
||||
initialValue={code()}
|
||||
onSave={(code) => {
|
||||
onSave={(code, updateThumbnail) => {
|
||||
if (presetKeys.includes(renderKey())) {
|
||||
createAndSelectFunc(renderKey(), code);
|
||||
} else {
|
||||
saveAndRun(code, true);
|
||||
saveAndRun(code, true, updateThumbnail);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
import type { Params } from "~/lib/params";
|
||||
import { FlatButton } from "../Button";
|
||||
import { clearToasts, toastError } from "../ErrorToasts";
|
||||
import { unwrap } from "solid-js/store";
|
||||
|
||||
function download(href: string, name: string) {
|
||||
const a = document.createElement("a");
|
||||
|
@ -85,8 +86,10 @@ function RenderedQrCode() {
|
|||
|
||||
// Track store without leaking extra params
|
||||
const paramsCopy: Params = {};
|
||||
const unwrapped = unwrap(params)
|
||||
Object.keys(paramsSchema()).forEach((key) => {
|
||||
paramsCopy[key] = params[key];
|
||||
params[key]; // access to track
|
||||
paramsCopy[key] = unwrapped[key];
|
||||
});
|
||||
|
||||
// all reactive deps must be above early return!
|
||||
|
|
|
@ -4,7 +4,7 @@ import { NumberInput } from "~/components/NumberInput";
|
|||
import { Select } from "~/components/Select";
|
||||
import { Switch } from "~/components/Switch";
|
||||
|
||||
const PARAM_TYPES = ["boolean", "number", "Color", "Select", "File"];
|
||||
const PARAM_TYPES = ["boolean", "number", "Color", "Select", "File", "Array"];
|
||||
|
||||
export const PARAM_COMPONENTS = {
|
||||
boolean: Switch,
|
||||
|
@ -17,38 +17,51 @@ export const PARAM_COMPONENTS = {
|
|||
export const PARAM_DEFAULTS = {
|
||||
boolean: false,
|
||||
number: 0,
|
||||
Color: "rgb(0,0,0)",
|
||||
Color: "#000000",
|
||||
// Select: //default is first option
|
||||
File: null,
|
||||
Array: [],
|
||||
};
|
||||
|
||||
export function paramsEqual(
|
||||
paramsSchemaA: ParamsSchema,
|
||||
paramsSchemaB: ParamsSchema
|
||||
) {
|
||||
const labelsA = Object.keys(paramsSchemaA);
|
||||
if (labelsA.length !== Object.keys(paramsSchemaB).length) return false;
|
||||
type PARAM_VALUE_TYPES = {
|
||||
boolean: boolean;
|
||||
number: number;
|
||||
Color: string;
|
||||
Select: any;
|
||||
File: File | null;
|
||||
Array: any[];
|
||||
};
|
||||
|
||||
for (const label of labelsA) {
|
||||
if (!paramsSchemaB.hasOwnProperty(label)) return false;
|
||||
export type Params = {
|
||||
[key: string]: PARAM_VALUE_TYPES[keyof PARAM_VALUE_TYPES];
|
||||
};
|
||||
|
||||
const propsA = Object.keys(paramsSchemaA[label]);
|
||||
if (propsA.length != Object.keys(paramsSchemaB[label]).length) return false;
|
||||
export type ParamsSchema = {
|
||||
[label: string]: Required<RawParamsSchema>;
|
||||
};
|
||||
|
||||
for (const prop of propsA) {
|
||||
if (prop === "options") {
|
||||
// @ts-expect-error Object.keys() returns keys
|
||||
const optionsA = paramsSchemaA[label][prop] as string[];
|
||||
// @ts-expect-error Object.keys() returns keys
|
||||
const optionsB = paramsSchemaA[label][prop] as string[];
|
||||
export type RawParamsSchema = {
|
||||
type: keyof PARAM_VALUE_TYPES;
|
||||
default?: any;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
if (optionsA.length !== optionsB.length) return false;
|
||||
if (optionsA.some((option) => !optionsB.includes(option))) return false;
|
||||
} else {
|
||||
// @ts-expect-error Object.keys() returns keys
|
||||
if (paramsSchemaA[label][prop] !== paramsSchemaB[label][prop])
|
||||
return false;
|
||||
}
|
||||
export function deepEqualObj(a: any, b: any) {
|
||||
if (Object.keys(a).length !== Object.keys(b).length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key in a) {
|
||||
if (!b.hasOwnProperty(key)) return false;
|
||||
if (
|
||||
typeof a[key] === "object" &&
|
||||
a[key] != null &&
|
||||
typeof b[key] === "object" &&
|
||||
b[key] != null
|
||||
) {
|
||||
if (!deepEqualObj(a[key], b[key])) return false;
|
||||
} else if (a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,42 +76,59 @@ export function parseParamsSchema(rawParamsSchema: any) {
|
|||
let parsedParamsSchema: ParamsSchema = {};
|
||||
if (typeof rawParamsSchema === "object") {
|
||||
for (const [key, value] of Object.entries(rawParamsSchema)) {
|
||||
if (
|
||||
value == null ||
|
||||
typeof value !== "object" ||
|
||||
!("type" in value) ||
|
||||
typeof value.type !== "string" ||
|
||||
!PARAM_TYPES.includes(value.type)
|
||||
) {
|
||||
continue;
|
||||
} else if (value.type === "Select") {
|
||||
if (
|
||||
!("options" in value) ||
|
||||
!Array.isArray(value.options) ||
|
||||
value.options.length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// === undefined b/c null is a valid default value for ImageInput
|
||||
// @ts-expect-error yes .default might be undefined thanks typescript
|
||||
if (value.default === undefined) {
|
||||
if (value.type === "Select") {
|
||||
// @ts-expect-error adding default, options validated above
|
||||
value.default = value.options[0];
|
||||
} else {
|
||||
// @ts-expect-error adding default, type validated above
|
||||
value.default = PARAM_DEFAULTS[value.type];
|
||||
}
|
||||
}
|
||||
// TODO so user set default could be wrong type
|
||||
// @ts-expect-error prop types not validated
|
||||
parsedParamsSchema[key] = value;
|
||||
const parsedField = parseField(value);
|
||||
if (parsedField == null) continue;
|
||||
parsedParamsSchema[key] = parsedField;
|
||||
}
|
||||
}
|
||||
return parsedParamsSchema;
|
||||
}
|
||||
|
||||
function parseField(value: any) {
|
||||
if (
|
||||
value == null ||
|
||||
typeof value !== "object" ||
|
||||
!("type" in value) ||
|
||||
typeof value.type !== "string" ||
|
||||
!PARAM_TYPES.includes(value.type)
|
||||
) {
|
||||
return null;
|
||||
} else if (value.type === "Select") {
|
||||
if (
|
||||
!("options" in value) ||
|
||||
!Array.isArray(value.options) ||
|
||||
value.options.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
} else if (value.type === "Array") {
|
||||
if (!("props" in value)) {
|
||||
return null;
|
||||
}
|
||||
const innerProps = parseField(value.props);
|
||||
if (innerProps == null || innerProps.default === undefined) {
|
||||
return null;
|
||||
}
|
||||
value.props = innerProps;
|
||||
}
|
||||
|
||||
// === undefined b/c null is a valid default value for ImageInput
|
||||
if (value.default === undefined) {
|
||||
if (value.type === "Select") {
|
||||
value.default = value.options[0];
|
||||
} else if (value.type === "Array") {
|
||||
value.default = Array.from(
|
||||
{ length: value.defaultLength ?? 1 },
|
||||
() => value.props.default
|
||||
);
|
||||
} else {
|
||||
// @ts-expect-error adding default, type validated above
|
||||
value.default = PARAM_DEFAULTS[value.type];
|
||||
}
|
||||
}
|
||||
|
||||
return parsedParamsSchema;
|
||||
return value;
|
||||
}
|
||||
|
||||
export function defaultParams(paramsSchema: ParamsSchema) {
|
||||
|
@ -108,50 +138,3 @@ export function defaultParams(paramsSchema: ParamsSchema) {
|
|||
});
|
||||
return defaultParams;
|
||||
}
|
||||
|
||||
type PARAM_VALUE_TYPES = {
|
||||
boolean: boolean;
|
||||
number: number;
|
||||
Color: string;
|
||||
Select: never; // see Params, uses options field instead of this mapping
|
||||
File: File | null;
|
||||
};
|
||||
|
||||
export type Params<T extends RawParamsSchema = ParamsSchema> = {
|
||||
[K in keyof T]: T[K] extends { type: "Select" }
|
||||
? T[K]["options"][number]
|
||||
: PARAM_VALUE_TYPES[T[K]["type"]];
|
||||
} & {}; // & {} necessary for readable typehints, see ts "prettify"
|
||||
|
||||
export type ParamsSchema = {
|
||||
[label: string]: Required<RawParamsSchema[string]>;
|
||||
};
|
||||
|
||||
export type RawParamsSchema = SchemaFromMapping<typeof PARAM_COMPONENTS>;
|
||||
|
||||
/**
|
||||
* Given object mapping keys to components, returns union of [key, props]
|
||||
*/
|
||||
type Step1<T extends { [key: keyof any]: (...args: any) => any }> = {
|
||||
[K in keyof T]: [K, Parameters<T[K]>[0]];
|
||||
}[keyof T];
|
||||
|
||||
/**
|
||||
* Given union of [key, props]:
|
||||
*
|
||||
* adds default to props based on type of value and removes value and setValue from props
|
||||
*/
|
||||
type Step2<T extends [keyof any, { [prop: string]: any }]> = T extends any
|
||||
? [T[0], Omit<T[1], "value" | "setValue"> & { default?: T[1]["value"] }]
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Converts each tuple to an object with a type corresponding to key
|
||||
* (discriminated union of component props) assignable to any label
|
||||
*/
|
||||
type Step3<T extends [keyof any, { [prop: string]: any }]> = {
|
||||
[label: string]: T extends any ? { type: T[0] } & T[1] : never;
|
||||
};
|
||||
|
||||
type SchemaFromMapping<T extends { [key: string]: (...args: any) => any }> =
|
||||
Step3<Step2<Step1<T>>>;
|
||||
|
|
Ładowanie…
Reference in New Issue