merge newui branch
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { input } from "./input";
|
||||
import { onrecordsave } from "./onrecordsave";
|
||||
import { settings } from "./settings";
|
||||
import { view } from "./view";
|
||||
|
||||
window.app = window.app || {};
|
||||
window.app.fieldTypes = window.app.fieldTypes || {};
|
||||
window.app.fieldTypes.json = {
|
||||
icon: "ri-braces-line",
|
||||
label: "JSON",
|
||||
settings,
|
||||
input,
|
||||
view,
|
||||
onrecordsave,
|
||||
dummyData: (f, forSubmit = false) => {
|
||||
return { "example": 123 };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
// {
|
||||
// collection: undefined,
|
||||
// originalRecord: undefined,
|
||||
// record: undefined,
|
||||
// field: undefined,
|
||||
// }
|
||||
export function input(props) {
|
||||
const uniqueId = "json_" + app.utils.randomString();
|
||||
|
||||
const local = store({
|
||||
value: "",
|
||||
});
|
||||
|
||||
const watchers = [
|
||||
watch(
|
||||
() => props.record[props.field.name],
|
||||
(newVal, oldVal) => {
|
||||
if (newVal !== "" && newVal === local.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// quote string values if not already
|
||||
if (typeof newVal == "string" && !newVal.startsWith("\"") && !newVal.endsWith("\"")) {
|
||||
local.value = JSON.stringify(typeof newVal === "undefined" ? null : newVal);
|
||||
props.record[props.field.name] = local.value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof newVal == "string" && newVal.startsWith("\"") && newVal.endsWith("\"")) {
|
||||
local.value = newVal; // already double quoted
|
||||
} else if (newVal === null) {
|
||||
local.value = "null";
|
||||
} else {
|
||||
local.value = JSON.stringify(typeof newVal === "undefined" ? null : newVal, null, 2);
|
||||
}
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
function updateRecordValue() {
|
||||
const trimmed = local.value.trim();
|
||||
|
||||
if (trimmed === "") {
|
||||
props.record[props.field.name] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let parsed = JSON.parse(trimmed);
|
||||
if (typeof parsed == "string") {
|
||||
props.record[props.field.name] = JSON.stringify(parsed);
|
||||
} else {
|
||||
props.record[props.field.name] = parsed;
|
||||
}
|
||||
} catch (_) {
|
||||
props.record[props.field.name] = trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
let updateRecordValueTimeoutId;
|
||||
|
||||
return t.div(
|
||||
{ className: "record-field-input field-type-json" },
|
||||
t.div(
|
||||
{
|
||||
className: "field",
|
||||
onunmount: () => {
|
||||
clearTimeout(updateRecordValueTimeoutId);
|
||||
watchers.forEach((w) => w?.unwatch());
|
||||
},
|
||||
},
|
||||
t.label(
|
||||
{ htmlFor: uniqueId },
|
||||
t.i({ className: app.fieldTypes.json.icon }),
|
||||
t.span({ className: "txt" }, () => props.field.name),
|
||||
t.span(
|
||||
{
|
||||
hidden: () => isValidStringifiedJSON(local.value.trim()),
|
||||
className: "json-state",
|
||||
ariaDescription: app.attrs.tooltip("Invalid JSON", "left"),
|
||||
},
|
||||
t.i({ className: "ri-error-warning-fill txt-danger" }),
|
||||
),
|
||||
t.span(
|
||||
{
|
||||
hidden: () => !isValidStringifiedJSON(local.value.trim()),
|
||||
className: "json-state",
|
||||
ariaDescription: app.attrs.tooltip("Valid JSON", "left"),
|
||||
},
|
||||
t.i({ className: "ri-checkbox-circle-fill txt-success" }),
|
||||
),
|
||||
),
|
||||
app.components.codeEditor({
|
||||
language: "js",
|
||||
id: uniqueId,
|
||||
name: () => props.field.name,
|
||||
required: () => props.field.required,
|
||||
value: () => local.value,
|
||||
oninput: (val) => (local.value = val),
|
||||
onblur: () => updateRecordValue(),
|
||||
}),
|
||||
),
|
||||
() => {
|
||||
if (props.field.help) {
|
||||
return t.div({ className: "field-help" }, props.field.help);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function isValidStringifiedJSON(val) {
|
||||
if (val === "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(val);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
// {
|
||||
// originalRecord: undefined,
|
||||
// record: undefined,
|
||||
// field: undefined,
|
||||
// payload: {},
|
||||
// }
|
||||
export function onrecordsave(props) {
|
||||
try {
|
||||
const val = props.record[props.field.name];
|
||||
if (typeof val == "string") {
|
||||
JSON.parse(val);
|
||||
}
|
||||
} catch (err) {
|
||||
// simulate API error
|
||||
throw new ClientResponseError({
|
||||
status: 400,
|
||||
response: {
|
||||
message: "Invalid JSON data",
|
||||
data: {
|
||||
[props.field.name]: {
|
||||
code: "invalid_json",
|
||||
message: err.toString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// {
|
||||
// originalCollection: undefined,
|
||||
// collection: undefined,
|
||||
// field
|
||||
// get fieldIndex: int/-1,
|
||||
// get originalField: undefined
|
||||
// }
|
||||
export function settings(data) {
|
||||
const uniqueId = "f_" + app.utils.randomString();
|
||||
|
||||
const local = store({
|
||||
showInfo: false,
|
||||
});
|
||||
|
||||
return app.components.fieldSettings(data, {
|
||||
content: () =>
|
||||
t.div(
|
||||
{ className: "grid sm" },
|
||||
t.div(
|
||||
{ className: "col-sm-12" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
t.label(
|
||||
{ htmlFor: uniqueId + ".maxSize" },
|
||||
t.span(null, "Max size "),
|
||||
t.small(null, "(bytes)"),
|
||||
),
|
||||
t.input({
|
||||
type: "number",
|
||||
id: uniqueId + ".maxSize",
|
||||
name: () => `fields.${data.fieldIndex}.maxSize`,
|
||||
min: 0,
|
||||
step: 1,
|
||||
max: Number.MAX_SAFE_INTEGER,
|
||||
placeholder: "Default to max ~1MB",
|
||||
value: () => data.field.maxSize || "",
|
||||
oninput: (e) => {
|
||||
data.field.maxSize = parseInt(e.target.value, 10);
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
t.div(
|
||||
{ className: "col-sm-12" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
t.label({ htmlFor: uniqueId + ".help" }, "Help text"),
|
||||
t.input({
|
||||
type: "text",
|
||||
id: uniqueId + ".help",
|
||||
name: () => `fields.${data.fieldIndex}.help`,
|
||||
value: () => data.field.help || "",
|
||||
oninput: (e) => (data.field.help = e.target.value),
|
||||
}),
|
||||
),
|
||||
),
|
||||
t.div(
|
||||
{ className: "col-sm-12" },
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: () => `btn sm secondary ${local.showInfo ? "" : "transparent"}`,
|
||||
onclick: () => (local.showInfo = !local.showInfo),
|
||||
},
|
||||
t.span({ className: "txt" }, "String value normalizations"),
|
||||
t.i({
|
||||
className: () => (local.showInfo ? "ri-arrow-up-s-line" : "ri-arrow-down-s-line"),
|
||||
}),
|
||||
),
|
||||
app.components.slide(
|
||||
() => local.showInfo,
|
||||
t.div(
|
||||
{ className: "alert m-t-10 info" },
|
||||
t.div(
|
||||
{ className: "content" },
|
||||
"In order to support seamlessly both ",
|
||||
t.code(null, "application/json"),
|
||||
" and ",
|
||||
t.code(null, "multipart/form-data"),
|
||||
"requests, the following normalization rules are applied if the ",
|
||||
t.code(null, "json"),
|
||||
" field is a plain string:",
|
||||
t.ul(
|
||||
null,
|
||||
t.li(null, `"true" is converted to the json `, t.code(null, "true")),
|
||||
t.li(null, `"false" is converted to the json `, t.code(null, "false")),
|
||||
t.li(null, `"null" is converted to the json `, t.code(null, "null")),
|
||||
t.li(null, `"[1,2,3]" is converted to the json `, t.code(null, "[1,2,3]")),
|
||||
t.li(
|
||||
null,
|
||||
`'{"a":1,"b":2}' is converted to the json `,
|
||||
t.code(null, `{"a":1,"b":2}`),
|
||||
),
|
||||
t.li(null, `numeric strings are converted to json number`),
|
||||
t.li(
|
||||
null,
|
||||
`double quoted strings are left as they are (aka. without normalizations)`,
|
||||
),
|
||||
t.li(null, `any other string (empty string too) is double quoted`),
|
||||
),
|
||||
"Alternatively, if you want to avoid the string value normalizations, you can wrap your data inside an object, eg. ",
|
||||
t.code(null, "{\"data\": anything}"),
|
||||
".",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
footer: () => [
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
t.input({
|
||||
className: "sm",
|
||||
type: "checkbox",
|
||||
id: uniqueId + ".required",
|
||||
name: () => `fields.${data.fieldIndex}.required`,
|
||||
checked: () => !!data.field.required,
|
||||
onchange: (e) => (data.field.required = e.target.checked),
|
||||
}),
|
||||
t.label(
|
||||
{ htmlFor: uniqueId + ".required" },
|
||||
t.span({ className: "txt" }, "Required"),
|
||||
t.i({
|
||||
className: "ri-information-line link-hint",
|
||||
ariaDescription: app.attrs.tooltip("Requires the field value NOT to be null, '', [], {}"),
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// {
|
||||
// record: undefined,
|
||||
// field: undefined,
|
||||
// short: false,
|
||||
// }
|
||||
export function view(props) {
|
||||
return t.div({ className: "record-field-view field-type-json" }, () => {
|
||||
const rawValue = props.record[props.field.name];
|
||||
|
||||
if (props.short) {
|
||||
return t.span({
|
||||
className: "txt-code txt-ellipsis",
|
||||
textContent: app.utils.truncate(app.utils.trimQuotedValue(JSON.stringify(rawValue)) || ""),
|
||||
});
|
||||
}
|
||||
|
||||
return app.components.codeBlock({
|
||||
value: () => JSON.stringify(rawValue, null, 2),
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user