edit schema wip
This commit is contained in:
@@ -8,3 +8,9 @@ main {
|
||||
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 { post } from "../../modules/remote";
|
||||
import { getApp } from "../../app";
|
||||
let { channel, user, data } = $props();
|
||||
let { channel, user, data, newRank } = $props();
|
||||
let name = $state("");
|
||||
let alias = $state("");
|
||||
const app = getApp();
|
||||
|
||||
function handleSchemaCreate(e) {
|
||||
function handleCreate(e) {
|
||||
e.preventDefault();
|
||||
console.log(data);
|
||||
post(
|
||||
app.url("fields"),
|
||||
{
|
||||
@@ -33,7 +32,7 @@
|
||||
{#snippet body()}
|
||||
<h3 class="header-small mb-4 mt-5">Create a <em>{data.type}</em> field</h3>
|
||||
|
||||
<form onsubmit={handleSchemaCreate}>
|
||||
<form onsubmit={handleCreate}>
|
||||
<fieldset>
|
||||
<label>
|
||||
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>
|
||||
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();
|
||||
let newSchemaName = $state("");
|
||||
let newSchemaAlias = $state("");
|
||||
let fields = $state(data.fields);
|
||||
const app = getApp();
|
||||
|
||||
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>
|
||||
|
||||
<ChannelLayout {body} {channel} {user}></ChannelLayout>
|
||||
@@ -65,15 +81,32 @@
|
||||
<div style="display: flex;gap:20px">
|
||||
{#each data.schemas as schema}
|
||||
<article style="min-width: 300px;">
|
||||
<header>{schema.name}</header>
|
||||
<header>
|
||||
<a href={app.url("schemas/edit/" + schema.id)}
|
||||
>{schema.name}</a
|
||||
>
|
||||
</header>
|
||||
<details>
|
||||
<summary>Fields</summary>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Title</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<Sortable
|
||||
type="table"
|
||||
onUpdate={handleSortUpdate}
|
||||
items={fields.filter(
|
||||
(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>
|
||||
</details>
|
||||
<details>
|
||||
|
||||
+103
-29
@@ -1,52 +1,126 @@
|
||||
import {formatDistanceToNow, parseJSON, format, parse} from "date-fns";
|
||||
import { formatDistanceToNow, parseJSON, format, parse } from "date-fns";
|
||||
|
||||
export function friendlyDate(date) {
|
||||
return formatDistanceToNow(parseJSON(date), {addSuffix: true});
|
||||
return formatDistanceToNow(parseJSON(date), { addSuffix: true });
|
||||
}
|
||||
|
||||
export function readableDate(date) {
|
||||
if(!date){
|
||||
return "";
|
||||
}
|
||||
return format(parseJSON(date), "dd MMM yyyy");
|
||||
if (!date) {
|
||||
return "";
|
||||
}
|
||||
return format(parseJSON(date), "dd MMM yyyy");
|
||||
}
|
||||
|
||||
export function readableDatetime(date) {
|
||||
if(!date){
|
||||
return "";
|
||||
}
|
||||
if (!date) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return format(parseJSON(date), "dd MMM yyyy HH:mm");
|
||||
return format(parseJSON(date), "dd MMM yyyy HH:mm");
|
||||
}
|
||||
|
||||
|
||||
export function stripHtml(html = "") {
|
||||
let tmp = document.createElement("div");
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || "";
|
||||
let tmp = document.createElement("div");
|
||||
tmp.innerHTML = html;
|
||||
return tmp.textContent || tmp.innerText || "";
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
|
||||
const handleClick = event => {
|
||||
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
||||
node.dispatchEvent(
|
||||
new CustomEvent('click_outside', node)
|
||||
)
|
||||
}
|
||||
const handleClick = (event) => {
|
||||
if (node && !node.contains(event.target) && !event.defaultPrevented) {
|
||||
node.dispatchEvent(new CustomEvent("click_outside", node));
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClick, true);
|
||||
document.addEventListener("click", handleClick, true);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
document.removeEventListener('click', handleClick, true);
|
||||
}
|
||||
}
|
||||
return {
|
||||
destroy() {
|
||||
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 SchemaEntry from "./entry/SchemaEntry/SchemaEntry.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 { createApp } from "./app";
|
||||
|
||||
@@ -31,6 +33,8 @@ const entryComponents = {
|
||||
setup: SetupIndex,
|
||||
schemas: SchemaEntry,
|
||||
fieldCreate: FieldCreateEntry,
|
||||
fieldEdit: FieldEditEntry,
|
||||
schemaEdit: SchemaEditEntry,
|
||||
};
|
||||
Turbo.start();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user