validation
This commit is contained in:
@@ -0,0 +1,139 @@
|
|||||||
|
/* eslint-disable no-undefined,no-param-reassign,no-shadow */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throttle execution of a function. Especially useful for rate limiting
|
||||||
|
* execution of handlers on events like resize and scroll.
|
||||||
|
*
|
||||||
|
* @param {number} delay - A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher)
|
||||||
|
* are most useful.
|
||||||
|
* @param {Function} callback - A function to be executed after delay milliseconds. The `this` context and all arguments are passed through,
|
||||||
|
* as-is, to `callback` when the throttled-function is executed.
|
||||||
|
* @param {object} [options] - An object to configure options.
|
||||||
|
* @param {boolean} [options.noTrailing] - Optional, defaults to false. If noTrailing is true, callback will only execute every `delay` milliseconds
|
||||||
|
* while the throttled-function is being called. If noTrailing is false or unspecified, callback will be executed
|
||||||
|
* one final time after the last throttled-function call. (After the throttled-function has not been called for
|
||||||
|
* `delay` milliseconds, the internal counter is reset).
|
||||||
|
* @param {boolean} [options.noLeading] - Optional, defaults to false. If noLeading is false, the first throttled-function call will execute callback
|
||||||
|
* immediately. If noLeading is true, the first the callback execution will be skipped. It should be noted that
|
||||||
|
* callback will never executed if both noLeading = true and noTrailing = true.
|
||||||
|
* @param {boolean} [options.debounceMode] - If `debounceMode` is true (at begin), schedule `clear` to execute after `delay` ms. If `debounceMode` is
|
||||||
|
* false (at end), schedule `callback` to execute after `delay` ms.
|
||||||
|
*
|
||||||
|
* @returns {Function} A new, throttled, function.
|
||||||
|
*/
|
||||||
|
export function throttle(delay, callback, options) {
|
||||||
|
const {
|
||||||
|
noTrailing = false,
|
||||||
|
noLeading = false,
|
||||||
|
debounceMode = undefined,
|
||||||
|
} = options || {};
|
||||||
|
/*
|
||||||
|
* After wrapper has stopped being called, this timeout ensures that
|
||||||
|
* `callback` is executed at the proper times in `throttle` and `end`
|
||||||
|
* debounce modes.
|
||||||
|
*/
|
||||||
|
let timeoutID;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
// Keep track of the last time `callback` was executed.
|
||||||
|
let lastExec = 0;
|
||||||
|
|
||||||
|
// Function to clear existing timeout
|
||||||
|
function clearExistingTimeout() {
|
||||||
|
if (timeoutID) {
|
||||||
|
clearTimeout(timeoutID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to cancel next exec
|
||||||
|
function cancel(options) {
|
||||||
|
const { upcomingOnly = false } = options || {};
|
||||||
|
clearExistingTimeout();
|
||||||
|
cancelled = !upcomingOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The `wrapper` function encapsulates all of the throttling / debouncing
|
||||||
|
* functionality and when executed will limit the rate at which `callback`
|
||||||
|
* is executed.
|
||||||
|
*/
|
||||||
|
function wrapper(...arguments_) {
|
||||||
|
let self = this;
|
||||||
|
let elapsed = Date.now() - lastExec;
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute `callback` and update the `lastExec` timestamp.
|
||||||
|
function exec() {
|
||||||
|
lastExec = Date.now();
|
||||||
|
callback.apply(self, arguments_);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If `debounceMode` is true (at begin) this is used to clear the flag
|
||||||
|
* to allow future `callback` executions.
|
||||||
|
*/
|
||||||
|
function clear() {
|
||||||
|
timeoutID = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noLeading && debounceMode && !timeoutID) {
|
||||||
|
/*
|
||||||
|
* Since `wrapper` is being called for the first time and
|
||||||
|
* `debounceMode` is true (at begin), execute `callback`
|
||||||
|
* and noLeading != true.
|
||||||
|
*/
|
||||||
|
exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearExistingTimeout();
|
||||||
|
|
||||||
|
if (debounceMode === undefined && elapsed > delay) {
|
||||||
|
if (noLeading) {
|
||||||
|
/*
|
||||||
|
* In throttle mode with noLeading, if `delay` time has
|
||||||
|
* been exceeded, update `lastExec` and schedule `callback`
|
||||||
|
* to execute after `delay` ms.
|
||||||
|
*/
|
||||||
|
lastExec = Date.now();
|
||||||
|
if (!noTrailing) {
|
||||||
|
timeoutID = setTimeout(debounceMode ? clear : exec, delay);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* In throttle mode without noLeading, if `delay` time has been exceeded, execute
|
||||||
|
* `callback`.
|
||||||
|
*/
|
||||||
|
exec();
|
||||||
|
}
|
||||||
|
} else if (noTrailing !== true) {
|
||||||
|
/*
|
||||||
|
* In trailing throttle mode, since `delay` time has not been
|
||||||
|
* exceeded, schedule `callback` to execute `delay` ms after most
|
||||||
|
* recent execution.
|
||||||
|
*
|
||||||
|
* If `debounceMode` is true (at begin), schedule `clear` to execute
|
||||||
|
* after `delay` ms.
|
||||||
|
*
|
||||||
|
* If `debounceMode` is false (at end), schedule `callback` to
|
||||||
|
* execute after `delay` ms.
|
||||||
|
*/
|
||||||
|
timeoutID = setTimeout(
|
||||||
|
debounceMode ? clear : exec,
|
||||||
|
debounceMode === undefined ? delay - elapsed : delay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.cancel = cancel;
|
||||||
|
|
||||||
|
// Return the wrapper function.
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debounce(delay, callback, options) {
|
||||||
|
const { atBegin = false } = options || {};
|
||||||
|
return throttle(delay, callback, { debounceMode: atBegin !== false });
|
||||||
|
}
|
||||||
@@ -82,6 +82,10 @@
|
|||||||
Developers will use this to reference the field
|
Developers will use this to reference the field
|
||||||
</small>
|
</small>
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
Help text
|
||||||
|
<input bind:value={data.field.help} />
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
bind:checked={data.field.translatable}
|
bind:checked={data.field.translatable}
|
||||||
@@ -90,6 +94,32 @@
|
|||||||
/>
|
/>
|
||||||
Is Translatable
|
Is Translatable
|
||||||
</label>
|
</label>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
bind:checked={data.field.required}
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
/>
|
||||||
|
Required
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
bind:checked={data.field.readonly}
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
/>
|
||||||
|
Readonly
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
bind:checked={data.field.hidden}
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
/>
|
||||||
|
Hidden
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{#if data.field.type === "text"}
|
{#if data.field.type === "text"}
|
||||||
|
|||||||
@@ -2,41 +2,12 @@
|
|||||||
let { field } = $props();
|
let { field } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<fieldset>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
bind:checked={field.props.required}
|
|
||||||
type="checkbox"
|
|
||||||
role="switch"
|
|
||||||
/>
|
|
||||||
Required
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
bind:checked={field.props.readonly}
|
|
||||||
type="checkbox"
|
|
||||||
role="switch"
|
|
||||||
/>
|
|
||||||
Readonly
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
bind:checked={field.props.hidden}
|
|
||||||
type="checkbox"
|
|
||||||
role="switch"
|
|
||||||
/>
|
|
||||||
Hidden
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<label>
|
||||||
Default
|
Default
|
||||||
<input bind:value={field.props.default} />
|
<input bind:value={field.props.default} />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
|
||||||
Help text
|
|
||||||
<input bind:value={field.props.help} />
|
|
||||||
</label>
|
|
||||||
<label>
|
<label>
|
||||||
Min characters
|
Min characters
|
||||||
<input type="number" bind:value={field.props.min} />
|
<input type="number" bind:value={field.props.min} />
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script>
|
||||||
|
import { getSelectedLocales, getLocaleName } from "./locale.svelte.js";
|
||||||
|
let { channel, onLocaleChange } = $props();
|
||||||
|
let selectedLocales = $state(getSelectedLocales());
|
||||||
|
|
||||||
|
let selectedLocaleNames = $derived(
|
||||||
|
selectedLocales.map((id) => getLocaleName(channel, id)),
|
||||||
|
);
|
||||||
|
function handleChange() {
|
||||||
|
localStorage.setItem("selectedLocales", selectedLocales);
|
||||||
|
onLocaleChange();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<details class="dropdown">
|
||||||
|
<summary>Locales: {selectedLocaleNames.join(", ")}</summary>
|
||||||
|
<ul>
|
||||||
|
{#each channel.locales as locale}
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:group={selectedLocales}
|
||||||
|
onchange={handleChange}
|
||||||
|
value={locale.id}
|
||||||
|
/>
|
||||||
|
{locale.name}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script>
|
||||||
|
import ChannelLayout from "../../layouts/ChannelLayout.svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import TextField from "./fields/TextField.svelte";
|
||||||
|
import LocaleChooser from "./LocaleChooser.svelte";
|
||||||
|
import { getSelectedLocales } from "./locale.svelte.js";
|
||||||
|
let { channel, user, data } = $props();
|
||||||
|
let selectedLocales = $state(getSelectedLocales());
|
||||||
|
let record = $state(data.record);
|
||||||
|
|
||||||
|
function handleLocaleChange() {
|
||||||
|
selectedLocales = getSelectedLocales();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- <svelte:window on:beforeunload={beforeUnload} /> -->
|
||||||
|
<ChannelLayout {body} {channel} schemas={data.schemas} {user}></ChannelLayout>
|
||||||
|
{#snippet body()}
|
||||||
|
<LocaleChooser {channel} onLocaleChange={handleLocaleChange}
|
||||||
|
></LocaleChooser>
|
||||||
|
{#each data.fields as field}
|
||||||
|
<div style="display:flex;gap:20px;">
|
||||||
|
{#if field.type === "text"}
|
||||||
|
<TextField
|
||||||
|
{channel}
|
||||||
|
{record}
|
||||||
|
errors={data.validationErrors.filter(
|
||||||
|
(f) => f.id === field.fieldId && f.locale === "main",
|
||||||
|
)}
|
||||||
|
schemaField={field}
|
||||||
|
locale="main"
|
||||||
|
dataField={data.draftData.find(
|
||||||
|
(f) => f.id === field.id && f.locale === "main",
|
||||||
|
)}
|
||||||
|
></TextField>
|
||||||
|
{#if field.translatable}
|
||||||
|
{#each selectedLocales as locale (locale)}
|
||||||
|
<TextField
|
||||||
|
{channel}
|
||||||
|
{record}
|
||||||
|
errors={data.validationErrors.filter(
|
||||||
|
(f) =>
|
||||||
|
f.fieldId === field.id &&
|
||||||
|
f.locale === locale,
|
||||||
|
)}
|
||||||
|
schemaField={field}
|
||||||
|
{locale}
|
||||||
|
dataField={data.draftData.find(
|
||||||
|
(f) => f.id === field.id && f.locale === locale,
|
||||||
|
)}
|
||||||
|
></TextField>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/snippet}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<script>
|
||||||
|
import { post } from "../../../modules/remote";
|
||||||
|
import { getApp } from "../../../app";
|
||||||
|
import { getLocaleName } from "../locale.svelte.js";
|
||||||
|
let { channel, record, schemaField, dataField, locale, errors } = $props();
|
||||||
|
let originalValue = dataField?.value ?? schemaField.props.default;
|
||||||
|
let newValue = $state(originalValue);
|
||||||
|
let valuesChanged = $derived(newValue !== originalValue);
|
||||||
|
let errorMessage = $state("");
|
||||||
|
let validationErrors = $state(errors);
|
||||||
|
let hasErrors = $derived(validationErrors.length > 0);
|
||||||
|
const app = getApp();
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
if (!valuesChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
post(
|
||||||
|
app.url("records/fields"),
|
||||||
|
{
|
||||||
|
recordId: record.id,
|
||||||
|
id: schemaField.id,
|
||||||
|
locale: locale,
|
||||||
|
value: newValue,
|
||||||
|
},
|
||||||
|
(data, err) => {
|
||||||
|
if (err.isNotEmpty()) {
|
||||||
|
errorMessage = err.first();
|
||||||
|
} else {
|
||||||
|
validationErrors = data.validationErrors;
|
||||||
|
originalValue = newValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let delayMs = 1000;
|
||||||
|
let timer = $state(undefined);
|
||||||
|
let isComposing = $state(false);
|
||||||
|
|
||||||
|
const schedule = () => {
|
||||||
|
if (isComposing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
timer = setTimeout(flush, delayMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = undefined;
|
||||||
|
}
|
||||||
|
// value = inputValue;
|
||||||
|
save();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInput = () => {
|
||||||
|
schedule();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeydown = (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
flush();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCompositionStart = () => {
|
||||||
|
isComposing = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCompositionEnd = () => {
|
||||||
|
isComposing = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- <div style="position: relative;">
|
||||||
|
{#if field.selectOptions}
|
||||||
|
<Autocomplete {field} bind:value></Autocomplete>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{id}
|
||||||
|
class="form-control"
|
||||||
|
class:is-invalid={errorMessage}
|
||||||
|
bind:value
|
||||||
|
autocomplete="off"
|
||||||
|
readonly={field.readonly && !isCreateMode}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div> -->
|
||||||
|
<div style="min-width: 400px;">
|
||||||
|
<label>
|
||||||
|
{#if locale !== "main"}
|
||||||
|
{getLocaleName(channel, locale)} >
|
||||||
|
{/if}
|
||||||
|
{schemaField.name} <br />
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={newValue}
|
||||||
|
autocomplete="off"
|
||||||
|
readonly={schemaField.readonly}
|
||||||
|
aria-describedby={schemaField.id + "-help"}
|
||||||
|
oninput={handleInput}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
onblur={handleBlur}
|
||||||
|
oncompositionstart={handleCompositionStart}
|
||||||
|
oncompositionend={handleCompositionEnd}
|
||||||
|
aria-invalid={hasErrors ? "true" : ""}
|
||||||
|
/>
|
||||||
|
{#if hasErrors}
|
||||||
|
<small id={schemaField.id + "-help"}
|
||||||
|
>{validationErrors[0].message}</small
|
||||||
|
>
|
||||||
|
{:else if schemaField.help != ""}
|
||||||
|
<small id={schemaField.id + "-help"}>{schemaField.help}</small>
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export function getSelectedLocales() {
|
||||||
|
let value = $state(localStorage.getItem("selectedLocales"));
|
||||||
|
if (value == "" || !value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return value.split(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocaleName(channel, id) {
|
||||||
|
return channel.locales.find((locale) => locale.id === id).name;
|
||||||
|
}
|
||||||
+2
-2
@@ -9,7 +9,7 @@ import Profile from "./svelte/account/Profile.svelte";
|
|||||||
import SetupEntry from "./entry/SetupEntry/SetupEntry.svelte";
|
import SetupEntry from "./entry/SetupEntry/SetupEntry.svelte";
|
||||||
import Members from "./svelte/members/Members.svelte";
|
import Members from "./svelte/members/Members.svelte";
|
||||||
import RecordNotFound from "./svelte/records/NotFound.svelte";
|
import RecordNotFound from "./svelte/records/NotFound.svelte";
|
||||||
import RecordEdit from "./svelte/records/Edit.svelte";
|
import RecordEditEntry from "./entry/RecordEditEntry/RecordEditEntry.svelte";
|
||||||
import ContentEntry from "./entry/ContentEntry/ContentEntry.svelte";
|
import ContentEntry from "./entry/ContentEntry/ContentEntry.svelte";
|
||||||
import HomeEntry from "./entry/HomeEntry/HomeEntry.svelte";
|
import HomeEntry from "./entry/HomeEntry/HomeEntry.svelte";
|
||||||
import SchemaEntry from "./entry/SchemaEntry/SchemaEntry.svelte";
|
import SchemaEntry from "./entry/SchemaEntry/SchemaEntry.svelte";
|
||||||
@@ -21,7 +21,7 @@ import { createApp } from "./app";
|
|||||||
|
|
||||||
const entryComponents = {
|
const entryComponents = {
|
||||||
members: Members,
|
members: Members,
|
||||||
recordEdit: RecordEdit,
|
recordEdit: RecordEditEntry,
|
||||||
recordNotFound: RecordNotFound,
|
recordNotFound: RecordNotFound,
|
||||||
contentIndex: ContentEntry,
|
contentIndex: ContentEntry,
|
||||||
homeIndex: HomeEntry,
|
homeIndex: HomeEntry,
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ class Record
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
public string $id,
|
public string $id,
|
||||||
public string $schemaId,
|
public string $schemaId,
|
||||||
public array $draftData,
|
|
||||||
public array $liveData,
|
|
||||||
public Carbon $createdAt,
|
public Carbon $createdAt,
|
||||||
public string $createdBy,
|
public string $createdBy,
|
||||||
public ?Carbon $publishedAt,
|
public ?Carbon $publishedAt,
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php namespace Lucent\Core\Data;
|
||||||
|
|
||||||
|
class RecordError
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $fieldId,
|
||||||
|
public string $locale,
|
||||||
|
public string $message,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -5,13 +5,12 @@ use Carbon\Carbon;
|
|||||||
class RecordField
|
class RecordField
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
public string $recordId,
|
||||||
public string $id,
|
public string $id,
|
||||||
public string $locale,
|
public string $locale,
|
||||||
|
public RecordMode $mode,
|
||||||
public mixed $value,
|
public mixed $value,
|
||||||
public Carbon $createdAt,
|
|
||||||
public string $createdBy,
|
|
||||||
public Carbon $updatedAt,
|
public Carbon $updatedAt,
|
||||||
public string $updatedBy,
|
public string $updatedBy,
|
||||||
public int $version,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?php namespace Lucent\Core\Data;
|
||||||
|
|
||||||
|
enum RecordMode: string
|
||||||
|
{
|
||||||
|
case DRAFT = "draft";
|
||||||
|
case LIVE = "live";
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php namespace Lucent\Core\Record;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Lucent\Core\Data\RecordField;
|
||||||
|
use Lucent\Core\Data\RecordMode;
|
||||||
|
use stdClass;
|
||||||
|
|
||||||
|
class RecordFieldModule
|
||||||
|
{
|
||||||
|
public static function fromDb(stdClass $field): RecordField
|
||||||
|
{
|
||||||
|
return new RecordField(
|
||||||
|
recordId: data_get($field, "record_id"),
|
||||||
|
id: data_get($field, "id"),
|
||||||
|
locale: data_get($field, "locale", []),
|
||||||
|
mode: RecordMode::from(data_get($field, "mode")),
|
||||||
|
value: data_get($field, "value"),
|
||||||
|
updatedAt: Carbon::parse(data_get($field, "updated_at")),
|
||||||
|
updatedBy: data_get($field, "updated_by"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function toDb(RecordField $field): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
"record_id" => $field->recordId,
|
||||||
|
"id" => $field->id,
|
||||||
|
"locale" => $field->locale,
|
||||||
|
"mode" => $field->mode->value,
|
||||||
|
"value" => $field->value,
|
||||||
|
"updated_at" => $field->updatedAt->toJSON(),
|
||||||
|
"updated_by" => $field->updatedBy,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,17 +7,25 @@ use stdClass;
|
|||||||
|
|
||||||
class RecordModule
|
class RecordModule
|
||||||
{
|
{
|
||||||
|
public static function updateField(
|
||||||
|
Record $record,
|
||||||
|
RecordField $field,
|
||||||
|
): Record {
|
||||||
|
$recordFields = collect($record->draftData)->filter(
|
||||||
|
fn(RecordField $rf) => !(
|
||||||
|
$rf->id == $field->id && $rf->locale == $field->locale
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$recordFields->push($field);
|
||||||
|
$record->draftData = $recordFields->values()->toArray();
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
|
||||||
public static function toDb(Record $record): array
|
public static function toDb(Record $record): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
"id" => $record->id,
|
"id" => $record->id,
|
||||||
"schema_id" => $record->schemaId,
|
"schema_id" => $record->schemaId,
|
||||||
"draft_data" => json_encode(
|
|
||||||
array_map(static::recordFieldToDb(...), $record->draftData),
|
|
||||||
),
|
|
||||||
"live_data" => json_encode(
|
|
||||||
array_map(static::recordFieldToDb(...), $record->liveData),
|
|
||||||
),
|
|
||||||
"created_at" => $record->createdAt->toJSON(),
|
"created_at" => $record->createdAt->toJSON(),
|
||||||
"created_by" => $record->createdBy,
|
"created_by" => $record->createdBy,
|
||||||
"published_at" => empty($record->publishedAt)
|
"published_at" => empty($record->publishedAt)
|
||||||
@@ -33,14 +41,9 @@ class RecordModule
|
|||||||
|
|
||||||
public static function fromDb(stdClass $data): Record
|
public static function fromDb(stdClass $data): Record
|
||||||
{
|
{
|
||||||
$draftDataArr = json_decode(data_get($data, "draft_data"), true);
|
|
||||||
$liveDataArr = json_decode(data_get($data, "live_data"), true);
|
|
||||||
|
|
||||||
return new Record(
|
return new Record(
|
||||||
id: data_get($data, "id"),
|
id: data_get($data, "id"),
|
||||||
schemaId: data_get($data, "schema_id"),
|
schemaId: data_get($data, "schema_id"),
|
||||||
draftData: array_map(static::recordFieldFromDb(...), $draftDataArr),
|
|
||||||
liveData: array_map(static::recordFieldFromDb(...), $liveDataArr),
|
|
||||||
createdAt: Carbon::parse(data_get($data, "created_at")),
|
createdAt: Carbon::parse(data_get($data, "created_at")),
|
||||||
createdBy: data_get($data, "created_by"),
|
createdBy: data_get($data, "created_by"),
|
||||||
publishedAt: empty(data_get($data, "published_at"))
|
publishedAt: empty(data_get($data, "published_at"))
|
||||||
@@ -53,32 +56,4 @@ class RecordModule
|
|||||||
trashedBy: data_get($data, "trashed_by"),
|
trashedBy: data_get($data, "trashed_by"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function recordFieldFromDb(array $field): RecordField
|
|
||||||
{
|
|
||||||
return new RecordField(
|
|
||||||
id: data_get($field, "id"),
|
|
||||||
locale: data_get($field, "locale", []),
|
|
||||||
value: data_get($field, "value"),
|
|
||||||
createdAt: Carbon::parse(data_get($field, "created_at")),
|
|
||||||
createdBy: data_get($field, "created_by"),
|
|
||||||
updatedAt: Carbon::parse(data_get($field, "updated_at")),
|
|
||||||
updatedBy: data_get($field, "updated_by"),
|
|
||||||
version: data_get($field, "version"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function recordFieldToDb(RecordField $field): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
"id" => $field->id,
|
|
||||||
"locale" => $field->locale,
|
|
||||||
"value" => $field->value,
|
|
||||||
"created_at" => $field->createdAt->toJSON(),
|
|
||||||
"created_by" => $field->createdBy,
|
|
||||||
"updated_at" => $field->updatedAt->toJSON(),
|
|
||||||
"updated_by" => $field->updatedBy,
|
|
||||||
"version" => $field->version,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<?php namespace Lucent\Core\Record;
|
||||||
|
|
||||||
|
use Lucent\Core\Data\RecordError;
|
||||||
|
use Lucent\Core\Data\RecordField;
|
||||||
|
use Lucent\Core\Data\RecordMode;
|
||||||
|
use Lucent\Core\Schema\Data\Field;
|
||||||
|
|
||||||
|
class RecordValidationModule
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Validates a record against a schema.
|
||||||
|
*
|
||||||
|
* @param string[] $locales
|
||||||
|
* @param Field[] $schemaFields
|
||||||
|
* @param RecordField[] $recordFields
|
||||||
|
* @return RecordError[]
|
||||||
|
*/
|
||||||
|
public static function validate(
|
||||||
|
array $locales,
|
||||||
|
array $schemaFields,
|
||||||
|
array $recordFields,
|
||||||
|
): array {
|
||||||
|
$errors = [];
|
||||||
|
foreach ($schemaFields as $schemaField) {
|
||||||
|
$res = static::validateField("main", $schemaField, $recordFields);
|
||||||
|
$errors = array_merge($errors, $res);
|
||||||
|
if ($schemaField->translatable) {
|
||||||
|
foreach ($locales as $locale) {
|
||||||
|
$res = static::validateField(
|
||||||
|
$locale["id"],
|
||||||
|
$schemaField,
|
||||||
|
$recordFields,
|
||||||
|
);
|
||||||
|
$errors = array_merge($errors, $res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($errors)
|
||||||
|
->filter(fn($err) => $err !== null)
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param Field $schemaField
|
||||||
|
* @param RecordField[] $recordFields
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function validateField(
|
||||||
|
string $locale,
|
||||||
|
Field $schemaField,
|
||||||
|
array $recordFields,
|
||||||
|
): array {
|
||||||
|
// General Validations
|
||||||
|
$error = static::validateRequired($locale, $schemaField, $recordFields);
|
||||||
|
// Type specific
|
||||||
|
|
||||||
|
return collect([$error])
|
||||||
|
->filter(fn($err) => $err !== null)
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param Field $schemaField
|
||||||
|
* @param RecordField[] $recordFields
|
||||||
|
* @return ?RecordError
|
||||||
|
*/
|
||||||
|
public static function validateRequired(
|
||||||
|
string $locale,
|
||||||
|
Field $schemaField,
|
||||||
|
array $recordFields,
|
||||||
|
): ?RecordError {
|
||||||
|
if ($schemaField->required === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$recordField = collect($recordFields)->first(
|
||||||
|
fn(RecordField $field) => $field->id === $schemaField->id &&
|
||||||
|
$field->locale === $locale &&
|
||||||
|
$field->mode === RecordMode::DRAFT,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (empty($recordField) || empty($recordField->value)) {
|
||||||
|
return new RecordError(
|
||||||
|
$schemaField->id,
|
||||||
|
$locale,
|
||||||
|
"This field is required",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,19 @@ class FieldRepo
|
|||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Field[]
|
||||||
|
*/
|
||||||
|
public static function findBySchemaId(string $schemaId): array
|
||||||
|
{
|
||||||
|
return DB::table(self::TABLE_NAME)
|
||||||
|
->where("schema_id", $schemaId)
|
||||||
|
->orderBy("rank")
|
||||||
|
->get()
|
||||||
|
->map(FieldModule::fromDb(...))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
public static function findOne(string $id): ?Field
|
public static function findOne(string $id): ?Field
|
||||||
{
|
{
|
||||||
return DB::table(self::TABLE_NAME)
|
return DB::table(self::TABLE_NAME)
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php namespace Lucent\Core\Repository;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Lucent\Core\Data\RecordField;
|
||||||
|
use Lucent\Core\Record\RecordFieldModule;
|
||||||
|
|
||||||
|
class RecordFieldRepo
|
||||||
|
{
|
||||||
|
const TABLE_NAME = "records_data";
|
||||||
|
|
||||||
|
public static function insert(RecordField $recordField): void
|
||||||
|
{
|
||||||
|
DB::table(self::TABLE_NAME)->insert(
|
||||||
|
RecordFieldModule::toDb($recordField),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function delete(RecordField $field): void
|
||||||
|
{
|
||||||
|
DB::table(self::TABLE_NAME)
|
||||||
|
->where("id", $field->id)
|
||||||
|
->where("locale", $field->locale)
|
||||||
|
->where("mode", $field->mode->value)
|
||||||
|
->where("record_id", $field->recordId)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function findOne(string $id): ?RecordField
|
||||||
|
{
|
||||||
|
return DB::table(self::TABLE_NAME)
|
||||||
|
->where("id", $id)
|
||||||
|
->get()
|
||||||
|
->map(RecordFieldModule::fromDb(...))
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return RecordField[]
|
||||||
|
*/
|
||||||
|
public static function findByRecordId(string $schemaId): array
|
||||||
|
{
|
||||||
|
return DB::table(self::TABLE_NAME)
|
||||||
|
->where("record_id", $schemaId)
|
||||||
|
->get()
|
||||||
|
->map(RecordFieldModule::fromDb(...))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,4 +12,13 @@ class RecordRepo
|
|||||||
{
|
{
|
||||||
DB::table(self::TABLE_NAME)->insert(RecordModule::toDb($record));
|
DB::table(self::TABLE_NAME)->insert(RecordModule::toDb($record));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function findOne(string $id): ?Record
|
||||||
|
{
|
||||||
|
return DB::table(self::TABLE_NAME)
|
||||||
|
->where("id", $id)
|
||||||
|
->get()
|
||||||
|
->map(RecordModule::fromDb(...))
|
||||||
|
->first();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ class Field
|
|||||||
public string $name,
|
public string $name,
|
||||||
public string $type,
|
public string $type,
|
||||||
public string $schemaId,
|
public string $schemaId,
|
||||||
|
public string $help,
|
||||||
public bool $translatable,
|
public bool $translatable,
|
||||||
|
public bool $required,
|
||||||
|
public bool $readonly,
|
||||||
|
public bool $hidden,
|
||||||
public int $rank,
|
public int $rank,
|
||||||
public IFieldProp $props,
|
public IFieldProp $props,
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -5,15 +5,7 @@ class FieldProp
|
|||||||
public static function fromType(string $type): IFieldProp
|
public static function fromType(string $type): IFieldProp
|
||||||
{
|
{
|
||||||
return match ($type) {
|
return match ($type) {
|
||||||
"text" => new TextFieldProp(
|
"text" => new TextFieldProp(min: 0, max: 0, default: ""),
|
||||||
required: false,
|
|
||||||
readonly: false,
|
|
||||||
hidden: false,
|
|
||||||
min: 0,
|
|
||||||
max: 0,
|
|
||||||
help: "",
|
|
||||||
default: "",
|
|
||||||
),
|
|
||||||
default => new InvalidFieldProp(),
|
default => new InvalidFieldProp(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -28,12 +20,8 @@ class FieldProp
|
|||||||
{
|
{
|
||||||
return match ($type) {
|
return match ($type) {
|
||||||
"text" => new TextFieldProp(
|
"text" => new TextFieldProp(
|
||||||
required: $props["required"] ?? false,
|
|
||||||
readonly: $props["readonly"] ?? false,
|
|
||||||
hidden: $props["hidden"] ?? false,
|
|
||||||
min: $props["min"] ?? 0,
|
min: $props["min"] ?? 0,
|
||||||
max: $props["max"] ?? 0,
|
max: $props["max"] ?? 0,
|
||||||
help: $props["help"] ?? "",
|
|
||||||
default: $props["default"] ?? "",
|
default: $props["default"] ?? "",
|
||||||
),
|
),
|
||||||
default => new InvalidFieldProp(),
|
default => new InvalidFieldProp(),
|
||||||
|
|||||||
@@ -3,12 +3,8 @@
|
|||||||
class TextFieldProp implements IFieldProp
|
class TextFieldProp implements IFieldProp
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public bool $required,
|
|
||||||
public bool $readonly,
|
|
||||||
public bool $hidden,
|
|
||||||
public string $default,
|
public string $default,
|
||||||
public int $min,
|
public int $min,
|
||||||
public int $max,
|
public int $max,
|
||||||
public string $help,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ class FieldModule
|
|||||||
"alias" => $field->alias,
|
"alias" => $field->alias,
|
||||||
"name" => $field->name,
|
"name" => $field->name,
|
||||||
"type" => $field->type,
|
"type" => $field->type,
|
||||||
|
"help" => $field->help,
|
||||||
"rank" => $field->rank,
|
"rank" => $field->rank,
|
||||||
|
"required" => $field->required,
|
||||||
|
"readonly" => $field->readonly,
|
||||||
|
"hidden" => $field->hidden,
|
||||||
"translatable" => $field->translatable,
|
"translatable" => $field->translatable,
|
||||||
"props" => json_encode($field->props),
|
"props" => json_encode($field->props),
|
||||||
"schema_id" => $field->schemaId,
|
"schema_id" => $field->schemaId,
|
||||||
@@ -37,8 +41,12 @@ class FieldModule
|
|||||||
alias: data_get($data, "alias"),
|
alias: data_get($data, "alias"),
|
||||||
name: data_get($data, "name"),
|
name: data_get($data, "name"),
|
||||||
type: data_get($data, "type"),
|
type: data_get($data, "type"),
|
||||||
|
help: data_get($data, "help"),
|
||||||
schemaId: data_get($data, "schema_id"),
|
schemaId: data_get($data, "schema_id"),
|
||||||
rank: data_get($data, "rank"),
|
rank: data_get($data, "rank"),
|
||||||
|
required: data_get($data, "required"),
|
||||||
|
readonly: data_get($data, "readonly"),
|
||||||
|
hidden: data_get($data, "hidden"),
|
||||||
translatable: data_get($data, "translatable"),
|
translatable: data_get($data, "translatable"),
|
||||||
props: FieldProp::fromDb(
|
props: FieldProp::fromDb(
|
||||||
data_get($data, "type"),
|
data_get($data, "type"),
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ class FieldController
|
|||||||
$schemaId = $request->input("schemaId");
|
$schemaId = $request->input("schemaId");
|
||||||
$name = $request->input("name");
|
$name = $request->input("name");
|
||||||
$alias = $request->input("alias");
|
$alias = $request->input("alias");
|
||||||
|
$help = $request->input("help") ?? "";
|
||||||
|
$required = $request->input("required", false);
|
||||||
|
$readonly = $request->input("readonly", false);
|
||||||
|
$hidden = $request->input("hidden", false);
|
||||||
$fieldType = $request->input("fieldType");
|
$fieldType = $request->input("fieldType");
|
||||||
$fields = FieldRepo::all();
|
$fields = FieldRepo::all();
|
||||||
$newRank = collect($fields)->last()->rank + 1;
|
$newRank = collect($fields)->last()->rank + 1;
|
||||||
@@ -75,8 +79,12 @@ class FieldController
|
|||||||
schemaId: $schemaId,
|
schemaId: $schemaId,
|
||||||
name: $name,
|
name: $name,
|
||||||
alias: $alias,
|
alias: $alias,
|
||||||
|
help: $help,
|
||||||
type: $fieldType,
|
type: $fieldType,
|
||||||
props: $fieldProps,
|
props: $fieldProps,
|
||||||
|
required: $required,
|
||||||
|
readonly: $readonly,
|
||||||
|
hidden: $hidden,
|
||||||
rank: $newRank,
|
rank: $newRank,
|
||||||
translatable: false,
|
translatable: false,
|
||||||
);
|
);
|
||||||
@@ -137,7 +145,11 @@ class FieldController
|
|||||||
schemaId: $field->schemaId,
|
schemaId: $field->schemaId,
|
||||||
name: data_get($fieldData, "name"),
|
name: data_get($fieldData, "name"),
|
||||||
alias: data_get($fieldData, "alias"),
|
alias: data_get($fieldData, "alias"),
|
||||||
|
help: data_get($fieldData, "help") ?? "",
|
||||||
translatable: data_get($fieldData, "translatable"),
|
translatable: data_get($fieldData, "translatable"),
|
||||||
|
required: data_get($fieldData, "required"),
|
||||||
|
readonly: data_get($fieldData, "readonly"),
|
||||||
|
hidden: data_get($fieldData, "hidden"),
|
||||||
type: $field->type,
|
type: $field->type,
|
||||||
rank: $field->rank,
|
rank: $field->rank,
|
||||||
props: $fieldProps,
|
props: $fieldProps,
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ use Lucent\Account\AccountService;
|
|||||||
use Lucent\Channel\ChannelService;
|
use Lucent\Channel\ChannelService;
|
||||||
use Lucent\Core\Auth\AuthModule;
|
use Lucent\Core\Auth\AuthModule;
|
||||||
use Lucent\Core\Data\RecordField;
|
use Lucent\Core\Data\RecordField;
|
||||||
|
use Lucent\Core\Repository\FieldRepo;
|
||||||
use Lucent\Core\Repository\SchemaRepo;
|
use Lucent\Core\Repository\SchemaRepo;
|
||||||
|
use Lucent\Core\Repository\RecordFieldRepo;
|
||||||
use Lucent\Id\Id;
|
use Lucent\Id\Id;
|
||||||
use Lucent\LucentException;
|
use Lucent\LucentException;
|
||||||
use Lucent\Query\Operator\OperatorRegistry;
|
use Lucent\Query\Operator\OperatorRegistry;
|
||||||
@@ -17,6 +19,8 @@ use Lucent\Record\InputData\EdgeInputData;
|
|||||||
use Lucent\Record\InputData\RecordInputData;
|
use Lucent\Record\InputData\RecordInputData;
|
||||||
use Lucent\Record\Manager;
|
use Lucent\Record\Manager;
|
||||||
use Lucent\Core\Repository\RecordRepo;
|
use Lucent\Core\Repository\RecordRepo;
|
||||||
|
use Lucent\Core\Channel\ChannelModule;
|
||||||
|
use Lucent\Core\Record\RecordValidationModule;
|
||||||
use Lucent\Record\RecordService;
|
use Lucent\Record\RecordService;
|
||||||
use Lucent\Record\Status;
|
use Lucent\Record\Status;
|
||||||
use Lucent\Schema\Ui\Reference;
|
use Lucent\Schema\Ui\Reference;
|
||||||
@@ -24,6 +28,8 @@ use Lucent\Schema\Validator\ValidatorException;
|
|||||||
use Lucent\Svelte\Svelte;
|
use Lucent\Svelte\Svelte;
|
||||||
use Lucent\ViewModel\ViewModel;
|
use Lucent\ViewModel\ViewModel;
|
||||||
use Lucent\Core\Data\Record;
|
use Lucent\Core\Data\Record;
|
||||||
|
use Lucent\Core\Data\RecordMode;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use function Lucent\Response\fail;
|
use function Lucent\Response\fail;
|
||||||
use function Lucent\Response\ok;
|
use function Lucent\Response\ok;
|
||||||
|
|
||||||
@@ -219,60 +225,57 @@ class RecordController
|
|||||||
|
|
||||||
public function edit(Request $request)
|
public function edit(Request $request)
|
||||||
{
|
{
|
||||||
$rid = $request->route("rid");
|
$recordId = $request->route("id");
|
||||||
|
|
||||||
$graph = $this->query
|
// $graph = $this->query
|
||||||
->filter(["id" => $rid])
|
// ->filter(["id" => $rid])
|
||||||
->limit(1)
|
// ->limit(1)
|
||||||
->skip(0)
|
// ->skip(0)
|
||||||
->childrenDepth(2)
|
// ->childrenDepth(2)
|
||||||
->childrenLimit(200)
|
// ->childrenLimit(200)
|
||||||
->parentsDepth(1)
|
// ->parentsDepth(1)
|
||||||
->parentsLimit(200)
|
// ->parentsLimit(200)
|
||||||
->run();
|
// ->run();
|
||||||
|
|
||||||
if ($graph->records->isEmpty()) {
|
// if ($graph->records->isEmpty()) {
|
||||||
return $this->svelte->render(
|
// return $this->svelte->render(
|
||||||
layout: "channel",
|
// layout: "channel",
|
||||||
view: "recordNotFound",
|
// view: "recordNotFound",
|
||||||
title: "Record Not Found",
|
// title: "Record Not Found",
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// $record = $graph->records->first();
|
||||||
|
//
|
||||||
|
$record = RecordRepo::findOne($recordId);
|
||||||
|
$recordFields = RecordFieldRepo::findByRecordId($recordId);
|
||||||
|
$draftData = collect($recordFields)
|
||||||
|
->where("mode", RecordMode::DRAFT)
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$schemas = SchemaRepo::all();
|
||||||
|
$schema = collect($schemas)->firstWhere("id", $record->schemaId);
|
||||||
|
$fields = FieldRepo::findBySchemaId($record->schemaId);
|
||||||
|
$locales = ChannelModule::get()->locales;
|
||||||
|
|
||||||
|
$validationErrors = RecordValidationModule::validate(
|
||||||
|
$locales,
|
||||||
|
$fields,
|
||||||
|
$draftData,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
$record = $graph->records->first();
|
return Svelte::view(
|
||||||
|
|
||||||
if (
|
|
||||||
!in_array(
|
|
||||||
$record->schema,
|
|
||||||
$this->accountService->currentReadableSchemas(),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return $this->svelte->render(
|
|
||||||
layout: "channel",
|
|
||||||
view: "recordNotFound",
|
|
||||||
title: "Schema Not Found",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$schema = $this->channelService->getSchema($record->schema)->get();
|
|
||||||
$recordHistory = $this->recordManager
|
|
||||||
->fromSession($request->session())
|
|
||||||
->push($rid)
|
|
||||||
->getRecords($rid);
|
|
||||||
return $this->svelte->render(
|
|
||||||
layout: "channel",
|
|
||||||
view: "recordEdit",
|
view: "recordEdit",
|
||||||
title: "Edit Record",
|
title: "Edit Record",
|
||||||
data: [
|
data: [
|
||||||
|
"schemas" => $schemas,
|
||||||
"schema" => $schema,
|
"schema" => $schema,
|
||||||
"graph" => toArray($graph),
|
"fields" => $fields,
|
||||||
|
// "graph" => toArray($graph),
|
||||||
"record" => toArray($record),
|
"record" => toArray($record),
|
||||||
"users" => $this->accountService->all(),
|
"draftData" => $draftData,
|
||||||
"recordHistory" => $recordHistory,
|
"validationErrors" => $validationErrors,
|
||||||
"isWritable" => in_array(
|
|
||||||
$record->schema,
|
|
||||||
$this->accountService->currentWritableSchemas(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -310,26 +313,14 @@ class RecordController
|
|||||||
public function postCreate(Request $request)
|
public function postCreate(Request $request)
|
||||||
{
|
{
|
||||||
$schemaId = $request->input("schemaId");
|
$schemaId = $request->input("schemaId");
|
||||||
|
$fields = FieldRepo::findBySchemaId($schemaId);
|
||||||
$title = $request->input("title");
|
$title = $request->input("title");
|
||||||
$now = Carbon::now();
|
$now = Carbon::now();
|
||||||
$userId = AuthModule::getCurrentUserId();
|
$userId = AuthModule::getCurrentUserId();
|
||||||
|
|
||||||
$titleField = new RecordField(
|
|
||||||
id: "_title",
|
|
||||||
locale: "main",
|
|
||||||
value: $title,
|
|
||||||
createdAt: $now,
|
|
||||||
createdBy: $userId,
|
|
||||||
updatedAt: $now,
|
|
||||||
updatedBy: $userId,
|
|
||||||
version: 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
$record = new Record(
|
$record = new Record(
|
||||||
id: Id::new(),
|
id: Id::new(),
|
||||||
schemaId: $schemaId,
|
schemaId: $schemaId,
|
||||||
draftData: [$titleField],
|
|
||||||
liveData: [],
|
|
||||||
createdAt: $now,
|
createdAt: $now,
|
||||||
createdBy: $userId,
|
createdBy: $userId,
|
||||||
publishedAt: null,
|
publishedAt: null,
|
||||||
@@ -339,9 +330,59 @@ class RecordController
|
|||||||
);
|
);
|
||||||
|
|
||||||
RecordRepo::insert($record);
|
RecordRepo::insert($record);
|
||||||
|
|
||||||
|
$titleField = collect($fields)->where("alias", "_title")->first();
|
||||||
|
|
||||||
|
$titleFieldData = new RecordField(
|
||||||
|
recordId: $record->id,
|
||||||
|
id: $titleField->id,
|
||||||
|
locale: "main",
|
||||||
|
mode: RecordMode::DRAFT,
|
||||||
|
value: $title,
|
||||||
|
updatedAt: $now,
|
||||||
|
updatedBy: $userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
RecordFieldRepo::insert($titleFieldData);
|
||||||
|
|
||||||
return ok(toArray($record));
|
return ok(toArray($record));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function saveField(Request $request)
|
||||||
|
{
|
||||||
|
$recordId = $request->input("recordId");
|
||||||
|
$id = $request->input("id");
|
||||||
|
$locale = $request->input("locale");
|
||||||
|
$value = $request->input("value") ?? "";
|
||||||
|
$now = Carbon::now();
|
||||||
|
$field = FieldRepo::findOne($id);
|
||||||
|
$userId = AuthModule::getCurrentUserId();
|
||||||
|
$recordField = new RecordField(
|
||||||
|
recordId: $recordId,
|
||||||
|
id: $field->id,
|
||||||
|
locale: $locale,
|
||||||
|
mode: RecordMode::DRAFT,
|
||||||
|
value: $value,
|
||||||
|
updatedAt: $now,
|
||||||
|
updatedBy: $userId,
|
||||||
|
);
|
||||||
|
DB::transaction(function () use ($recordField) {
|
||||||
|
RecordFieldRepo::delete($recordField);
|
||||||
|
RecordFieldRepo::insert($recordField);
|
||||||
|
});
|
||||||
|
|
||||||
|
$validationErrors = RecordValidationModule::validateField(
|
||||||
|
$locale,
|
||||||
|
$field,
|
||||||
|
[$recordField],
|
||||||
|
);
|
||||||
|
|
||||||
|
return ok([
|
||||||
|
"recordField" => toArray($recordField),
|
||||||
|
"validationErrors" => $validationErrors,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function save(Request $request)
|
public function save(Request $request)
|
||||||
{
|
{
|
||||||
$recordId = $request->input("record.id");
|
$recordId = $request->input("record.id");
|
||||||
|
|||||||
@@ -54,15 +54,11 @@ class SchemaController
|
|||||||
name: "Title",
|
name: "Title",
|
||||||
alias: "_title",
|
alias: "_title",
|
||||||
type: "text",
|
type: "text",
|
||||||
props: new TextFieldProp(
|
help: "",
|
||||||
required: true,
|
required: true,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
default: "",
|
props: new TextFieldProp(default: "", min: 1, max: 255),
|
||||||
min: 1,
|
|
||||||
max: 255,
|
|
||||||
help: "",
|
|
||||||
),
|
|
||||||
rank: 0,
|
rank: 0,
|
||||||
translatable: false,
|
translatable: false,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ Route::group(
|
|||||||
// RECORD
|
// RECORD
|
||||||
Route::get("records/{id}", [RecordController::class, "edit"]);
|
Route::get("records/{id}", [RecordController::class, "edit"]);
|
||||||
Route::post("records", [RecordController::class, "postCreate"]);
|
Route::post("records", [RecordController::class, "postCreate"]);
|
||||||
|
Route::post("records/fields", [
|
||||||
|
RecordController::class,
|
||||||
|
"saveField",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
//OLD
|
//OLD
|
||||||
|
|||||||
Reference in New Issue
Block a user