edit schema wip
This commit is contained in:
@@ -8,3 +8,9 @@ main {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.danger {
|
||||||
|
background-color: #d93526;
|
||||||
|
border-color: #d93526;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script>
|
||||||
|
let { onDelete, text } = $props();
|
||||||
|
let isClicked = $state(false);
|
||||||
|
|
||||||
|
function handleTryDelete(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
isClicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
isClicked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRealDelete(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
onDelete();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !isClicked}
|
||||||
|
<form onsubmit={handleTryDelete}>
|
||||||
|
<button class="danger" type="submit">
|
||||||
|
{@render text()}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
{#if isClicked}
|
||||||
|
Are you sure?
|
||||||
|
<form onsubmit={handleCancel}>
|
||||||
|
<button class="secondary" type="submit">No</button>
|
||||||
|
</form>
|
||||||
|
<form onsubmit={handleRealDelete}>
|
||||||
|
<button class="danger" type="submit">Yes</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<script>
|
||||||
|
import { arrayMoveElement } from "../helpers";
|
||||||
|
import { flip } from "svelte/animate";
|
||||||
|
|
||||||
|
let {
|
||||||
|
sortableClass,
|
||||||
|
onUpdate,
|
||||||
|
items,
|
||||||
|
itemView,
|
||||||
|
itemCssClass,
|
||||||
|
itemKey = "id",
|
||||||
|
type = "list",
|
||||||
|
handleClass = null,
|
||||||
|
disabled = false,
|
||||||
|
} = $props();
|
||||||
|
// let sortableInstance = $state();
|
||||||
|
let sortableContainer = $state();
|
||||||
|
let draggedItem = $state();
|
||||||
|
|
||||||
|
function handleDragStart(event, item) {
|
||||||
|
if (disabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
draggedItem = item;
|
||||||
|
// Set data required for drag operation
|
||||||
|
event.dataTransfer.effectAllowed = "move";
|
||||||
|
event.dataTransfer.setData("text/plain", item.id);
|
||||||
|
// Set a ghost drag image
|
||||||
|
event.currentTarget.classList.add("dragging");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drag over another item
|
||||||
|
function handleDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.target.closest(".draggable-item").classList.add("dragover");
|
||||||
|
event.dataTransfer.dropEffect = "move";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle dropping the item
|
||||||
|
function handleDrop(event, targetItem) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (draggedItem === targetItem) return;
|
||||||
|
// Find positions of dragged and target items
|
||||||
|
const draggedIndex = items.findIndex(
|
||||||
|
(item) => getItem(item, itemKey) === getItem(draggedItem, itemKey),
|
||||||
|
);
|
||||||
|
const targetIndex = items.findIndex(
|
||||||
|
(item) => getItem(item, itemKey) === getItem(targetItem, itemKey),
|
||||||
|
);
|
||||||
|
|
||||||
|
onUpdate(
|
||||||
|
arrayMoveElement([...items], draggedIndex, targetIndex),
|
||||||
|
draggedIndex,
|
||||||
|
targetIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItem(item, key) {
|
||||||
|
if (key === null) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
if (key.includes(".")) {
|
||||||
|
return key.split(".").reduce((a, b) => a[b] ?? null, item);
|
||||||
|
}
|
||||||
|
return item[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd(event) {
|
||||||
|
event.target.classList.remove("dragging");
|
||||||
|
sortableContainer
|
||||||
|
.querySelector(".dragover")
|
||||||
|
?.classList.remove("dragover");
|
||||||
|
draggedItem = null;
|
||||||
|
const draggableItem = event.target.closest(".draggable-item");
|
||||||
|
draggableItem.draggable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragLeave(event) {
|
||||||
|
// event.target.classList.remove("dragover");
|
||||||
|
const draggableItem = event.target.closest(".draggable-item");
|
||||||
|
draggableItem.classList.remove("dragover");
|
||||||
|
draggableItem.draggable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseDown(e) {
|
||||||
|
const handleEl = e.target.closest("." + handleClass);
|
||||||
|
if (!handleEl) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const draggableItem = e.target.closest(".draggable-item");
|
||||||
|
draggableItem.draggable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp(e) {
|
||||||
|
const handleEl = e.target.closest("." + handleClass);
|
||||||
|
if (!handleEl) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const draggableItem = e.target.closest(".draggable-item");
|
||||||
|
draggableItem.draggable = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if type === "table"}
|
||||||
|
<tbody
|
||||||
|
class="sortable-container {sortableClass}"
|
||||||
|
bind:this={sortableContainer}
|
||||||
|
>
|
||||||
|
{#each items as item (getItem(item, itemKey))}
|
||||||
|
<tr
|
||||||
|
animate:flip={{ duration: 100 }}
|
||||||
|
class="draggable-item {itemCssClass}"
|
||||||
|
draggable="false"
|
||||||
|
onmousedown={handleMouseDown}
|
||||||
|
onmouseup={handleMouseUp}
|
||||||
|
ondragstart={(e) => handleDragStart(e, item)}
|
||||||
|
ondragover={handleDragOver}
|
||||||
|
ondragleave={handleDragLeave}
|
||||||
|
ondrop={(e) => handleDrop(e, item)}
|
||||||
|
ondragend={handleDragEnd}
|
||||||
|
>
|
||||||
|
{@render itemView(item)}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="sortable-container {sortableClass}"
|
||||||
|
bind:this={sortableContainer}
|
||||||
|
>
|
||||||
|
{#each items as item (getItem(item, itemKey))}
|
||||||
|
<div
|
||||||
|
animate:flip={{ duration: 100 }}
|
||||||
|
role="listitem"
|
||||||
|
class="draggable-item {itemCssClass}"
|
||||||
|
draggable="false"
|
||||||
|
onmousedown={handleMouseDown}
|
||||||
|
onmouseup={handleMouseUp}
|
||||||
|
ondragstart={(e) => handleDragStart(e, item)}
|
||||||
|
ondragover={handleDragOver}
|
||||||
|
ondragleave={handleDragLeave}
|
||||||
|
ondrop={(e) => handleDrop(e, item)}
|
||||||
|
ondragend={handleDragEnd}
|
||||||
|
>
|
||||||
|
{@render itemView(item)}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -2,14 +2,13 @@
|
|||||||
import ChannelLayout from "../../layouts/ChannelLayout.svelte";
|
import ChannelLayout from "../../layouts/ChannelLayout.svelte";
|
||||||
import { post } from "../../modules/remote";
|
import { post } from "../../modules/remote";
|
||||||
import { getApp } from "../../app";
|
import { getApp } from "../../app";
|
||||||
let { channel, user, data } = $props();
|
let { channel, user, data, newRank } = $props();
|
||||||
let name = $state("");
|
let name = $state("");
|
||||||
let alias = $state("");
|
let alias = $state("");
|
||||||
const app = getApp();
|
const app = getApp();
|
||||||
|
|
||||||
function handleSchemaCreate(e) {
|
function handleCreate(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log(data);
|
|
||||||
post(
|
post(
|
||||||
app.url("fields"),
|
app.url("fields"),
|
||||||
{
|
{
|
||||||
@@ -33,7 +32,7 @@
|
|||||||
{#snippet body()}
|
{#snippet body()}
|
||||||
<h3 class="header-small mb-4 mt-5">Create a <em>{data.type}</em> field</h3>
|
<h3 class="header-small mb-4 mt-5">Create a <em>{data.type}</em> field</h3>
|
||||||
|
|
||||||
<form onsubmit={handleSchemaCreate}>
|
<form onsubmit={handleCreate}>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<label>
|
||||||
Name
|
Name
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<script>
|
||||||
|
import ChannelLayout from "../../layouts/ChannelLayout.svelte";
|
||||||
|
import TextFieldProps from "./TextFieldProps.svelte";
|
||||||
|
import DeleteButton from "../../common/DeleteButton.svelte";
|
||||||
|
import { post } from "../../modules/remote";
|
||||||
|
import { getApp } from "../../app";
|
||||||
|
let { channel, user, data } = $props();
|
||||||
|
|
||||||
|
const app = getApp();
|
||||||
|
|
||||||
|
function handleUpdate(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
post(
|
||||||
|
app.url("fields/update"),
|
||||||
|
{
|
||||||
|
field: data.field,
|
||||||
|
},
|
||||||
|
(data, err) => {
|
||||||
|
if (err.isEmpty()) {
|
||||||
|
Turbo.visit(app.url("schemas"));
|
||||||
|
} else {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
post(
|
||||||
|
app.url("fields/delete"),
|
||||||
|
{
|
||||||
|
fieldId: data.field.id,
|
||||||
|
},
|
||||||
|
(data, err) => {
|
||||||
|
if (err.isEmpty()) {
|
||||||
|
Turbo.visit(app.url("schemas"));
|
||||||
|
} else {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ChannelLayout {body} {channel} {user}></ChannelLayout>
|
||||||
|
{#snippet body()}
|
||||||
|
<h3 class="header-small mb-4 mt-5">
|
||||||
|
Edit <em>{data.field.type}</em> field {data.field.name}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<form onsubmit={handleUpdate}>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
Name
|
||||||
|
<input
|
||||||
|
bind:value={data.field.name}
|
||||||
|
placeholder="ex. Description"
|
||||||
|
minlength="2"
|
||||||
|
maxlength="30"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Alias
|
||||||
|
<input
|
||||||
|
bind:value={data.field.alias}
|
||||||
|
placeholder="ex. description"
|
||||||
|
minlength="2"
|
||||||
|
maxlength="30"
|
||||||
|
required
|
||||||
|
aria-describedby="alias-helper"
|
||||||
|
/>
|
||||||
|
<small id="alias-helper">
|
||||||
|
Developers will use this to reference the field
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{#if data.field.type === "text"}
|
||||||
|
<TextFieldProps field={data.field}></TextFieldProps>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button type="submit">Update</button>
|
||||||
|
</form>
|
||||||
|
<DeleteButton onDelete={handleDelete}>
|
||||||
|
{#snippet text()}
|
||||||
|
Delete field
|
||||||
|
{/snippet}
|
||||||
|
</DeleteButton>
|
||||||
|
{/snippet}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script>
|
||||||
|
let { field } = $props();
|
||||||
|
</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>
|
||||||
|
<label>
|
||||||
|
Default
|
||||||
|
<input bind:value={field.props.default} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Help text
|
||||||
|
<input bind:value={field.props.help} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Min characters
|
||||||
|
<input type="number" bind:value={field.props.min} />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Max characters
|
||||||
|
<input type="number" bind:value={field.props.max} />
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<script>
|
||||||
|
import ChannelLayout from "../../layouts/ChannelLayout.svelte";
|
||||||
|
import Sortable from "../../common/Sortable.svelte";
|
||||||
|
import { post } from "../../modules/remote";
|
||||||
|
import { getApp } from "../../app";
|
||||||
|
let { channel, user, data } = $props();
|
||||||
|
const app = getApp();
|
||||||
|
|
||||||
|
function handleSchemaCreate(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
// post(
|
||||||
|
// channel.lucentUrl + "/schemas",
|
||||||
|
// {
|
||||||
|
// name: newSchemaName,
|
||||||
|
// alias: newSchemaAlias,
|
||||||
|
// },
|
||||||
|
// (data, err) => {
|
||||||
|
// if (err.isEmpty()) {
|
||||||
|
// Turbo.visit(window.location.href);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ChannelLayout {body} {channel} {user}></ChannelLayout>
|
||||||
|
{#snippet body()}
|
||||||
|
<h3>Edit Schema</h3>
|
||||||
|
|
||||||
|
<form onsubmit={handleSchemaCreate}>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
Name
|
||||||
|
<input
|
||||||
|
bind:value={data.schema.name}
|
||||||
|
placeholder="ex. Blog Posts"
|
||||||
|
minlength="2"
|
||||||
|
maxlength="30"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<small id="alias-helper">Plural is recommended</small>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Alias
|
||||||
|
<input
|
||||||
|
bind:value={data.schema.alias}
|
||||||
|
placeholder="ex. blog_posts"
|
||||||
|
minlength="2"
|
||||||
|
maxlength="30"
|
||||||
|
required
|
||||||
|
aria-describedby="alias-helper"
|
||||||
|
/>
|
||||||
|
<small id="alias-helper">
|
||||||
|
Developers will use this to reference the field
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<button type="submit">Update</button>
|
||||||
|
</form>
|
||||||
|
{/snippet}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import ChannelLayout from "../../layouts/ChannelLayout.svelte";
|
import ChannelLayout from "../../layouts/ChannelLayout.svelte";
|
||||||
|
import Sortable from "../../common/Sortable.svelte";
|
||||||
import { post } from "../../modules/remote";
|
import { post } from "../../modules/remote";
|
||||||
import { getApp } from "../../app";
|
import { getApp } from "../../app";
|
||||||
let { channel, user, data } = $props();
|
let { channel, user, data } = $props();
|
||||||
let newSchemaName = $state("");
|
let newSchemaName = $state("");
|
||||||
let newSchemaAlias = $state("");
|
let newSchemaAlias = $state("");
|
||||||
|
let fields = $state(data.fields);
|
||||||
const app = getApp();
|
const app = getApp();
|
||||||
|
|
||||||
function handleSchemaCreate(e) {
|
function handleSchemaCreate(e) {
|
||||||
@@ -22,6 +24,20 @@
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSortUpdate(updatedFields) {
|
||||||
|
let updatedFieldIds = updatedFields.map((f) => f.id);
|
||||||
|
fields = fields.filter((f) => !updatedFieldIds.includes(f.id));
|
||||||
|
fields = [...fields, ...updatedFields];
|
||||||
|
|
||||||
|
post(
|
||||||
|
channel.lucentUrl + "/fields/reorder",
|
||||||
|
{
|
||||||
|
ids: updatedFieldIds,
|
||||||
|
},
|
||||||
|
(data, err) => {},
|
||||||
|
);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ChannelLayout {body} {channel} {user}></ChannelLayout>
|
<ChannelLayout {body} {channel} {user}></ChannelLayout>
|
||||||
@@ -65,15 +81,32 @@
|
|||||||
<div style="display: flex;gap:20px">
|
<div style="display: flex;gap:20px">
|
||||||
{#each data.schemas as schema}
|
{#each data.schemas as schema}
|
||||||
<article style="min-width: 300px;">
|
<article style="min-width: 300px;">
|
||||||
<header>{schema.name}</header>
|
<header>
|
||||||
|
<a href={app.url("schemas/edit/" + schema.id)}
|
||||||
|
>{schema.name}</a
|
||||||
|
>
|
||||||
|
</header>
|
||||||
<details>
|
<details>
|
||||||
<summary>Fields</summary>
|
<summary>Fields</summary>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<Sortable
|
||||||
<tr>
|
type="table"
|
||||||
<td>Title</td>
|
onUpdate={handleSortUpdate}
|
||||||
</tr>
|
items={fields.filter(
|
||||||
</tbody>
|
(field) => field.schemaId === schema.id,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{#snippet itemView(field)}
|
||||||
|
<td
|
||||||
|
><a
|
||||||
|
href={app.url(
|
||||||
|
"fields/edit/" + field.id,
|
||||||
|
)}>{field.name}</a
|
||||||
|
></td
|
||||||
|
>
|
||||||
|
<td>{field.type}</td>
|
||||||
|
{/snippet}
|
||||||
|
</Sortable>
|
||||||
</table>
|
</table>
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
|
|||||||
+87
-13
@@ -19,34 +19,108 @@ export function readableDatetime(date) {
|
|||||||
return format(parseJSON(date), "dd MMM yyyy HH:mm");
|
return format(parseJSON(date), "dd MMM yyyy HH:mm");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function stripHtml(html = "") {
|
export function stripHtml(html = "") {
|
||||||
let tmp = document.createElement("div");
|
let tmp = document.createElement("div");
|
||||||
tmp.innerHTML = html;
|
tmp.innerHTML = html;
|
||||||
return tmp.textContent || tmp.innerText || "";
|
return tmp.textContent || tmp.innerText || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function randomId(length = 10) {
|
export function randomId(length = 10) {
|
||||||
return Math.random().toString(36).substring(2, length + 2);
|
return Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substring(2, length + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clickOutside(node) {
|
export function clickOutside(node) {
|
||||||
|
const handleClick = (event) => {
|
||||||
const handleClick = event => {
|
|
||||||
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
||||||
node.dispatchEvent(
|
node.dispatchEvent(new CustomEvent("click_outside", node));
|
||||||
new CustomEvent('click_outside', node)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
document.addEventListener('click', handleClick, true);
|
document.addEventListener("click", handleClick, true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
document.removeEventListener('click', handleClick, true);
|
document.removeEventListener("click", handleClick, true);
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function arrayUnique(array) {
|
||||||
|
return array.filter((value, index) => array.indexOf(value) === index);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayUniqueBy(items, uniqueBy) {
|
||||||
|
const ids = new Set(items.map((item) => item[uniqueBy]));
|
||||||
|
return [...ids].map((id) => items.find((i) => i[uniqueBy] === id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayMoveElement(array, from, to) {
|
||||||
|
if (from === to) {
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = array.find((v, i) => i === from);
|
||||||
|
const arrayWithout = array.filter((v, i) => i !== from);
|
||||||
|
if (from > to) {
|
||||||
|
return arrayWithout.reduce((c, v, i) => {
|
||||||
|
if (i === to) {
|
||||||
|
return [...c, item, v];
|
||||||
|
}
|
||||||
|
return [...c, v];
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return arrayWithout.reduce((c, v, i) => {
|
||||||
|
if (i + 1 === to) {
|
||||||
|
return [...c, v, item];
|
||||||
|
}
|
||||||
|
return [...c, v];
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEqual(db, ed) {
|
||||||
|
let isObject = (x) =>
|
||||||
|
typeof x === "object" && !Array.isArray(x) && x !== null;
|
||||||
|
let isArray = (x) => x?.constructor === Array;
|
||||||
|
let isEmpty = (x) => x === null || x === undefined || x == [];
|
||||||
|
const db_value = db ?? null;
|
||||||
|
const ed_value = ed ?? null;
|
||||||
|
|
||||||
|
if (isObject(db_value)) {
|
||||||
|
let keys = Object.keys(db_value);
|
||||||
|
return keys.reduce((acc, k) => {
|
||||||
|
if (acc === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isEqual(db_value?.[k], ed_value?.[k]);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
if (isArray(db_value)) {
|
||||||
|
return db_value.reduce((c, v, i) => {
|
||||||
|
if (c === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isEqual(v, ed_value?.[i]);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(db_value) && isEmpty(ed_value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db_value == ed_value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// const ok = Object.keys,
|
||||||
|
// tx = typeof x,
|
||||||
|
// ty = typeof y;
|
||||||
|
// return x && y && tx === "object" && tx === ty
|
||||||
|
// ? ok(x).length === ok(y).length &&
|
||||||
|
// ok(x).every((key) => isEqual(x[key], y[key]))
|
||||||
|
// : x === y;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import ContentIndex from "./svelte/content/Index.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";
|
||||||
import FieldCreateEntry from "./entry/FieldCreateEntry/FieldCreateEntry.svelte";
|
import FieldCreateEntry from "./entry/FieldCreateEntry/FieldCreateEntry.svelte";
|
||||||
|
import FieldEditEntry from "./entry/FieldEditEntry/FieldEditEntry.svelte";
|
||||||
|
import SchemaEditEntry from "./entry/SchemaEditEntry/SchemaEditEntry.svelte";
|
||||||
import BuildReport from "./svelte/build/Report.svelte";
|
import BuildReport from "./svelte/build/Report.svelte";
|
||||||
import { createApp } from "./app";
|
import { createApp } from "./app";
|
||||||
|
|
||||||
@@ -31,6 +33,8 @@ const entryComponents = {
|
|||||||
setup: SetupIndex,
|
setup: SetupIndex,
|
||||||
schemas: SchemaEntry,
|
schemas: SchemaEntry,
|
||||||
fieldCreate: FieldCreateEntry,
|
fieldCreate: FieldCreateEntry,
|
||||||
|
fieldEdit: FieldEditEntry,
|
||||||
|
schemaEdit: SchemaEditEntry,
|
||||||
};
|
};
|
||||||
Turbo.start();
|
Turbo.start();
|
||||||
|
|
||||||
|
|||||||
@@ -10,19 +10,40 @@ class FieldRepo
|
|||||||
const TABLE_NAME = "fields";
|
const TABLE_NAME = "fields";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @@return Field[]
|
* @return Field[]
|
||||||
*/
|
*/
|
||||||
public static function all(): array
|
public static function all(): array
|
||||||
{
|
{
|
||||||
return DB::table(self::TABLE_NAME)
|
return DB::table(self::TABLE_NAME)
|
||||||
->orderBy("name")
|
->orderBy("rank")
|
||||||
->get()
|
->get()
|
||||||
->map(SchemaModule::fromDb(...))
|
->map(FieldModule::fromDb(...))
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function findOne(string $id): ?Field
|
||||||
|
{
|
||||||
|
return DB::table(self::TABLE_NAME)
|
||||||
|
->where("id", $id)
|
||||||
|
->get()
|
||||||
|
->map(FieldModule::fromDb(...))
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
public static function insert(Field $field): void
|
public static function insert(Field $field): void
|
||||||
{
|
{
|
||||||
DB::table(self::TABLE_NAME)->insert(FieldModule::toDb($field));
|
DB::table(self::TABLE_NAME)->insert(FieldModule::toDb($field));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function update(Field $field): void
|
||||||
|
{
|
||||||
|
DB::table(self::TABLE_NAME)
|
||||||
|
->where("id", $field->id)
|
||||||
|
->update(FieldModule::toDb($field));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function delete(string $fieldId): void
|
||||||
|
{
|
||||||
|
DB::table(self::TABLE_NAME)->where("id", $fieldId)->delete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ class SchemaRepo
|
|||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function findOne(string $id): ?Schema
|
||||||
|
{
|
||||||
|
return DB::table(self::TABLE_NAME)
|
||||||
|
->where("id", $id)
|
||||||
|
->get()
|
||||||
|
->map(SchemaModule::fromDb(...))
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
public static function insert(Schema $schema): void
|
public static function insert(Schema $schema): void
|
||||||
{
|
{
|
||||||
DB::table(self::TABLE_NAME)->insert(SchemaModule::toDb($schema));
|
DB::table(self::TABLE_NAME)->insert(SchemaModule::toDb($schema));
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class Field
|
|||||||
public string $name,
|
public string $name,
|
||||||
public string $type,
|
public string $type,
|
||||||
public string $schemaId,
|
public string $schemaId,
|
||||||
|
public int $rank,
|
||||||
public IFieldProp $props,
|
public IFieldProp $props,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,15 @@ class FieldProp
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function fromDb(string $propsJson): IFieldProp
|
public static function fromDb(string $type, string $propsJson): IFieldProp
|
||||||
{
|
{
|
||||||
$props = json_decode($propsJson, true);
|
$props = json_decode($propsJson, true);
|
||||||
return match ($props["type"]) {
|
return self::fromArray($type, $props);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromArray(string $type, array $props): IFieldProp
|
||||||
|
{
|
||||||
|
return match ($type) {
|
||||||
"text" => new TextFieldProp(
|
"text" => new TextFieldProp(
|
||||||
required: $props["required"] ?? false,
|
required: $props["required"] ?? false,
|
||||||
readonly: $props["readonly"] ?? false,
|
readonly: $props["readonly"] ?? false,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class FieldModule
|
|||||||
"alias" => $field->alias,
|
"alias" => $field->alias,
|
||||||
"name" => $field->name,
|
"name" => $field->name,
|
||||||
"type" => $field->type,
|
"type" => $field->type,
|
||||||
|
"rank" => $field->rank,
|
||||||
"props" => json_encode($field->props),
|
"props" => json_encode($field->props),
|
||||||
"schema_id" => $field->schemaId,
|
"schema_id" => $field->schemaId,
|
||||||
];
|
];
|
||||||
@@ -36,7 +37,11 @@ class FieldModule
|
|||||||
name: data_get($data, "name"),
|
name: data_get($data, "name"),
|
||||||
type: data_get($data, "type"),
|
type: data_get($data, "type"),
|
||||||
schemaId: data_get($data, "schema_id"),
|
schemaId: data_get($data, "schema_id"),
|
||||||
props: FieldProp::fromDb(data_get($data, "props")),
|
rank: data_get($data, "rank"),
|
||||||
|
props: FieldProp::fromDb(
|
||||||
|
data_get($data, "type"),
|
||||||
|
data_get($data, "props"),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ class FieldController extends Controller
|
|||||||
$name = $request->input("name");
|
$name = $request->input("name");
|
||||||
$alias = $request->input("alias");
|
$alias = $request->input("alias");
|
||||||
$fieldType = $request->input("fieldType");
|
$fieldType = $request->input("fieldType");
|
||||||
|
$fields = FieldRepo::all();
|
||||||
|
$newRank = collect($fields)->last()->rank + 1;
|
||||||
|
|
||||||
$validator = Validator::make($request->all(), [
|
$validator = Validator::make($request->all(), [
|
||||||
"name" => "required|string|max:30|min:2",
|
"name" => "required|string|max:30|min:2",
|
||||||
@@ -77,9 +79,98 @@ class FieldController extends Controller
|
|||||||
alias: $alias,
|
alias: $alias,
|
||||||
type: $fieldType,
|
type: $fieldType,
|
||||||
props: $fieldProps,
|
props: $fieldProps,
|
||||||
|
rank: $newRank,
|
||||||
);
|
);
|
||||||
|
|
||||||
FieldRepo::insert($field);
|
FieldRepo::insert($field);
|
||||||
return response()->json(["field" => $field], 201);
|
return response()->json(["field" => $field], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function edit(Request $request)
|
||||||
|
{
|
||||||
|
$fieldId = $request->route("id");
|
||||||
|
$fields = FieldRepo::all();
|
||||||
|
$field = collect($fields)->firstWhere("id", $fieldId);
|
||||||
|
|
||||||
|
if (empty($field)) {
|
||||||
|
return response()->json(["errors" => ["Field not found"]], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$schemas = SchemaRepo::all();
|
||||||
|
$schema = collect($schemas)->firstWhere("id", $field->schemaId);
|
||||||
|
|
||||||
|
return $this->svelte->render(
|
||||||
|
view: "fieldEdit",
|
||||||
|
title: "Edit Field",
|
||||||
|
data: [
|
||||||
|
"schemas" => $schemas,
|
||||||
|
"schema" => $schema,
|
||||||
|
"field" => $field,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postUpdate(Request $request)
|
||||||
|
{
|
||||||
|
$fieldData = $request->input("field");
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
"field.name" => "required|string|max:30|min:2",
|
||||||
|
"field.alias" => "required|alpha_dash:ascii|max:30|min:2",
|
||||||
|
]);
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return response()->json(["errors" => $validator->errors()], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$field = FieldRepo::findOne(data_get($fieldData, "id"));
|
||||||
|
|
||||||
|
if (empty($field)) {
|
||||||
|
return response()->json(["errors" => ["Field not found"]], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldProps = FieldProp::fromArray(
|
||||||
|
$field->type,
|
||||||
|
data_get($fieldData, "props"),
|
||||||
|
);
|
||||||
|
|
||||||
|
$field = new Field(
|
||||||
|
id: $field->id,
|
||||||
|
schemaId: $field->schemaId,
|
||||||
|
name: data_get($fieldData, "name"),
|
||||||
|
alias: data_get($fieldData, "alias"),
|
||||||
|
type: $field->type,
|
||||||
|
rank: $field->rank,
|
||||||
|
props: $fieldProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
FieldRepo::update($field);
|
||||||
|
return response()->json(["field" => $field], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postDelete(Request $request)
|
||||||
|
{
|
||||||
|
$fieldId = $request->input("fieldId");
|
||||||
|
$field = FieldRepo::findOne($fieldId);
|
||||||
|
|
||||||
|
if (empty($field)) {
|
||||||
|
return response()->json(["errors" => ["Field not found"]], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldRepo::delete($fieldId);
|
||||||
|
return response()->json([], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postReorder(Request $request)
|
||||||
|
{
|
||||||
|
$ids = $request->input("ids");
|
||||||
|
|
||||||
|
foreach ($ids as $index => $id) {
|
||||||
|
$field = FieldRepo::findOne($id);
|
||||||
|
if ($field) {
|
||||||
|
$field->rank = $index;
|
||||||
|
FieldRepo::update($field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response()->json(["ids" => $ids], 200);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use Illuminate\Http\Request;
|
|||||||
use Lucent\Account\AccountService;
|
use Lucent\Account\AccountService;
|
||||||
use Lucent\Account\AuthService;
|
use Lucent\Account\AuthService;
|
||||||
use Lucent\Core\Repository\SchemaRepo;
|
use Lucent\Core\Repository\SchemaRepo;
|
||||||
|
use Lucent\Core\Repository\FieldRepo;
|
||||||
use Lucent\Core\Schema\Data\Schema;
|
use Lucent\Core\Schema\Data\Schema;
|
||||||
use Lucent\Id\Id;
|
use Lucent\Id\Id;
|
||||||
use Lucent\Svelte\Svelte;
|
use Lucent\Svelte\Svelte;
|
||||||
@@ -23,12 +24,14 @@ class SchemaController extends Controller
|
|||||||
public function home()
|
public function home()
|
||||||
{
|
{
|
||||||
$schemas = SchemaRepo::all();
|
$schemas = SchemaRepo::all();
|
||||||
|
$fields = FieldRepo::all();
|
||||||
|
|
||||||
return $this->svelte->render(
|
return $this->svelte->render(
|
||||||
view: "schemas",
|
view: "schemas",
|
||||||
title: "Schemas",
|
title: "Schemas",
|
||||||
data: [
|
data: [
|
||||||
"schemas" => $schemas,
|
"schemas" => $schemas,
|
||||||
|
"fields" => $fields,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -58,4 +61,22 @@ class SchemaController extends Controller
|
|||||||
201,
|
201,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function edit(Request $request)
|
||||||
|
{
|
||||||
|
$id = $request->route("id");
|
||||||
|
$schema = SchemaRepo::findOne($id);
|
||||||
|
|
||||||
|
if (empty($schema)) {
|
||||||
|
return response()->json(["errors" => ["Schema not found"]], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->svelte->render(
|
||||||
|
view: "schemaEdit",
|
||||||
|
title: "Edit Schema",
|
||||||
|
data: [
|
||||||
|
"schema" => $schema,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,9 +61,23 @@ Route::group(
|
|||||||
"build",
|
"build",
|
||||||
]);
|
]);
|
||||||
Route::get("/schemas", [SchemaController::class, "home"]);
|
Route::get("/schemas", [SchemaController::class, "home"]);
|
||||||
|
Route::get("/schemas/edit/{id}", [SchemaController::class, "edit"]);
|
||||||
Route::post("/schemas", [SchemaController::class, "postCreate"]);
|
Route::post("/schemas", [SchemaController::class, "postCreate"]);
|
||||||
Route::get("/fields/create", [FieldController::class, "create"]);
|
Route::get("/fields/create", [FieldController::class, "create"]);
|
||||||
|
Route::get("/fields/edit/{id}", [FieldController::class, "edit"]);
|
||||||
Route::post("/fields", [FieldController::class, "postCreate"]);
|
Route::post("/fields", [FieldController::class, "postCreate"]);
|
||||||
|
Route::post("/fields/update", [
|
||||||
|
FieldController::class,
|
||||||
|
"postUpdate",
|
||||||
|
]);
|
||||||
|
Route::post("/fields/delete", [
|
||||||
|
FieldController::class,
|
||||||
|
"postDelete",
|
||||||
|
]);
|
||||||
|
Route::post("/fields/reorder", [
|
||||||
|
FieldController::class,
|
||||||
|
"postReorder",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware(["lucent.auth"])->group(function () {
|
Route::middleware(["lucent.auth"])->group(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user