crazy stuff

This commit is contained in:
2026-01-07 21:42:18 +02:00
parent 57b0727788
commit 874ddd33e2
41 changed files with 5580 additions and 5039 deletions
+10
View File
@@ -0,0 +1,10 @@
@import "./pico.min.css";
main {
display: flex;
gap: 40px;
aside {
width: 250px;
padding: 20px;
}
}
+4
View File
File diff suppressed because one or more lines are too long
+36
View File
@@ -0,0 +1,36 @@
<script>
import { getContext, onMount } from "svelte";
import RecordRow from "./RecordRow.svelte";
import ChannelLayout from "../../layouts/ChannelLayout.svelte";
import { get } from "../../modules/remote";
let { channel, user } = $props();
let records = $state([]);
let graph = $state(null);
let users = $state([]);
onMount(() => {
get(channel.lucentUrl + "/home/records", {}, (data) => {
records = data.records;
graph = data.graph;
users = data.users;
});
});
</script>
<ChannelLayout {body} {channel} {user}></ChannelLayout>
{#snippet body()}
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
{#if records.length > 0}
<div class="table">
<table class="">
<tbody>
{#each records as record (record.id)}
<tr>
<RecordRow {channel} {graph} {record} {users}
></RecordRow>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{/snippet}
+41
View File
@@ -0,0 +1,41 @@
<script>
import { formatDistanceToNow, parseJSON } from "date-fns";
import Avatar from "../../svelte/account/Avatar.svelte";
import { previewTitle } from "../../svelte/records/Preview";
import Preview from "../../svelte/files/Preview.svelte";
import { usernameById } from "../../svelte/account/users";
let { channel, users, record, graph } = $props();
let schema = $derived(
channel.schemas.find((s) => s.name === record.schema),
);
let frieldlyUpdatedAt = formatDistanceToNow(
parseJSON(record._sys.updatedAt),
{ addSuffix: true },
);
</script>
<td>
<div class="row-name">
{#if record.status === "draft"}
<span class="status">DRAFT</span>
{/if}
{#if schema.type === "files"}
<!-- <Preview {record} size="tiny" showFilename={true} /> -->
{:else}
<a href="{channel.lucentUrl}/records/{record.id}">
<!-- {previewTitle(channel.schemas, record, graph)} -->
</a>
{/if}
</div>
</td>
<td><a href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a> </td>
<td>
<div style="display: flex;gap: 14px">
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24} />
<div class="ms-2">
{frieldlyUpdatedAt}
</div>
</div>
</td>
+15
View File
@@ -0,0 +1,15 @@
<script>
import Header from "./Header.svelte";
import Navbar from "./Navbar.svelte";
let { body, channel, user } = $props();
</script>
<Header {channel} {user}></Header>
<main>
<aside class="sidebar-content">
<Navbar {channel}></Navbar>
</aside>
<div class="main-content">
{@render body()}
</div>
</main>
@@ -1,26 +1,23 @@
<script>
import Avatar from "../account/Avatar.svelte";
import {getContext} from "svelte";
import Dropdown from "../common/Dropdown.svelte";
const channel = getContext("channel");
const user = getContext("user");
console.log( channel.commands)
import Avatar from "../common/Avatar.svelte";
// import Dropdown from "../svelte/common/Dropdown.svelte";
let { channel, user } = $props();
</script>
<div class="top-nav ">
<div class="top-nav">
<a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a>
{#if channel.commands.length > 0}
<!-- {#if channel.commands.length > 0}
<Dropdown>
<div slot="button">Actions</div>
{#each channel.commands as command}
<a href="{channel.lucentUrl}/command-report/{command.signature}" class="top-nav-item">{command.name}</a>
<a
href="{channel.lucentUrl}/command-report/{command.signature}"
class="top-nav-item">{command.name}</a
>
{/each}
</Dropdown>
{/if}
{/if} -->
<!-- <div>-->
<!-- <form method="GET">-->
<!-- <input type="search" name="filter[search_regex]" placeholder="Search"-->
@@ -28,7 +25,6 @@
<!-- </form>-->
<!-- </div>-->
<a href="{channel.lucentUrl}/profile">
<Avatar side="28" name={user.name}/>
<Avatar side="28" name={user.name} />
</a>
</div>
+9
View File
@@ -0,0 +1,9 @@
<script>
let { channel } = $props();
</script>
<div class="sidebar-top">
<a class="logo" href={channel.lucentUrl}>{channel.name}</a>
<a class="nav-item" href="{channel.lucentUrl}/profile"> </a>
</div>
<div class="sidebar"></div>
+49 -39
View File
@@ -1,54 +1,64 @@
import {axiosInstance} from "./bootstrap";
import "../sass/app.scss";
import Account from "./svelte/Account.svelte";
import Channel from "./svelte/Channel.svelte";
import Mustache from "mustache";
import 'htmx.org';
import { axiosInstance } from "./bootstrap";
import { mount, setContext, unmount } from "svelte";
Mustache.escape = function (value) {
return value;
};
// import "../sass/app.scss";
import "../css/app.css";
import Register from "./svelte/account/Register.svelte";
import Login from "./svelte/account/Login.svelte";
import Verify from "./svelte/account/Verify.svelte";
import Profile from "./svelte/account/Profile.svelte";
import SetupIndex from "./svelte/setup/Index.svelte";
import Members from "./svelte/members/Members.svelte";
import RecordNotFound from "./svelte/records/NotFound.svelte";
import RecordEdit from "./svelte/records/Edit.svelte";
import ContentIndex from "./svelte/content/Index.svelte";
import HomeEntry from "./entry/HomeEntry/HomeEntry.svelte";
import BuildReport from "./svelte/build/Report.svelte";
// Define all components
const entryComponents = {
account: Account,
channel: Channel,
members: Members,
recordEdit: RecordEdit,
recordNotFound: RecordNotFound,
contentIndex: ContentIndex,
homeIndex: HomeEntry,
buildReport: BuildReport,
register: Register,
login: Login,
verify: Verify,
profile: Profile,
setup: SetupIndex,
};
let loadedComponents = [];
let loadSvelte = function () {
loadedComponents.map((comp) => comp.$destroy());
loadedComponents = [];
loadedComponents.map((comp) => unmount(comp));
loadedComponents = [];
const elements = document.body.querySelectorAll(".lucent-component");
if (elements.length === 0) {
return;
const elements = document.body.querySelectorAll(".lucent-component");
if (elements.length === 0) {
return;
}
const loadElement = function (element) {
const jsonData = document.getElementById("json-data").innerHTML;
const props = JSON.parse(jsonData);
const [__, view] = Object.entries(entryComponents).find(
([key, _]) => props.view === key,
);
if (!view) {
return [];
}
const loadElement = function (element) {
const componentId = element.attributes["data-layout"].value;
const [_, component] = Object.entries(entryComponents).find(
([key, _]) => componentId === key
);
if (!component) {
return [];
}
// props.axios = axiosInstance;
const jsonData = document.getElementById(
"json-" + componentId
).innerHTML;
const props = JSON.parse(jsonData);
props.axios = axiosInstance;
const compOptions = {
target: element,
props: props,
};
loadedComponents = [...loadedComponents, new component(compOptions)];
const compOptions = {
target: element,
props: props,
};
Array.from(elements).map(loadElement);
loadedComponents = [...loadedComponents, mount(view, compOptions)];
};
Array.from(elements).map(loadElement);
};
// document.addEventListener("turbo:load", loadSvelte);
document.addEventListener("DOMContentLoaded", loadSvelte);
+111
View File
@@ -0,0 +1,111 @@
import axios from "axios";
class Errors {
constructor(data) {
this.data = data ?? [];
}
first() {
return this.data[0] ?? null;
}
all() {
return this.data;
}
isEmpty() {
return this.data.length === 0;
}
isNotEmpty() {
return !this.isEmpty();
}
}
function makeErrors(errs) {
return new Errors(errs);
}
export function post(url, postData, callback) {
axios
.post(url, postData)
.then((res) => {
if (res.data.redirect !== undefined) {
// Turbo.visit(link(res.data.redirect));
return;
}
const errors = makeErrors(res.data.errors);
if (errors.isNotEmpty()) {
callback?.(null, errors);
} else {
callback?.(res.data, errors);
}
})
.catch((err) => {
const errors = makeErrors(["something went wrong"]);
callback?.(null, errors);
});
}
export function get(url, urlParams, callback) {
let params = url.endsWith("query")
? {
params: { query: JSON.stringify(urlParams) },
paramsSerializer: (params) => serializeParams(params),
}
: {
params: urlParams,
};
return axios
.get(url, params)
.then((res) => {
callback(res.data);
})
.catch((err) => {
console.log(err);
let errors = makeErrors(["something went wrong"]);
if (!err) {
errors = makeErrors([]);
}
callback(null, errors);
});
}
function serializeParams(obj, prefix = "") {
const params = [];
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
const paramKey = prefix ? `${prefix}[${key}]` : key;
if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value)
) {
// Nested object
params.push(serializeParams(value, paramKey));
} else if (Array.isArray(value)) {
// Array
value.forEach((item, index) => {
if (typeof item === "object" && item !== null) {
params.push(serializeParams(item, `${paramKey}[${index}]`));
} else {
params.push(
`${encodeURIComponent(`${paramKey}[${index}]`)}=${encodeURIComponent(item)}`,
);
}
});
} else {
// Simple value
params.push(
`${encodeURIComponent(paramKey)}=${encodeURIComponent(value)}`,
);
}
}
}
return params.flat().join("&");
}
@@ -1,7 +1,6 @@
<script>
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
import { range } from "lodash";
import NavItem from "./NavItem.svelte";
export let inModal;
export let modalUrl;
@@ -1,8 +1,6 @@
<script>
import {createEventDispatcher, getContext} from "svelte";
import {debounce} from "lodash";
import {previewTitle} from "../../records/Preview";
import { createEventDispatcher, getContext } from "svelte";
import { previewTitle } from "../../records/Preview";
const channel = getContext("channel");
const dispatch = createEventDispatcher();
@@ -10,9 +8,8 @@
export let value = "";
export let field;
let search = ""
$: searchOptions = []
let search = "";
$: searchOptions = [];
const updateResults = debounce((e) => {
axios
@@ -35,46 +32,36 @@
function apply(e, newOption) {
e.preventDefault();
value = newOption.id
value = newOption.id;
dispatch("addFilter");
value = ""
value = "";
}
</script>
<div class="reference-tags">
<input
type="search"
on:keyup={updateResults}
bind:value={search}
placeholder={"Search for "+field.label}
autocomplete="off"
type="search"
on:keyup={updateResults}
bind:value={search}
placeholder={"Search for " + field.label}
autocomplete="off"
/>
<div class="reference-tags-results">
{#if searchOptions}
{#each searchOptions as option (option.id)}
<div
class="reference-tags-option"
role="button"
tabindex="0"
on:click={(e) => apply(e, option)}
on:keypress={(e) => apply(e, option)}
class="reference-tags-option"
role="button"
tabindex="0"
on:click={(e) => apply(e, option)}
on:keypress={(e) => apply(e, option)}
>
{previewTitle(channel.schemas, option)}
</div>
{:else}
<div
class="start-typing">
Start typing...
</div>
<div class="start-typing">Start typing...</div>
{/each}
{/if}
</div>
</div>
-41
View File
@@ -1,41 +0,0 @@
<script>
import {getContext, onMount} from "svelte";
import RecordRow from "./RecordRow.svelte"
const channel = getContext("channel");
let records = [];
let graph = null;
let users = [];
onMount(() => {
axios
.get(channel.lucentUrl + "/home/records")
.then((response) => {
records = response.data.records;
graph = response.data.graph;
users = response.data.users;
})
.catch((error) => {
console.log(error);
});
});
</script>
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
{#if records.length > 0}
<div class="table">
<table class="">
<tbody>
{#each records as record (record.id)}
<tr>
<RecordRow {graph} {record} {users}/>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
-50
View File
@@ -1,50 +0,0 @@
<script>
import {formatDistanceToNow, parseJSON} from "date-fns";
import Avatar from "../account/Avatar.svelte";
import {previewTitle} from "../records/Preview";
import Preview from "../files/Preview.svelte";
import {usernameById} from "../account/users";
import {getContext} from "svelte";
const channel = getContext("channel");
export let users;
export let graph;
export let record;
let schema = channel.schemas.find((s) => s.name === record.schema);
let frieldlyUpdatedAt = formatDistanceToNow(
parseJSON(record._sys.updatedAt),
{addSuffix: true}
);
</script>
<td>
<div class="row-name">
{#if record.status === "draft"}
<span class="status">DRAFT</span>
{/if}
{#if schema.type === "files"}
<Preview {record} size="tiny" showFilename={true}/>
{:else}
<a
href="{channel.lucentUrl}/records/{record.id}"
>
{previewTitle(channel.schemas, record, graph)}
</a>
{/if}
</div>
</td>
<td><a
href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a
>
</td>
<td>
<div style="display: flex;gap: 14px">
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
<div class="ms-2">
{frieldlyUpdatedAt}
</div>
</div>
</td>
-39
View File
@@ -1,39 +0,0 @@
<script>
import NavbarMenu from "./NavbarMenu.svelte";
import {getContext} from "svelte";
export let schema;
const channel = getContext("channel");
const readableSchemas = getContext("readableSchemas");
const fileSchemas = readableSchemas.filter((sc) => sc.type === "files");
const otherSchemas = readableSchemas.filter((sc) => !sc.isEntry && sc.type === "collection");
</script>
<div class="sidebar-top">
<a class="logo" href="{channel.lucentUrl}">{channel.name}</a>
<a class="nav-item" href="{channel.lucentUrl}/profile">
</a>
</div>
<div class="sidebar">
<NavbarMenu
title="Content"
schemas={ readableSchemas.filter((sc) => sc.isEntry)}
schema={schema}
expanded={true}
/>
<NavbarMenu
title="Files"
schemas={ fileSchemas}
schema={schema}
/>
<NavbarMenu
title="Other"
schemas={ otherSchemas}
schema={schema}
/>
</div>
+47 -57
View File
@@ -1,14 +1,13 @@
<script>
import {afterUpdate, getContext, onMount} from "svelte";
import {isEqual} from "lodash";
import { afterUpdate, getContext, onMount } from "svelte";
import axios from "axios";
import EditHeader from "./header/EditHeader.svelte"
import FilePreview from "./FilePreview.svelte"
import ContentTabs from "./header/ContentTabs.svelte"
import FormField from "./FormField.svelte"
import Graph from "./Graph.svelte"
import Info from "./Info.svelte"
import ErrorAlert from "../common/ErrorAlert.svelte"
import EditHeader from "./header/EditHeader.svelte";
import FilePreview from "./FilePreview.svelte";
import ContentTabs from "./header/ContentTabs.svelte";
import FormField from "./FormField.svelte";
import Graph from "./Graph.svelte";
import Info from "./Info.svelte";
import ErrorAlert from "../common/ErrorAlert.svelte";
import Title from "./header/Title.svelte";
const channel = getContext("channel");
@@ -17,7 +16,7 @@
export let record;
export let graph = {
records: [],
edges: []
edges: [],
};
// export let recordHistory;
export let isCreateMode;
@@ -29,14 +28,11 @@
$: validationErrors = null;
$: errorMessage = validationErrors
? `Record submission failed. ${
Object.entries(validationErrors).length
} error(s)`
Object.entries(validationErrors).length
} error(s)`
: null;
let activeFields = schema.fields.filter(
(f) => f.name !== "id"
);
let activeFields = schema.fields.filter((f) => f.name !== "id");
onMount(() => {
setOriginalContent();
@@ -104,7 +100,9 @@
}
// remove trashed edges
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id);
graph.edges = graph.edges?.filter(
(edge) => !edge._isTrashed && edge.source === record.id,
);
axios
.post(channel.lucentUrl + "/records", {
record: record,
@@ -115,7 +113,8 @@
console.log("SAVE: SAVED");
if (isCreateMode) {
window.location = channel.lucentUrl + "/records/" + record.id;
window.location =
channel.lucentUrl + "/records/" + record.id;
} else {
record = response.data.records[0] ?? null;
if (!record) {
@@ -137,7 +136,7 @@
errorMessage = error.response.data.error;
} else {
validationErrors = error.response.data.error;
console.log(validationErrors)
console.log(validationErrors);
}
}
resolve(null);
@@ -149,70 +148,61 @@
}
</script>
<svelte:window on:beforeunload={beforeUnload}/>
<svelte:window on:beforeunload={beforeUnload} />
<div class="record-edit">
<div class="tools-header">
<!-- <Manager managerRecords={recordHistory} {graph}/>-->
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab/>
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab />
{#if isCreateMode}
<button
class="button primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
<button class="button primary btn-spinner" on:click={save}>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Create
</button>
{:else if hasUnsavedData}
<button
type="button"
class="button primary ms-2 btn btn-primary btn-spinner"
on:click={save}
type="button"
class="button primary ms-2 btn btn-primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Save
</button>
{/if}
</div>
<Title {schema} {record} {isCreateMode}/>
<Title {schema} {record} {isCreateMode} />
<ErrorAlert message={errorMessage}/>
<ErrorAlert message={errorMessage} />
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
<ContentTabs
{schema}
{isCreateMode}
bind:active={activeContentTab}
/>
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
{#if !["_graph", "_info"].includes(activeContentTab)}
<FilePreview {record} {schema}/>
<FilePreview {record} {schema} />
{#each activeFields as field (field.name)}
{#if activeContentTab === field.group}
<FormField
bind:data={record.data}
bind:graph={graph}
{field}
{schema}
{record}
{validationErrors}
{isCreateMode}
bind:data={record.data}
bind:graph
{field}
{schema}
{record}
{validationErrors}
{isCreateMode}
/>
{/if}
{/each}
{:else if activeContentTab === "_graph"}
<Graph {graph} {record}/>
<Graph {graph} {record} />
{:else if activeContentTab === "_info"}
<Info {record} {graph} {users} {schema}/>
<Info {record} {graph} {users} {schema} />
{/if}
</div>
</div>
+53 -71
View File
@@ -1,11 +1,10 @@
<script>
import {friendlyDate} from "../../helpers";
import { friendlyDate } from "../../helpers";
import Avatar from "../account/Avatar.svelte";
import {usernameById} from "../account/users";
import {isEqual} from "lodash";
import { usernameById } from "../account/users";
import Icon from "../common/Icon.svelte";
import RevisionCell from "./revisions/RevisionCell.svelte";
import {getContext} from "svelte";
import { getContext } from "svelte";
import RevisionEdgeRow from "./revisions/RevisionEdgeRow.svelte";
const channel = getContext("channel");
@@ -30,27 +29,27 @@
});
function getEdgesByField(fieldsWithDiff, revision) {
edgeFieldsDiff = graph.edges.filter((e) => e.depth === 1).reduce((c, e) => {
if (!c[e.field]) {
c[e.field] = {
record: [],
revision: [],
edgeFieldsDiff = graph.edges
.filter((e) => e.depth === 1)
.reduce((c, e) => {
if (!c[e.field]) {
c[e.field] = {
record: [],
revision: [],
};
}
}
c[e.field]["record"].push(e)
return c;
}, {});
c[e.field]["record"].push(e);
return c;
}, {});
edgeFieldsDiff = revision._edges.reduce((c, e) => {
if (!c[e.field]) {
c[e.field] = {
record: [],
revision: [],
}
};
}
c[e.field]["revision"].push(e)
c[e.field]["revision"].push(e);
return c;
}, edgeFieldsDiff);
}
@@ -62,7 +61,7 @@
fieldsWithDiff = schema.fields.filter((f) => {
return !isEqual(selectedRevision.data[f.name], record.data[f.name]);
});
getEdgesByField(fieldsWithDiff, revision)
getEdgesByField(fieldsWithDiff, revision);
revisionSection.scrollIntoView();
}
@@ -71,7 +70,7 @@
rollbackError = "";
axios
.post(
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision._sys.version}`
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision._sys.version}`,
)
.then((response) => {
window.location.reload();
@@ -84,7 +83,7 @@
}
</script>
<div class="lx-card ">
<div class="lx-card">
<div class="row">
<div class="col-8">
<div>
@@ -98,24 +97,22 @@
<div>
<span class="label text-end text-muted"> created </span>
<Avatar
name={usernameById(users, record._sys.createdBy)}
side={24}
name={usernameById(users, record._sys.createdBy)}
side={24}
/>
{friendlyDate(record._sys.createdAt)}
</div>
<div>
<span class="label text-end text-muted">updated </span>
<span class="label text-end text-muted">updated </span>
<Avatar
name={usernameById(users, record._sys.updatedBy)}
side={24}
name={usernameById(users, record._sys.updatedBy)}
side={24}
/>
{friendlyDate(record._sys.updatedAt)}
</div>
</div>
<div class="col-4">
<span class="label d-block text-muted "
>Rules for this schema
</span>
<span class="label d-block text-muted">Rules for this schema </span>
<small>
Each record maintains the last {schema.revisions}
versions
@@ -125,33 +122,31 @@
</div>
<div class="revisions">
{#if schema.revisions > 0}
<div class="header-small mb-3">Revisions</div>
<div class="header-small mb-3">Revisions</div>
{#each revisions as revision}
{#if revision._sys.version !== record._sys.version}
<div
class="revision"
class:active={revision._sys.version ===
class="revision"
class:active={revision._sys.version ===
selectedRevision?._sys.version}
>
<div class="version">
<span>version {revision._sys.version}</span>
<Avatar
name={usernameById(users, revision._sys.updatedBy)}
side={24}
name={usernameById(users, revision._sys.updatedBy)}
side={24}
/>
{friendlyDate(revision._sys.updatedAt)}
</div>
<div class="col-3 text-center">
<button
disabled={revision._sys.version ===
disabled={revision._sys.version ===
selectedRevision?._sys.version}
class="button"
on:click={(e) => compare(e, revision)}
>Compare
</button
>
class="button"
on:click={(e) => compare(e, revision)}
>Compare
</button>
</div>
</div>
{/if}
@@ -169,15 +164,13 @@
<p class="text-center fw-bold mb-3 mt-5">
If you choose to rollback to this revision
</p>
<button
on:click={rollback}
class="button"
>
<button on:click={rollback} class="button">
Rollback to version {selectedRevision._sys.version}
</button>
{#if rollbackError}
<span class="d-block text-danger mt-3">{rollbackError}</span>
<span class="d-block text-danger mt-3">{rollbackError}</span
>
{/if}
<div class="mt-3">
{#each fieldsWithDiff as field}
@@ -188,31 +181,28 @@
<!-- <div class="d-block" style="width:200px;">
{field.label}
</div> -->
<div
class="revision-field"
style="overflow:hidden"
>
<div class="revision-field" style="overflow:hidden">
<div class="compare-left">
<RevisionCell
{field}
side={record.data[field.name]}
colorClass="text-danger"
{field}
side={record.data[field.name]}
colorClass="text-danger"
/>
</div>
<div class="compare-center">
<span class="me-1">{field.label}</span>
<Icon
icon="angle-right"
width="12"
height="12"
icon="angle-right"
width="12"
height="12"
/>
</div>
<div class="compare-right">
<RevisionCell
edges={selectedRevision._edges}
{field}
side={selectedRevision.data[field.name]}
colorClass="text-success"
edges={selectedRevision._edges}
{field}
side={selectedRevision.data[field.name]}
colorClass="text-success"
/>
</div>
</div>
@@ -226,22 +216,16 @@
{/if}
<div class="mt-3">
<p class="text-center fw-bold mb-3 mt-5">
Record References
</p>
<p class="text-center fw-bold mb-3 mt-5">Record References</p>
{#each Object.entries(edgeFieldsDiff) as [field, edges]}
<div
class="revision-references"
style="overflow:hidden"
>
<div class="revision-references" style="overflow:hidden">
<div class="reference-field">
{field}:
</div>
<div class="reference-compare">
<p class="">Record</p>
{#each edges.record as edge}
<RevisionEdgeRow {edge}/>
<RevisionEdgeRow {edge} />
{:else}
<p>No references</p>
{/each}
@@ -249,7 +233,7 @@
<div class="reference-compare">
<p class="text-success">Revision</p>
{#each edges.revision as edge}
<RevisionEdgeRow {edge}/>
<RevisionEdgeRow {edge} />
{:else}
<p>No references</p>
{/each}
@@ -258,7 +242,5 @@
{/each}
</div>
</div>
{/if}
</div>
+41 -46
View File
@@ -1,7 +1,11 @@
<script>
import {afterUpdate, createEventDispatcher, getContext, onMount} from "svelte";
import {
afterUpdate,
createEventDispatcher,
getContext,
onMount,
} from "svelte";
import {isEqual} from "lodash";
import FormField from "./FormField.svelte";
import FilePreview from "./FilePreview.svelte";
import ContentTabs from "./header/ContentTabs.svelte";
@@ -16,7 +20,7 @@
export let record;
export let graph = {
records: [],
edges: []
edges: [],
};
export let isCreateMode;
let originalContent;
@@ -25,13 +29,11 @@
$: validationErrors = null;
$: errorMessage = validationErrors
? `Record submission failed. ${
Object.entries(validationErrors).length
} error(s)`
Object.entries(validationErrors).length
} error(s)`
: null;
let activeFields = schema.fields.filter(
(f) => f.name !== "id"
);
let activeFields = schema.fields.filter((f) => f.name !== "id");
let tabname = "_default";
let fieldToTabs = schema.fields.reduce((c, f) => {
@@ -114,8 +116,10 @@
return;
}
// remove trashed edges
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id) ?? [];
graph.edges =
graph.edges?.filter(
(edge) => !edge._isTrashed && edge.source === record.id,
) ?? [];
axios
.post(channel.lucentUrl + "/records", {
@@ -150,64 +154,55 @@
}
</script>
<svelte:window on:beforeunload={beforeUnload}/>
<svelte:window on:beforeunload={beforeUnload} />
<div class="inline-edit record-edit">
<div class="tools-header">
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab/>
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab />
{#if isCreateMode}
<button
class="button primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
<button class="button primary btn-spinner" on:click={save}>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Create
</button>
{:else if hasUnsavedData}
<button
type="button"
class="button primary ms-2 btn btn-primary btn-spinner"
on:click={save}
type="button"
class="button primary ms-2 btn btn-primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Save
</button>
{/if}
</div>
<Title {schema} {record} {isCreateMode}/>
<ErrorAlert message={errorMessage}/>
<Title {schema} {record} {isCreateMode} />
<ErrorAlert message={errorMessage} />
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
<ContentTabs
{schema}
{isCreateMode}
bind:active={activeContentTab}
/>
<FilePreview {record} {schema}/>
<ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
<FilePreview {record} {schema} />
<!-- <fieldset disabled="disabled"> -->
{#each activeFields as field (field.name)}
{#if activeContentTab === field.group}
<FormField
bind:data={record.data}
bind:graph={graph}
{field}
{schema}
{record}
{validationErrors}
{isCreateMode}
bind:data={record.data}
bind:graph
{field}
{schema}
{record}
{validationErrors}
{isCreateMode}
/>
{/if}
{/each}
<!-- </fieldset> -->
</div>
</div>
@@ -1,9 +1,8 @@
<script>
import {getContext} from "svelte";
import {debounce} from "lodash";
import {previewTitle} from "../Preview";
import {getErrorMessage} from "./errorMessage";
import {insertEdges} from "./reference.js";
import { getContext } from "svelte";
import { previewTitle } from "../Preview";
import { getErrorMessage } from "./errorMessage";
import { insertEdges } from "./reference.js";
import Icon from "../../common/Icon.svelte";
const channel = getContext("channel");
@@ -15,20 +14,24 @@
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
$: references = graph.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id == edge.target && record.id == edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let search = ""
$: searchOptions = []
$: references =
graph.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find(
(increc) =>
increc.id == edge.target && record.id == edge.source,
);
})
.filter((rec) => (rec?.id ? true : false)) ?? [];
let search = "";
$: searchOptions = [];
function removeReference(e, recordId) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === recordId && edge.field === field.name)
(edge) => !(edge.target === recordId && edge.field === field.name),
);
}
@@ -41,14 +44,14 @@
schema: field.collections[0],
status: "published",
data: {
[field.searchField]: newValue
}
[field.searchField]: newValue,
},
},
})
.then((response) => {
searchOptions = [];
insert(e, response.data.records[0]);
console.log(response)
console.log(response);
})
.catch((error) => {
searchOptions = [];
@@ -58,10 +61,16 @@
function insert(e, insertRecord) {
e.preventDefault();
graph = insertEdges(graph, record, [insertRecord], field.name, e.detail.action);
search = ""
searchEl.focus()
searchEl.blur()
graph = insertEdges(
graph,
record,
[insertRecord],
field.name,
e.detail.action,
);
search = "";
searchEl.focus();
searchEl.blur();
}
const updateResults = debounce((e) => {
@@ -82,9 +91,8 @@
console.log(error);
});
}, 500);
</script>
<div class="reference-tags">
{#if errorMessage}
<div class="invalid-feedback d-block mb-3">
@@ -93,44 +101,39 @@
{/if}
<input
type="search"
bind:this={searchEl}
{id}
on:keyup={updateResults}
class:is-invalid={errorMessage}
bind:value={search}
placeholder={"Search for "+field.label}
autocomplete="off"
type="search"
bind:this={searchEl}
{id}
on:keyup={updateResults}
class:is-invalid={errorMessage}
bind:value={search}
placeholder={"Search for " + field.label}
autocomplete="off"
/>
<div class="reference-tags-results">
{#if searchOptions}
{#each searchOptions as option (option.id)}
<div
class="reference-tags-option"
role="button"
tabindex="0"
on:click={(e) => insert(e, option)}
on:keypress={(e) => insert(e, option)}
>
{previewTitle(channel.schemas, option ,graph)}
</div>
{:else}
<div
class="start-typing">
Start typing...
</div>
{/each}
{/if}
{#if search }
<div
class="reference-tags-option"
role="button"
tabindex="0"
on:click={(e) => saveNew(e,search)}
on:keypress={(e) => saveNew(e,search)}
on:click={(e) => insert(e, option)}
on:keypress={(e) => insert(e, option)}
>
{previewTitle(channel.schemas, option, graph)}
</div>
{:else}
<div class="start-typing">Start typing...</div>
{/each}
{/if}
{#if search}
<div
class="reference-tags-option"
role="button"
tabindex="0"
on:click={(e) => saveNew(e, search)}
on:keypress={(e) => saveNew(e, search)}
>
Add "{search}"
</div>
@@ -142,26 +145,23 @@
<div style="display: flex;align-items: center;gap: 4px">
{#each references as record (record.id)}
<span class="reference-tags-selected-value">
<a
class="record-title"
href="{channel.lucentUrl}/records/{record.id}"
>
{previewTitle(channel.schemas, record)}
</a>
<a
class="record-title"
href="{channel.lucentUrl}/records/{record.id}"
>
{previewTitle(channel.schemas, record)}
</a>
<button
on:click|preventDefault={(e) => removeReference(e, record.id)}
type="button"
class="button-text"
aria-label="Close"
on:click|preventDefault={(e) =>
removeReference(e, record.id)}
type="button"
class="button-text"
aria-label="Close"
>
<Icon width={12} height={12} icon="close"></Icon>
</button>
<Icon width={12} height={12} icon="close"></Icon>
</button>
</span>
{/each}
</div>
{/if}
+8 -10
View File
@@ -1,5 +1,4 @@
<script>
import { v4 as uuidv4 } from "uuid";
import { getContext } from "svelte";
import Icon from "../../common/Icon.svelte";
import { getErrorMessage } from "./errorMessage";
@@ -14,12 +13,11 @@
function generateId(e) {
e.preventDefault();
value = uuidv4();
value = randomUUID();
}
</script>
<div class="mb-0">
<div class="d-flex justify-content-between">
<input
type="text"
@@ -31,13 +29,13 @@
{readonly}
/>
{#if !readonly}
<button
class="btn btn-primary ms-2"
title="Generate a new UUIDv4"
on:click={generateId}
>
<Icon icon="dice" />
</button>
<button
class="btn btn-primary ms-2"
title="Generate a new UUIDv4"
on:click={generateId}
>
<Icon icon="dice" />
</button>
{/if}
</div>
+2 -3
View File
@@ -1,11 +1,10 @@
<script>
import { uniqueId } from "lodash";
import { getContext } from "svelte";
const channelurl = getContext("channelurl");
export let field;
export let value;
export let schema;
let id = uniqueId();
let id = randomUUID();
</script>
<div class="mb-0">
@@ -25,6 +24,6 @@
placeholder="https://www.example.com"
/>
{#if field.help}
<small class=" text-primary opacity-50">{field.help}</small>
<small class=" text-primary opacity-50">{field.help}</small>
{/if}
</div>
+28 -21
View File
@@ -1,24 +1,31 @@
import {uniqBy} from "lodash";
export function insertEdges(
graph,
sourceRecord,
targetRecords,
fieldName,
action = "",
) {
let newEdges = targetRecords.map((r) => {
return {
target: r.id,
source: sourceRecord.id,
sourceSchema: sourceRecord.schema,
targetSchema: r.schema,
field: fieldName,
depth: 1,
rank: "",
};
});
export function insertEdges(graph, sourceRecord, targetRecords, fieldName, action = "") {
let newEdges = targetRecords.map((r) => {
return {
target: r.id,
source: sourceRecord.id,
sourceSchema: sourceRecord.schema,
targetSchema: r.schema,
field: fieldName,
depth: 1,
rank: ""
};
});
let replacedEdges = graph.edges;
if (action === "replace") {
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
}
let replacedEdges = graph.edges;
if (action === "replace") {
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
}
graph.records = uniqBy([...graph.records, ...targetRecords], (r) => r.id);
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.source + edge.target + edge.field + edge.depth);
return graph;
graph.records = uniqBy([...graph.records, ...targetRecords], (r) => r.id);
graph.edges = uniqBy(
[...replacedEdges, ...newEdges],
(edge) => edge.source + edge.target + edge.field + edge.depth,
);
return graph;
}
+4611 -4376
View File
File diff suppressed because it is too large Load Diff
+28 -32
View File
@@ -1,34 +1,30 @@
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"@codemirror/commands": "^6.6.0",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.2.5",
"@codemirror/state": "^6.4.1",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"axios": "^1.7.4",
"codemirror": "^6.0.1",
"date-fns": "^3.6.0",
"flatpickr": "^4.6.13",
"fuse.js": "^7.0.0",
"htmx.org": "^2.0.1",
"install": "^0.13.0",
"laravel-vite-plugin": "^1.0.5",
"lodash": "^4.17.21",
"mustache": "^4.2.0",
"npm": "^10.8.2",
"postcss": "8.4.31",
"sass": "^1.77.8",
"sortablejs": "^1.15.2",
"svelte": "^4.2.18",
"tinymce": "^6.8.4",
"trix": "^2.1.5",
"uuid": "^10.0.0",
"vite": "5.2.6"
}
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"@codemirror/commands": "^6.6.0",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.2.5",
"@codemirror/state": "^6.4.1",
"@sveltejs/vite-plugin-svelte": "^6.2.3",
"axios": "^1.7.4",
"codemirror": "^6.0.1",
"date-fns": "^3.6.0",
"flatpickr": "^4.6.13",
"fuse.js": "^7.0.0",
"install": "^0.13.0",
"mustache": "^4.2.0",
"npm": "^10.8.2",
"postcss": "8.4.31",
"sass": "^1.77.8",
"sortablejs": "^1.15.2",
"svelte": "^5.46.1",
"tinymce": "^6.8.4",
"trix": "^2.1.5",
"vite": "^7.3.1"
}
}
+30 -6
View File
@@ -1,7 +1,31 @@
@extends('lucent::layouts.'.$layout)
@section('title')
{{ $title }}
@endsection
@section('content')
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title> {{ $title }} - Lucent Data Platform</title>
@if(config("lucent.env") == "production")
<!-- if production -->
<link rel="stylesheet" href="{{url('vendor/lucent/dist/'.$manifest['main.js']["css"][0])}}"/>
<script type="module" src="{{url('vendor/lucent/dist/'.$manifest['main.js']["file"])}}"></script>
@else
<!-- if development -->
@php
echo '<script type="module" crossorigin src="http://127.0.0.1:5173/@vite/client"></script>';
@endphp
<script type="module" crossorigin src="http://127.0.0.1:5173/main.js"></script>
@endif
<link rel="icon" type="image/x-icon" href="{{url('favicon.ico')}}">
</head>
<body>
{!! $svelte !!}
@endsection
</body>
</html>