[#3191] added client-side validation and syntax highlight for the json field

This commit is contained in:
Gani Georgiev
2023-08-29 22:10:57 +03:00
parent 7d10b3c502
commit a394777264
39 changed files with 437 additions and 317 deletions
+3
View File
@@ -53,6 +53,7 @@
closeBracketsKeymap,
} from "@codemirror/autocomplete";
import { html as htmlLang } from "@codemirror/lang-html";
import { json as jsonLang } from "@codemirror/lang-json";
import { sql, SQLDialect } from "@codemirror/lang-sql";
import { javascript as javascriptLang } from "@codemirror/lang-javascript";
// ---
@@ -160,6 +161,8 @@
switch (language) {
case "html":
return htmlLang();
case "json":
return jsonLang();
case "sql-create-index":
return sql({
// lightweight sql dialect with mostly SELECT statements keywords
@@ -2,6 +2,7 @@
import { createEventDispatcher, tick } from "svelte";
import { slide } from "svelte/transition";
import CommonHelper from "@/utils/CommonHelper";
import { ClientResponseError } from "pocketbase";
import ApiClient from "@/utils/ApiClient";
import tooltip from "@/actions/tooltip";
import { setErrors } from "@/stores/errors";
@@ -174,43 +175,40 @@
window.localStorage.removeItem(draftKey());
}
function save(hidePanel = true) {
async function save(hidePanel = true) {
if (isSaving || !canSave || !collection?.id) {
return;
}
isSaving = true;
const data = exportFormData();
try {
const data = exportFormData();
let request;
if (isNew) {
request = ApiClient.collection(collection.id).create(data);
} else {
request = ApiClient.collection(collection.id).update(record.id, data);
let result;
if (isNew) {
result = await ApiClient.collection(collection.id).create(data);
} else {
result = await ApiClient.collection(collection.id).update(record.id, data);
}
addSuccessToast(isNew ? "Successfully created record." : "Successfully updated record.");
deleteDraft();
if (hidePanel) {
confirmClose = false;
hide();
} else {
replaceOriginal(result);
}
dispatch("save", result);
} catch (err) {
ApiClient.error(err);
}
request
.then((result) => {
addSuccessToast(isNew ? "Successfully created record." : "Successfully updated record.");
deleteDraft();
if (hidePanel) {
confirmClose = false;
hide();
} else {
replaceOriginal(result);
}
dispatch("save", result);
})
.catch((err) => {
ApiClient.error(err);
})
.finally(() => {
isSaving = false;
});
isSaving = false;
}
function deleteConfirm() {
@@ -240,8 +238,14 @@
id: data.id,
};
const jsonFields = {};
for (const field of collection?.schema || []) {
exportableFields[field.name] = true;
if (field.type == "json") {
jsonFields[field.name] = true;
}
}
if (isAuthCollection) {
@@ -265,6 +269,26 @@
data[key] = null;
}
// "validate" json fields
if (jsonFields[key] && data[key] !== "") {
try {
JSON.parse(data[key]);
} catch (err) {
const fieldErr = {};
fieldErr[key] = {
code: "invalid_json",
message: err.toString(),
};
// emulate server error
throw new ClientResponseError({
status: 400,
response: {
data: fieldErr,
},
});
}
}
CommonHelper.addValueToFormData(formData, key, data[key]);
}
@@ -1,31 +1,80 @@
<script>
import { onMount } from "svelte";
import tooltip from "@/actions/tooltip";
import CommonHelper from "@/utils/CommonHelper";
import Field from "@/components/base/Field.svelte";
export let field;
export let value = undefined;
let serialized = JSON.stringify(typeof value === "undefined" ? null : value, null, 2);
let editorComponent;
let serialized = serialize(value);
$: if (value !== serialized?.trim()) {
serialized = JSON.stringify(typeof value === "undefined" ? null : value, null, 2);
serialized = serialize(value);
value = serialized;
}
$: isValid = isValidJson(serialized);
function serialize(val) {
return JSON.stringify(typeof val === "undefined" ? null : val, null, 2);
}
function isValidJson(val) {
try {
JSON.parse(val === "" ? null : val);
return true;
} catch (_) {}
return false;
}
onMount(async () => {
try {
editorComponent = (await import("@/components/base/CodeEditor.svelte")).default;
} catch (err) {
console.warn(err);
}
});
</script>
<Field class="form-field {field.required ? 'required' : ''}" name={field.name} let:uniqueId>
<label for={uniqueId}>
<i class={CommonHelper.getFieldTypeIcon(field.type)} />
<span class="txt">{field.name}</span>
<span
class="json-state"
use:tooltip={{ position: "left", text: isValid ? "Valid JSON" : "Invalid JSON" }}
>
{#if isValid}
<i class="ri-checkbox-circle-fill txt-success" />
{:else}
<i class="ri-error-warning-fill txt-danger" />
{/if}
</span>
</label>
<textarea
id={uniqueId}
class="txt-mono"
required={field.required}
value={serialized}
on:input={(e) => {
serialized = e.target.value;
value = e.target.value.trim(); // trim the submitted value
}}
/>
{#if editorComponent}
<svelte:component
this={editorComponent}
id={uniqueId}
maxHeight="500"
language="json"
value={serialized}
on:change={(e) => {
serialized = e.detail;
value = serialized.trim(); // trim the submitted value
}}
/>
{:else}
<input type="text" class="txt-mono" value="Loading..." disabled />
{/if}
</Field>
<style>
.json-state {
position: absolute;
right: 10px;
}
</style>