file uploads

This commit is contained in:
2026-05-06 18:11:42 +03:00
parent 16e50e2d49
commit 5587e8b4b6
41 changed files with 685 additions and 1067 deletions
-68
View File
@@ -1,68 +0,0 @@
<script>
import {getContext} from "svelte";
import Preview from "../files/Preview.svelte";
import {selectRecord} from "./functions/recordSelect.js";
const channel = getContext("channel");
export let schema;
export let records;
export let isWritable;
export let selected = [];
function select(record) {
selected = selectRecord(record, selected)
}
</script>
<div class="row" style="max-width:1000px">
{#each records as record (record.id)}
<div class="col-6 col-md-4">
<div
class="file-wrapper rounded p-2 mb-4 bg-light"
class:selected={selected.includes(record)}
>
{#if isWritable}
<div class="form-check">
<input
on:change={() => select(record)}
class="form-check-input "
type="checkbox"
checked={selected.find(
(r) => r.id === record.id
)}
value={record}
/>
</div>
{/if}
<div class="d-flex justify-content-center">
<Preview {record} size="medium"/>
</div>
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path}
class="d-block text-center overflow-hidden text-nowrap my-2 "
style="
text-overflow: ellipsis;
font-size: 13px;
color: #333;
">{record._file.path}</a
>
<span
class="lx-small-text text-muted d-block text-center"
>{record._file.mime}</span
>
</div>
</div>
{/each}
</div>
<style>
.form-check {
display: inline-block;
margin-bottom: 0;
}
</style>
+32 -34
View File
@@ -3,7 +3,7 @@
import Pagination from "./pagination/Pagination.svelte"; import Pagination from "./pagination/Pagination.svelte";
import ActionsOnSelected from "./ActionsOnSelected.svelte"; import ActionsOnSelected from "./ActionsOnSelected.svelte";
import Table from "./Table.svelte"; import Table from "./Table.svelte";
import {getContext} from "svelte"; import { getContext } from "svelte";
const axios = getContext("axios"); const axios = getContext("axios");
export let schema; export let schema;
@@ -47,51 +47,49 @@
</script> </script>
<div class=""> <div class="">
<div class="{inModal ? 'mt-0' : 'mt-5'}"> <div class={inModal ? "mt-0" : "mt-5"}>
<h3 class="header-normal mb-5 "> <h3 class="header-normal mb-5">
{schema.label} {schema.label}
</h3> </h3>
{#if selected.length > 0 && !inModal && isWritable} {#if selected.length > 0 && !inModal && isWritable}
<ActionsOnSelected {schema} {selected} {filter}/> <ActionsOnSelected {schema} {selected} {filter} />
{:else} {:else}
<Tools <Tools
bind:schema bind:schema
bind:records bind:records
{systemFields} {systemFields}
{sortParam} {sortParam}
{sortField} {sortField}
{operators} {operators}
{filter} {filter}
{graph} {graph}
{inModal} {inModal}
{modalUrl} {modalUrl}
{isWritable} {isWritable}
on:refresh={refresh} on:refresh={refresh}
/> />
{/if} {/if}
<Table <Table
{records} {records}
{graph} {graph}
{schema} {schema}
{sortParam} {sortParam}
{sortField} {sortField}
{systemFields} {systemFields}
{inModal} {inModal}
{users} {users}
{isWritable} {isWritable}
bind:selected bind:selected
/> />
</div> </div>
<Pagination <Pagination
{limit} {limit}
{skip} {skip}
{total} {total}
on:refresh={refresh} on:refresh={refresh}
{inModal} {inModal}
{modalUrl} {modalUrl}
/> />
</div> </div>
+20 -13
View File
@@ -2,8 +2,8 @@
import RenderField from "./RenderField.svelte"; import RenderField from "./RenderField.svelte";
import Avatar from "../account/Avatar.svelte"; import Avatar from "../account/Avatar.svelte";
import Status from "../records/Status.svelte"; import Status from "../records/Status.svelte";
import {usernameById} from "../account/users"; import { usernameById } from "../account/users";
import {friendlyDate} from "../../helpers"; import { friendlyDate } from "../../helpers";
export let schema; export let schema;
export let users; export let users;
@@ -12,7 +12,6 @@
export let sortParam; export let sortParam;
export let sortField; export let sortField;
export let visibleColumns; export let visibleColumns;
</script> </script>
{#each visibleColumns as field, index} {#each visibleColumns as field, index}
@@ -20,7 +19,7 @@
class="field-ui-{field.info.name}" class="field-ui-{field.info.name}"
class:is-sort={field.name === sortField.name} class:is-sort={field.name === sortField.name}
> >
<RenderField {record} {schema} {graph} {field}/> <RenderField {record} {schema} {graph} {field} />
</td> </td>
{/each} {/each}
{#if schema.visible?.includes("status")} {#if schema.visible?.includes("status")}
@@ -28,32 +27,40 @@
class="text-center" class="text-center"
class:is-sort={"-status" == sortParam || "status" == sortParam} class:is-sort={"-status" == sortParam || "status" == sortParam}
> >
<Status status={record.status}/> <Status status={record.status} />
</td> </td>
{/if} {/if}
{#if schema.visible?.includes("_sys.createdBy")} {#if schema.visible?.includes("_sys.createdBy")}
<td <td
class="text-center" class="text-center"
class:is-sort={"-_sys.createdBy" == sortParam || "_sys.createdBy" == sortParam} class:is-sort={"-_sys.createdBy" == sortParam ||
"_sys.createdBy" == sortParam}
> >
<Avatar name={usernameById(users, record._sys.createdBy)} side={24}/> <Avatar name={usernameById(users, record.createdBy)} side={24} />
</td> </td>
{/if} {/if}
{#if schema.visible?.includes("_sys.updatedBy")} {#if schema.visible?.includes("_sys.updatedBy")}
<td <td
class="text-center" class="text-center"
class:is-sort={"-_sys.updatedBy" == sortParam || "_sys.updatedBy" == sortParam} class:is-sort={"-_sys.updatedBy" == sortParam ||
"_sys.updatedBy" == sortParam}
> >
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/> <Avatar name={usernameById(users, record.updatedBy)} side={24} />
</td> </td>
{/if} {/if}
{#if schema.visible?.includes("_sys.createdAt")} {#if schema.visible?.includes("_sys.createdAt")}
<td class:is-sort={"-_sys.createdAt" == sortParam || "_sys.createdAt" == sortParam}> <td
{friendlyDate(record._sys.createdAt)} class:is-sort={"-_sys.createdAt" == sortParam ||
"_sys.createdAt" == sortParam}
>
{friendlyDate(record.createdAt)}
</td> </td>
{/if} {/if}
{#if schema.visible?.includes("_sys.updatedAt")} {#if schema.visible?.includes("_sys.updatedAt")}
<td class:is-sort={"-_sys.updatedAt" == sortParam || "_sys.updatedAt" == sortParam}> <td
{friendlyDate(record._sys.updatedAt)} class:is-sort={"-_sys.updatedAt" == sortParam ||
"_sys.updatedAt" == sortParam}
>
{friendlyDate(record.updatedAt)}
</td> </td>
{/if} {/if}
+60 -91
View File
@@ -1,13 +1,13 @@
<script> <script>
import RecordRow from "./RecordRow.svelte"; import RecordRow from "./RecordRow.svelte";
import {previewTitle} from "../records/Preview"; import { previewTitle } from "../records/Preview";
import {usernameById} from "../account/users"; import { usernameById } from "../account/users";
import {getContext} from "svelte"; import { getContext } from "svelte";
import Avatar from "../account/Avatar.svelte"; import Avatar from "../account/Avatar.svelte";
import {selectRecord, toggleAll} from "./functions/recordSelect.js"; import { selectRecord, toggleAll } from "./functions/recordSelect.js";
import Checkbox from "../common/Checkbox.svelte"; import Checkbox from "../common/Checkbox.svelte";
import Preview from "../files/Preview.svelte"; import Preview from "../files/Preview.svelte";
import {fileurl} from "../files/imageserver.js"; import { fileurl } from "../files/imageserver.js";
const channel = getContext("channel"); const channel = getContext("channel");
@@ -23,107 +23,80 @@
export let selected = []; export let selected = [];
function eventToggleAll(e) { function eventToggleAll(e) {
selected = toggleAll(e, records, selected) selected = toggleAll(e, records, selected);
} }
function select(record) { function select(record) {
selected = selectRecord(record, selected) selected = selectRecord(record, selected);
} }
$: visibleColumns = schema.fields.filter(c => schema.visible?.includes(c.name) ?? []) $: visibleColumns = schema.fields.filter(
(c) => schema.visible?.includes(c.name) ?? [],
);
</script> </script>
<div class="table mt-5 "> <div class="table mt-5">
<table> <table>
<thead> <thead>
<tr> <tr>
{#if isWritable} {#if isWritable}
<th> <th>
<Checkbox <Checkbox
value="" value=""
on:change={eventToggleAll} on:change={eventToggleAll}
indeterminate={selected.length > 0 && selected.length < records.length} indeterminate={selected.length > 0 &&
selected.length < records.length}
checked={selected.length === records.length} checked={selected.length === records.length}
> ></Checkbox>
</Checkbox> </th>
</th> {/if}
{/if}
{#each visibleColumns as field} {#each visibleColumns as field}
<th <th
class="field-ui-{field.info.name ?? field.ui}" class="field-ui-{field.info.name ?? field.ui}"
class:is-sort={field.name === sortField.name} class:is-sort={field.name === sortField.name}
scope="col" scope="col"
title={field.help} title={field.help}>{field.label}</th
>{field.label}</th >
> {/each}
{/each} {#each systemFields.filter( (c) => schema.visible?.includes(c.name), ) as sysField}
{#each systemFields.filter(c => schema.visible?.includes(c.name)) as sysField} <th class:is-sort={sysField.name === sortField.name}
<th class:is-sort={sysField.name === sortField.name}>{sysField.label}</th> >{sysField.label}</th
{/each} >
<th></th> {/each}
</tr> <th></th>
</tr>
</thead> </thead>
<tbody> <tbody>
{#each records as record (record.id)} {#each records as record (record.id)}
<tr> <tr>
<td class="title-td"> <td class="title-td">
<div <div class="title-td-contents">
class="title-td-contents" {#if isWritable}
> <Checkbox
{#if isWritable}
<Checkbox
on:change={() => select(record)} on:change={() => select(record)}
checked={selected.find((r) => r.id === record.id)} checked={selected.find(
(r) => r.id === record.id,
)}
value={record} value={record}
> ></Checkbox>
</Checkbox> {/if}
{/if}
{#if record._file?.path}
<div class="file-table-row">
<Preview record={record} size={record._file?.width > 0 ? "medium" : "small"}/>
<div>
{#if record.status === "draft"}
<span style="text-transform: uppercase;font-size:10px">{record.status}</span>
{/if}
<a
href="{channel.lucentUrl}/records/{record.id}"
target={inModal ? "_blank" : "_self"}
>
{previewTitle(channel.schemas, record, graph)}
</a>
<span>{(record._file.size / 1024).toFixed(1)}kB</span>
{#if record._file.width > 0}
<span>{record._file.width + "x" + record._file.height}</span>
{/if}
<a
href="{fileurl(channel,record)}"
target="_blank"
>
Download
</a>
</div>
</div>
{:else}
<a <a
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/records/{record.id}"
target={inModal ? "_blank" : "_self"} target={inModal ? "_blank" : "_self"}
> >
{#if record.status === "draft"} {#if record.status === "draft"}
<span style="text-transform: uppercase;font-size:10px">{record.status}</span> <span
style="text-transform: uppercase;font-size:10px"
>{record.status}</span
>
{/if} {/if}
{previewTitle(channel.schemas, record, graph)} {previewTitle(channel.schemas, record, graph)}
</a> </a>
{/if} </div>
</td>
<RecordRow
</div>
</td>
<RecordRow
{record} {record}
{graph} {graph}
{schema} {schema}
@@ -131,19 +104,15 @@
{sortParam} {sortParam}
{sortField} {sortField}
{users} {users}
/>
<td>
<Avatar
name={usernameById(
users,
record._sys.updatedBy
)}
side={24}
/> />
</td> <td>
</tr> <Avatar
{/each} name={usernameById(users, record.updatedBy)}
side={24}
/>
</td>
</tr>
{/each}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -1,6 +1,5 @@
<script> <script>
import FilterFields from "./FilterFields.svelte"; import FilterFields from "./FilterFields.svelte";
import Uploader from "../../files/Uploader.svelte";
import Icon from "../../common/Icon.svelte"; import Icon from "../../common/Icon.svelte";
import SortFields from "./SortFields.svelte"; import SortFields from "./SortFields.svelte";
import AppliedFilter from "./AppliedFilter.svelte"; import AppliedFilter from "./AppliedFilter.svelte";
+2 -8
View File
@@ -33,18 +33,12 @@
function insert(e) { function insert(e) {
e.preventDefault(); e.preventDefault();
dispatch("insert", { dispatch("insert_files", selectedRecords);
records: selectedRecords,
action: "insert",
});
} }
function replace(e) { function replace(e) {
e.preventDefault(); e.preventDefault();
dispatch("insert", { dispatch("replace_files", selectedRecords);
records: selectedRecords,
action: "replace",
});
} }
export function open(recordId) { export function open(recordId) {
-15
View File
@@ -97,21 +97,6 @@
</div> </div>
</div> </div>
</td> </td>
<!-- <RecordRow
{record}
{graph}
{schema}
{visibleColumns}
{sortParam}
{sortField}
{users}
/> -->
<!-- <td>
<Avatar
name={usernameById(users, record._sys.updatedBy)}
side={24}
/>
</td> -->
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
+24 -20
View File
@@ -1,25 +1,29 @@
export function sortByField(from, to, edges, fieldName, references) { export function sortByField(from, to, edges, fieldName, references) {
if (from === to) { if (from === to) {
return edges; return edges;
} }
let referenceIds = references.map(r => r.id); let referenceIds = references.map((r) => r.id);
let edgesTosort = edges?.filter((ed) => ed.field === fieldName && ed.depth === 1 && referenceIds.includes(ed.target)) ?? []; let edgesTosort =
let remainingEdge = edges?.filter((ed) => !(ed.field === fieldName && ed.depth === 1)) ?? []; edges?.filter(
(ed) =>
ed.field === fieldName &&
ed.depth === 1 &&
referenceIds.includes(ed.target),
) ?? [];
let remainingEdge =
edges?.filter((ed) => !(ed.field === fieldName && ed.depth === 1)) ?? [];
edgesTosort = array_move(edgesTosort,from, to); edgesTosort = array_move(edgesTosort, from, to);
return [...remainingEdge, ...edgesTosort]; return [...remainingEdge, ...edgesTosort];
} }
function array_move(arr, old_index, new_index) { export function array_move(arr, old_index, new_index) {
if (new_index >= arr.length) { if (new_index >= arr.length) {
var k = new_index - arr.length + 1; var k = new_index - arr.length + 1;
while (k--) { while (k--) {
arr.push(undefined); arr.push(undefined);
}
} }
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); }
return arr; // for testing arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
}; return arr; // for testing
}
+19 -27
View File
@@ -1,48 +1,40 @@
<script> <script>
import {formatDistanceToNow, parseJSON} from "date-fns"; import { formatDistanceToNow, parseJSON } from "date-fns";
import Avatar from "../account/Avatar.svelte"; import Avatar from "../account/Avatar.svelte";
import {previewTitle} from "../records/Preview"; import { previewTitle } from "../records/Preview";
import Preview from "../files/Preview.svelte"; import Preview from "../files/Preview.svelte";
import {usernameById} from "../account/users"; import { usernameById } from "../account/users";
import {getContext} from "svelte"; import { getContext } from "svelte";
const channel = getContext("channel"); const channel = getContext("channel");
export let users; export let users;
export let graph; export let graph;
export let record; export let record;
let schema = channel.schemas.find((s) => s.name === record.schema); let schema = channel.schemas.find((s) => s.name === record.schema);
let frieldlyUpdatedAt = formatDistanceToNow( let frieldlyUpdatedAt = formatDistanceToNow(parseJSON(record.updatedAt), {
parseJSON(record._sys.updatedAt), addSuffix: true,
{addSuffix: true} });
);
</script> </script>
<td> <td>
<div class="row-name"> <div class="row-name">
{#if record.status === "draft"} {#if record.status === "draft"}
<span class="status">DRAFT</span> <span class="status">DRAFT</span>
{/if} {/if}
{#if schema.type === "files"} {#if schema.type === "files"}
<Preview {record} size="tiny" showFilename={true}/> <Preview {record} size="tiny" showFilename={true} />
{:else} {:else}
<a <a href="{channel.lucentUrl}/records/{record.id}">
href="{channel.lucentUrl}/records/{record.id}" {previewTitle(channel.schemas, record, graph)}
</a>
> {/if}
{previewTitle(channel.schemas, record, graph)}
</a>
{/if}
</div> </div>
</td> </td>
<td><a <td><a href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a> </td>
href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a
>
</td>
<td> <td>
<div style="display: flex;gap: 14px"> <div style="display: flex;gap: 14px">
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/> <Avatar name={usernameById(users, record.updatedBy)} side={24} />
<div class="ms-2"> <div class="ms-2">
{frieldlyUpdatedAt} {frieldlyUpdatedAt}
</div> </div>
+15 -23
View File
@@ -1,14 +1,13 @@
<script> <script>
// https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387 // https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387
import {onDestroy, onMount} from "svelte"; import { onDestroy, onMount } from "svelte";
import {basicSetup, EditorView} from "codemirror"; import { basicSetup, EditorView } from "codemirror";
import {autocompletion, completionKeymap} from "@codemirror/autocomplete"; import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
import {Compartment, EditorState} from "@codemirror/state"; import { Compartment, EditorState } from "@codemirror/state";
import {keymap} from "@codemirror/view"; import { keymap } from "@codemirror/view";
import {indentWithTab} from "@codemirror/commands"; import { indentWithTab } from "@codemirror/commands";
import {markdown} from "@codemirror/lang-markdown"; import { markdown } from "@codemirror/lang-markdown";
import {lintKeymap} from "@codemirror/lint"; import { lintKeymap } from "@codemirror/lint";
let parentElement; let parentElement;
let codeMirrorView; let codeMirrorView;
@@ -17,10 +16,10 @@
export function insertMedia(info) { export function insertMedia(info) {
let insertText = ""; let insertText = "";
if (info.record._file.width > 0) { if (info.file.width > 0) {
insertText = `![${info.record._file.originalName}](${info.url})`; insertText = `![${info.file.filename}](${info.url})`;
} else { } else {
insertText = `[${info.record._file.originalName}](${info.originalUrl})`; insertText = `[${info.file.filename}](${info.originalUrl})`;
} }
const cursor = codeMirrorView.state.selection.main.head; const cursor = codeMirrorView.state.selection.main.head;
const transaction = codeMirrorView.state.update({ const transaction = codeMirrorView.state.update({
@@ -29,7 +28,7 @@
insert: insertText, insert: insertText,
}, },
// the next 2 lines will set the appropriate cursor position after inserting the new text. // the next 2 lines will set the appropriate cursor position after inserting the new text.
selection: {anchor: cursor + 1}, selection: { anchor: cursor + 1 },
scrollIntoView: true, scrollIntoView: true,
}); });
@@ -46,11 +45,7 @@
doc: value, doc: value,
extensions: [ extensions: [
basicSetup, basicSetup,
keymap.of([ keymap.of([indentWithTab, ...lintKeymap, ...completionKeymap]),
indentWithTab,
...lintKeymap,
...completionKeymap
]),
language.of(markdown()), language.of(markdown()),
markdown(), markdown(),
autocompletion(), autocompletion(),
@@ -63,17 +58,14 @@
} }
}), }),
EditorView.lineWrapping, EditorView.lineWrapping,
EditorView.contentAttributes.of({spellcheck: "true"}) EditorView.contentAttributes.of({ spellcheck: "true" }),
], ],
}); });
codeMirrorView = new EditorView({ codeMirrorView = new EditorView({
state, state,
parent: parentElement, parent: parentElement,
}); });
}); });
onDestroy(() => { onDestroy(() => {
@@ -83,4 +75,4 @@
}); });
</script> </script>
<div class="is-editable-{editable}" bind:this={parentElement}/> <div class="is-editable-{editable}" bind:this={parentElement} />
+34 -36
View File
@@ -1,68 +1,66 @@
<script> <script>
import {onDestroy, onMount} from "svelte"; import { onDestroy, onMount } from "svelte";
import Trix from "trix" import Trix from "trix";
import "trix/dist/trix.css" import "trix/dist/trix.css";
export let value = ""; export let value = "";
export let field; export let field;
let editor; let editor;
function updateValue(e) { function updateValue(e) {
value = e.target.value; value = e.target.value;
} }
export function insertMedia(info){ export function insertMedia(info) {
if(info.record._file.width > 0){ if (info.file.width > 0) {
var attachment = new Trix.Attachment({ content: info.html }) var attachment = new Trix.Attachment({ content: info.html });
editor.editor.insertAttachment(attachment) editor.editor.insertAttachment(attachment);
}else{ } else {
editor.editor.insertHTML(`<a href="${info.originalUrl}">${info.record._file.originalName}</a>`) editor.editor.insertHTML(
`<a href="${info.originalUrl}">${info.file.filename}</a>`,
);
} }
} }
onMount(() => { onMount(() => {
editor.addEventListener("trix-file-accept", (e) => { editor.addEventListener("trix-file-accept", (e) => {
e.preventDefault(); e.preventDefault();
}) });
editor.addEventListener("trix-before-initialize", (e) => { editor.addEventListener("trix-before-initialize", (e) => {
Trix.config.blockAttributes.heading1.tagName = 'h2'; Trix.config.blockAttributes.heading1.tagName = "h2";
const { toolbarElement } = e.target const { toolbarElement } = e.target;
const h1Button = toolbarElement.querySelector("[data-trix-attribute=heading1]") const h1Button = toolbarElement.querySelector(
h1Button.insertAdjacentHTML("afterend", `<button style="text-indent: initial;padding: 14px 10px !important;" type="button" class="trix-button trix-button--icon" data-trix-attribute="heading3" title="Heading 3" tabindex="-1" data-trix-active="">H3</button>`) "[data-trix-attribute=heading1]",
}) );
h1Button.insertAdjacentHTML(
"afterend",
}) `<button style="text-indent: initial;padding: 14px 10px !important;" type="button" class="trix-button trix-button--icon" data-trix-attribute="heading3" title="Heading 3" tabindex="-1" data-trix-active="">H3</button>`,
);
});
});
// onDestroy(() => { // onDestroy(() => {
// editor.removeEventListener("trix-before-initialize") // editor.removeEventListener("trix-before-initialize")
// }) // })
Trix.config.blockAttributes.default.breakOnReturn = false;
Trix.config.blockAttributes.default.breakOnReturn = false
Trix.config.blockAttributes.heading3 = { Trix.config.blockAttributes.heading3 = {
tagName: 'h3', tagName: "h3",
terminal: true, terminal: true,
breakOnReturn: true, breakOnReturn: true,
group: false group: false,
} };
// console.log(Trix.config) // console.log(Trix.config)
</script> </script>
<div class="tox-wrapper"> <div class="tox-wrapper">
<input id="x-{field.name}" {value} type="hidden"> <input id="x-{field.name}" {value} type="hidden" />
<trix-editor <trix-editor
bind:this={editor} bind:this={editor}
class=" content" class=" content"
input="x-{field.name}" input="x-{field.name}"
role="textbox" role="textbox"
tabindex="0" tabindex="0"
on:trix-change={updateValue} on:trix-change={updateValue}
></trix-editor> ></trix-editor>
</div> </div>
+46 -63
View File
@@ -1,14 +1,13 @@
<script> <script>
import {afterUpdate, getContext, onMount} from "svelte"; import { afterUpdate, getContext, onMount } from "svelte";
import {isEqual} from "lodash"; import { isEqual } from "lodash";
import axios from "axios"; import axios from "axios";
import EditHeader from "./header/EditHeader.svelte" import EditHeader from "./header/EditHeader.svelte";
import FilePreview from "./FilePreview.svelte" import ContentTabs from "./header/ContentTabs.svelte";
import ContentTabs from "./header/ContentTabs.svelte" import FormField from "./FormField.svelte";
import FormField from "./FormField.svelte" import Graph from "./Graph.svelte";
import Graph from "./Graph.svelte" import Info from "./Info.svelte";
import Info from "./Info.svelte" import ErrorAlert from "../common/ErrorAlert.svelte";
import ErrorAlert from "../common/ErrorAlert.svelte"
import Title from "./header/Title.svelte"; import Title from "./header/Title.svelte";
const channel = getContext("channel"); const channel = getContext("channel");
@@ -17,7 +16,7 @@
export let record; export let record;
export let graph = { export let graph = {
records: [], records: [],
edges: [] edges: [],
}; };
// export let recordHistory; // export let recordHistory;
export let isCreateMode; export let isCreateMode;
@@ -29,14 +28,11 @@
$: validationErrors = null; $: validationErrors = null;
$: errorMessage = validationErrors $: errorMessage = validationErrors
? `Record submission failed. ${ ? `Record submission failed. ${
Object.entries(validationErrors).length Object.entries(validationErrors).length
} error(s)` } error(s)`
: null; : null;
let activeFields = schema.fields.filter( let activeFields = schema.fields.filter((f) => f.name !== "id");
(f) => f.name !== "id"
);
onMount(() => { onMount(() => {
setOriginalContent(); setOriginalContent();
@@ -45,10 +41,7 @@
function setOriginalContent() { function setOriginalContent() {
originalContent = { originalContent = {
data: JSON.parse(JSON.stringify(record.data)), data: JSON.parse(JSON.stringify(record.data)),
schema: record.schema,
status: record.status, status: record.status,
_sys: JSON.parse(JSON.stringify(record._sys)),
_file: JSON.parse(JSON.stringify(record._file)),
edges: JSON.parse(JSON.stringify(graph.edges)), edges: JSON.parse(JSON.stringify(graph.edges)),
}; };
} }
@@ -79,10 +72,7 @@
} }
return !isEqual(originalContent, { return !isEqual(originalContent, {
data: record.data, data: record.data,
schema: record.schema,
status: record.status, status: record.status,
_sys: record._sys,
_file: record._file,
edges: graph.edges, edges: graph.edges,
}); });
} }
@@ -104,7 +94,9 @@
} }
// remove trashed edges // 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 axios
.post(channel.lucentUrl + "/records", { .post(channel.lucentUrl + "/records", {
record: record, record: record,
@@ -115,7 +107,8 @@
console.log("SAVE: SAVED"); console.log("SAVE: SAVED");
if (isCreateMode) { if (isCreateMode) {
window.location = channel.lucentUrl + "/records/" + record.id; window.location =
channel.lucentUrl + "/records/" + record.id;
} else { } else {
record = response.data.records[0] ?? null; record = response.data.records[0] ?? null;
if (!record) { if (!record) {
@@ -137,7 +130,7 @@
errorMessage = error.response.data.error; errorMessage = error.response.data.error;
} else { } else {
validationErrors = error.response.data.error; validationErrors = error.response.data.error;
console.log(validationErrors) console.log(validationErrors);
} }
} }
resolve(null); resolve(null);
@@ -149,70 +142,60 @@
} }
</script> </script>
<svelte:window on:beforeunload={beforeUnload}/> <svelte:window on:beforeunload={beforeUnload} />
<div class="record-edit"> <div class="record-edit">
<div class="tools-header"> <div class="tools-header">
<!-- <Manager managerRecords={recordHistory} {graph}/>--> <!-- <Manager managerRecords={recordHistory} {graph}/>-->
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab/> <EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab />
{#if isCreateMode} {#if isCreateMode}
<button <button class="button primary btn-spinner" on:click={save}>
class="button primary btn-spinner" <span
on:click={save} class="spinner-border spinner-border-sm"
> role="status"
<span aria-hidden="true"
class="spinner-border spinner-border-sm" />
role="status"
aria-hidden="true"
/>
Create Create
</button> </button>
{:else if hasUnsavedData} {:else if hasUnsavedData}
<button <button
type="button" type="button"
class="button primary ms-2 btn btn-primary btn-spinner" class="button primary ms-2 btn btn-primary btn-spinner"
on:click={save} on:click={save}
> >
<span <span
class="spinner-border spinner-border-sm" class="spinner-border spinner-border-sm"
role="status" role="status"
aria-hidden="true" aria-hidden="true"
/> />
Save Save
</button> </button>
{/if} {/if}
</div> </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;"> <div class=" mt-4" style="margin-bottom:150px;position:relative;">
<ContentTabs <ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
{schema}
{isCreateMode}
bind:active={activeContentTab}
/>
{#if !["_graph", "_info"].includes(activeContentTab)} {#if !["_graph", "_info"].includes(activeContentTab)}
<FilePreview {record} {schema}/>
{#each activeFields as field (field.name)} {#each activeFields as field (field.name)}
{#if activeContentTab === field.group} {#if activeContentTab === field.group}
<FormField <FormField
bind:data={record.data} bind:data={record.data}
bind:graph={graph} bind:graph
{field} {field}
{schema} {schema}
{record} {record}
{validationErrors} {validationErrors}
{isCreateMode} {isCreateMode}
/> />
{/if} {/if}
{/each} {/each}
{:else if activeContentTab === "_graph"} {:else if activeContentTab === "_graph"}
<Graph {graph} {record}/> <Graph {graph} {record} />
{:else if activeContentTab === "_info"} {:else if activeContentTab === "_info"}
<Info {record} {graph} {users} {schema}/> <Info {record} {graph} {users} {schema} />
{/if} {/if}
</div> </div>
</div> </div>
+27 -32
View File
@@ -45,25 +45,20 @@
</script> </script>
<div class="editor-field"> <div class="editor-field">
<FieldHeader {field} {id}/> <FieldHeader {field} {id} />
{#if field.info.name === "reference" && field.layout === "tags"} {#if field.info.name === "reference" && field.layout === "tags"}
<ReferenceTags <ReferenceTags bind:graph {id} {record} {field} {validationErrors} />
bind:graph
{id}
{record}
{field}
{validationErrors}
/>
{:else if field.info.name === "reference"} {:else if field.info.name === "reference"}
<Reference <Reference bind:graph {id} {record} {field} {validationErrors} />
bind:graph {:else if field.info.name === "file"}
{id} <!-- <File bind:graph {record} {field} {validationErrors} /> -->
<File
bind:value={data[field.name]}
{record} {record}
{id}
{field} {field}
{validationErrors} {validationErrors}
/> />
{:else if field.info.name === "file"}
<File bind:graph {record} {field} {validationErrors}/>
{:else if field.info.name === "text"} {:else if field.info.name === "text"}
<Text <Text
bind:value={data[field.name]} bind:value={data[field.name]}
@@ -74,11 +69,11 @@
/> />
{:else if field.info.name === "slug"} {:else if field.info.name === "slug"}
<Slug <Slug
bind:value={data[field.name]} bind:value={data[field.name]}
{field} {field}
{id} {id}
{validationErrors} {validationErrors}
{isCreateMode} {isCreateMode}
/> />
{:else if field.info.name === "textarea"} {:else if field.info.name === "textarea"}
<Textarea <Textarea
@@ -90,23 +85,23 @@
/> />
{:else if field.info.name === "rich"} {:else if field.info.name === "rich"}
<RichEditor <RichEditor
bind:value={data[field.name]} bind:value={data[field.name]}
{schema} {schema}
{field} {field}
{validationErrors} {validationErrors}
{isCreateMode} {isCreateMode}
bind:graph bind:graph
{record} {record}
/> />
{:else if field.info.name === "markdown"} {:else if field.info.name === "markdown"}
<Markdown <Markdown
bind:value={data[field.name]} bind:value={data[field.name]}
{schema} {schema}
{field} {field}
{validationErrors} {validationErrors}
{isCreateMode} {isCreateMode}
bind:graph bind:graph
{record} {record}
/> />
{:else} {:else}
<svelte:component <svelte:component
+63 -80
View File
@@ -1,11 +1,11 @@
<script> <script>
import {friendlyDate} from "../../helpers"; import { friendlyDate } from "../../helpers";
import Avatar from "../account/Avatar.svelte"; import Avatar from "../account/Avatar.svelte";
import {usernameById} from "../account/users"; import { usernameById } from "../account/users";
import {isEqual} from "lodash"; import { isEqual } from "lodash";
import Icon from "../common/Icon.svelte"; import Icon from "../common/Icon.svelte";
import RevisionCell from "./revisions/RevisionCell.svelte"; import RevisionCell from "./revisions/RevisionCell.svelte";
import {getContext} from "svelte"; import { getContext } from "svelte";
import RevisionEdgeRow from "./revisions/RevisionEdgeRow.svelte"; import RevisionEdgeRow from "./revisions/RevisionEdgeRow.svelte";
const channel = getContext("channel"); const channel = getContext("channel");
@@ -30,27 +30,27 @@
}); });
function getEdgesByField(fieldsWithDiff, revision) { function getEdgesByField(fieldsWithDiff, revision) {
edgeFieldsDiff = graph.edges
edgeFieldsDiff = graph.edges.filter((e) => e.depth === 1).reduce((c, e) => { .filter((e) => e.depth === 1)
if (!c[e.field]) { .reduce((c, e) => {
c[e.field] = { if (!c[e.field]) {
record: [], c[e.field] = {
revision: [], record: [],
revision: [],
};
} }
} c[e.field]["record"].push(e);
c[e.field]["record"].push(e) return c;
return c; }, {});
}, {});
edgeFieldsDiff = revision._edges.reduce((c, e) => { edgeFieldsDiff = revision._edges.reduce((c, e) => {
if (!c[e.field]) { if (!c[e.field]) {
c[e.field] = { c[e.field] = {
record: [], record: [],
revision: [], revision: [],
} };
} }
c[e.field]["revision"].push(e) c[e.field]["revision"].push(e);
return c; return c;
}, edgeFieldsDiff); }, edgeFieldsDiff);
} }
@@ -62,7 +62,7 @@
fieldsWithDiff = schema.fields.filter((f) => { fieldsWithDiff = schema.fields.filter((f) => {
return !isEqual(selectedRevision.data[f.name], record.data[f.name]); return !isEqual(selectedRevision.data[f.name], record.data[f.name]);
}); });
getEdgesByField(fieldsWithDiff, revision) getEdgesByField(fieldsWithDiff, revision);
revisionSection.scrollIntoView(); revisionSection.scrollIntoView();
} }
@@ -71,7 +71,7 @@
rollbackError = ""; rollbackError = "";
axios axios
.post( .post(
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision._sys.version}` `${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision.version}`,
) )
.then((response) => { .then((response) => {
window.location.reload(); window.location.reload();
@@ -84,7 +84,7 @@
} }
</script> </script>
<div class="lx-card "> <div class="lx-card">
<div class="row"> <div class="row">
<div class="col-8"> <div class="col-8">
<div> <div>
@@ -93,29 +93,27 @@
</div> </div>
<div> <div>
<span class="label text-end text-muted">current version </span> <span class="label text-end text-muted">current version </span>
{record._sys.version} {record.version}
</div> </div>
<div> <div>
<span class="label text-end text-muted"> created </span> <span class="label text-end text-muted"> created </span>
<Avatar <Avatar
name={usernameById(users, record._sys.createdBy)} name={usernameById(users, record.createdBy)}
side={24} side={24}
/> />
{friendlyDate(record._sys.createdAt)} {friendlyDate(record.createdAt)}
</div> </div>
<div> <div>
<span class="label text-end text-muted">updated </span> <span class="label text-end text-muted">updated </span>
<Avatar <Avatar
name={usernameById(users, record._sys.updatedBy)} name={usernameById(users, record.updatedBy)}
side={24} side={24}
/> />
{friendlyDate(record._sys.updatedAt)} {friendlyDate(record.updatedAt)}
</div> </div>
</div> </div>
<div class="col-4"> <div class="col-4">
<span class="label d-block text-muted " <span class="label d-block text-muted">Rules for this schema </span>
>Rules for this schema
</span>
<small> <small>
Each record maintains the last {schema.revisions} Each record maintains the last {schema.revisions}
versions versions
@@ -125,33 +123,31 @@
</div> </div>
<div class="revisions"> <div class="revisions">
{#if schema.revisions > 0} {#if schema.revisions > 0}
<div class="header-small mb-3">Revisions</div> <div class="header-small mb-3">Revisions</div>
{#each revisions as revision} {#each revisions as revision}
{#if revision._sys.version !== record._sys.version} {#if revision.version !== record.version}
<div <div
class="revision" class="revision"
class:active={revision._sys.version === class:active={revision.version ===
selectedRevision?._sys.version} selectedRevision?.version}
> >
<div class="version"> <div class="version">
<span>version {revision._sys.version}</span> <span>version {revision.version}</span>
<Avatar <Avatar
name={usernameById(users, revision._sys.updatedBy)} name={usernameById(users, revision.updatedBy)}
side={24} side={24}
/> />
{friendlyDate(revision._sys.updatedAt)} {friendlyDate(revision.updatedAt)}
</div> </div>
<div class="col-3 text-center"> <div class="col-3 text-center">
<button <button
disabled={revision._sys.version === disabled={revision.version ===
selectedRevision?._sys.version} selectedRevision?.version}
class="button" class="button"
on:click={(e) => compare(e, revision)} on:click={(e) => compare(e, revision)}
>Compare >Compare
</button </button>
>
</div> </div>
</div> </div>
{/if} {/if}
@@ -169,15 +165,13 @@
<p class="text-center fw-bold mb-3 mt-5"> <p class="text-center fw-bold mb-3 mt-5">
If you choose to rollback to this revision If you choose to rollback to this revision
</p> </p>
<button <button on:click={rollback} class="button">
on:click={rollback} Rollback to version {selectedRevision.version}
class="button"
>
Rollback to version {selectedRevision._sys.version}
</button> </button>
{#if rollbackError} {#if rollbackError}
<span class="d-block text-danger mt-3">{rollbackError}</span> <span class="d-block text-danger mt-3">{rollbackError}</span
>
{/if} {/if}
<div class="mt-3"> <div class="mt-3">
{#each fieldsWithDiff as field} {#each fieldsWithDiff as field}
@@ -188,31 +182,28 @@
<!-- <div class="d-block" style="width:200px;"> <!-- <div class="d-block" style="width:200px;">
{field.label} {field.label}
</div> --> </div> -->
<div <div class="revision-field" style="overflow:hidden">
class="revision-field"
style="overflow:hidden"
>
<div class="compare-left"> <div class="compare-left">
<RevisionCell <RevisionCell
{field} {field}
side={record.data[field.name]} side={record.data[field.name]}
colorClass="text-danger" colorClass="text-danger"
/> />
</div> </div>
<div class="compare-center"> <div class="compare-center">
<span class="me-1">{field.label}</span> <span class="me-1">{field.label}</span>
<Icon <Icon
icon="angle-right" icon="angle-right"
width="12" width="12"
height="12" height="12"
/> />
</div> </div>
<div class="compare-right"> <div class="compare-right">
<RevisionCell <RevisionCell
edges={selectedRevision._edges} edges={selectedRevision._edges}
{field} {field}
side={selectedRevision.data[field.name]} side={selectedRevision.data[field.name]}
colorClass="text-success" colorClass="text-success"
/> />
</div> </div>
</div> </div>
@@ -226,22 +217,16 @@
{/if} {/if}
<div class="mt-3"> <div class="mt-3">
<p class="text-center fw-bold mb-3 mt-5"> <p class="text-center fw-bold mb-3 mt-5">Record References</p>
Record References
</p>
{#each Object.entries(edgeFieldsDiff) as [field, edges]} {#each Object.entries(edgeFieldsDiff) as [field, edges]}
<div <div class="revision-references" style="overflow:hidden">
class="revision-references"
style="overflow:hidden"
>
<div class="reference-field"> <div class="reference-field">
{field}: {field}:
</div> </div>
<div class="reference-compare"> <div class="reference-compare">
<p class="">Record</p> <p class="">Record</p>
{#each edges.record as edge} {#each edges.record as edge}
<RevisionEdgeRow {edge}/> <RevisionEdgeRow {edge} />
{:else} {:else}
<p>No references</p> <p>No references</p>
{/each} {/each}
@@ -249,7 +234,7 @@
<div class="reference-compare"> <div class="reference-compare">
<p class="text-success">Revision</p> <p class="text-success">Revision</p>
{#each edges.revision as edge} {#each edges.revision as edge}
<RevisionEdgeRow {edge}/> <RevisionEdgeRow {edge} />
{:else} {:else}
<p>No references</p> <p>No references</p>
{/each} {/each}
@@ -258,7 +243,5 @@
{/each} {/each}
</div> </div>
</div> </div>
{/if} {/if}
</div> </div>
+42 -50
View File
@@ -1,7 +1,12 @@
<script> <script>
import {afterUpdate, createEventDispatcher, getContext, onMount} from "svelte"; import {
afterUpdate,
createEventDispatcher,
getContext,
onMount,
} from "svelte";
import {isEqual} from "lodash"; import { isEqual } from "lodash";
import FormField from "./FormField.svelte"; import FormField from "./FormField.svelte";
import FilePreview from "./FilePreview.svelte"; import FilePreview from "./FilePreview.svelte";
import ContentTabs from "./header/ContentTabs.svelte"; import ContentTabs from "./header/ContentTabs.svelte";
@@ -16,7 +21,7 @@
export let record; export let record;
export let graph = { export let graph = {
records: [], records: [],
edges: [] edges: [],
}; };
export let isCreateMode; export let isCreateMode;
let originalContent; let originalContent;
@@ -25,13 +30,11 @@
$: validationErrors = null; $: validationErrors = null;
$: errorMessage = validationErrors $: errorMessage = validationErrors
? `Record submission failed. ${ ? `Record submission failed. ${
Object.entries(validationErrors).length Object.entries(validationErrors).length
} error(s)` } error(s)`
: null; : null;
let activeFields = schema.fields.filter( let activeFields = schema.fields.filter((f) => f.name !== "id");
(f) => f.name !== "id"
);
let tabname = "_default"; let tabname = "_default";
let fieldToTabs = schema.fields.reduce((c, f) => { let fieldToTabs = schema.fields.reduce((c, f) => {
@@ -53,8 +56,6 @@
data: JSON.parse(JSON.stringify(record.data)), data: JSON.parse(JSON.stringify(record.data)),
schema: record.schema, schema: record.schema,
status: record.status, status: record.status,
_sys: JSON.parse(JSON.stringify(record._sys)),
_file: JSON.parse(JSON.stringify(record._file)),
edges: JSON.parse(JSON.stringify(graph.edges)), edges: JSON.parse(JSON.stringify(graph.edges)),
}; };
} }
@@ -87,8 +88,6 @@
data: record.data, data: record.data,
schema: record.schema, schema: record.schema,
status: record.status, status: record.status,
_sys: record._sys,
_file: record._file,
edges: graph.edges, edges: graph.edges,
}); });
} }
@@ -114,8 +113,10 @@
return; return;
} }
// remove trashed edges // 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 axios
.post(channel.lucentUrl + "/records", { .post(channel.lucentUrl + "/records", {
@@ -150,64 +151,55 @@
} }
</script> </script>
<svelte:window on:beforeunload={beforeUnload}/> <svelte:window on:beforeunload={beforeUnload} />
<div class="inline-edit record-edit"> <div class="inline-edit record-edit">
<div class="tools-header"> <div class="tools-header">
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab/> <EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab />
{#if isCreateMode} {#if isCreateMode}
<button <button class="button primary btn-spinner" on:click={save}>
class="button primary btn-spinner" <span
on:click={save} class="spinner-border spinner-border-sm"
> role="status"
<span aria-hidden="true"
class="spinner-border spinner-border-sm" />
role="status"
aria-hidden="true"
/>
Create Create
</button> </button>
{:else if hasUnsavedData} {:else if hasUnsavedData}
<button <button
type="button" type="button"
class="button primary ms-2 btn btn-primary btn-spinner" class="button primary ms-2 btn btn-primary btn-spinner"
on:click={save} on:click={save}
> >
<span <span
class="spinner-border spinner-border-sm" class="spinner-border spinner-border-sm"
role="status" role="status"
aria-hidden="true" aria-hidden="true"
/> />
Save Save
</button> </button>
{/if} {/if}
</div> </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;"> <div class=" mt-4" style="margin-bottom:150px;position:relative;">
<ContentTabs <ContentTabs {schema} {isCreateMode} bind:active={activeContentTab} />
{schema} <FilePreview {record} {schema} />
{isCreateMode}
bind:active={activeContentTab}
/>
<FilePreview {record} {schema}/>
<!-- <fieldset disabled="disabled"> --> <!-- <fieldset disabled="disabled"> -->
{#each activeFields as field (field.name)} {#each activeFields as field (field.name)}
{#if activeContentTab === field.group} {#if activeContentTab === field.group}
<FormField <FormField
bind:data={record.data} bind:data={record.data}
bind:graph={graph} bind:graph
{field} {field}
{schema} {schema}
{record} {record}
{validationErrors} {validationErrors}
{isCreateMode} {isCreateMode}
/> />
{/if} {/if}
{/each} {/each}
<!-- </fieldset> --> <!-- </fieldset> -->
</div> </div>
</div> </div>
+21 -21
View File
@@ -1,33 +1,33 @@
import Mustache from "mustache"; import Mustache from "mustache";
import {stripHtml} from "../../helpers"; import { stripHtml } from "../../helpers";
export function previewTitle(schemas, record, graph) { export function previewTitle(schemas, record, graph) {
let schema = schemas.find((aSchema) => aSchema.name === record?.schema); let schema = schemas.find((aSchema) => aSchema.name === record?.schema);
if (!schema?.cardTitle) { if (!schema?.cardTitle) {
return noTemplate(schema, record); return noTemplate(schema, record);
} }
let recordData = record.data; let recordData = record.data;
let render = Mustache.render(schema.cardTitle, recordData); let render = Mustache.render(schema.cardTitle, recordData);
if (!render || render === "") { if (!render || render === "") {
return noTemplate(schema, record); return noTemplate(schema, record);
} }
return stripHtml(render.slice(0, 300)); return stripHtml(render.slice(0, 300));
} }
function noTemplate(schema, record) { function noTemplate(schema, record) {
if (schema?.type === "files") { if (schema?.type === "files") {
return record._file.path; return file.path;
} }
let title = stripHtml( let title = stripHtml(
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name] record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name],
).slice(0, 300); ).slice(0, 300);
if(title.trim() === ""){ if (title.trim() === "") {
return "~Untitled~"; return "~Untitled~";
} }
return title; return title;
} }
+24 -31
View File
@@ -1,10 +1,7 @@
<script> <script>
import { sortByField } from "../../edges/sortEdges"; import { array_move } from "../../edges/sortEdges";
import Sortable from "../../libs/Sortable.svelte"; import Sortable from "../../libs/Sortable.svelte";
import PreviewFile from "../previews/PreviewFile.svelte"; import PreviewFile from "../previews/PreviewFile.svelte";
import Dropdown from "../../common/Dropdown.svelte";
import Dialog from "../../dialog/Dialog.svelte";
import { insertEdges } from "./reference.js";
import { getContext } from "svelte"; import { getContext } from "svelte";
import FileDialog from "../../dialog/FileDialog.svelte"; import FileDialog from "../../dialog/FileDialog.svelte";
import Uploader from "../../files/Uploader.svelte"; import Uploader from "../../files/Uploader.svelte";
@@ -12,36 +9,28 @@
const channel = getContext("channel"); const channel = getContext("channel");
export let field; export let field;
export let record; export let record;
export let graph; export let value = [];
let browseModal; let browseModal;
function removeReference(e) { function removeFile(e) {
e.preventDefault(); e.preventDefault();
graph.edges = graph.edges.filter( value = value.filter((f) => !(f.id === e.detail));
(edge) => !(edge.target === e.detail && edge.field === field.name),
);
} }
async function reorder(e) { async function reorder(e) {
graph.edges = await sortByField( value = await array_move(value, e.detail.source, e.detail.target);
e.detail.source,
e.detail.target,
graph.edges,
field.name,
references,
);
} }
function insert(e) { function insertFiles(e) {
e.preventDefault(); e.preventDefault();
browseModal.close(); browseModal.close();
graph = insertEdges( value = [...(value ?? []), ...(e.detail ?? [])];
graph, }
record,
e.detail.records, function replaceFiles(e) {
field.name, e.preventDefault();
e.detail.action, browseModal.close();
); value = e.detail ?? [];
} }
function uploadComplete(e) { function uploadComplete(e) {
@@ -61,18 +50,22 @@
<Uploader recordId={record.id} on:uploadComplete={uploadComplete} /> <Uploader recordId={record.id} on:uploadComplete={uploadComplete} />
</div> </div>
</div> </div>
<!-- {#if references.length > 0} {#if value.length > 0}
<Sortable sortableClass="mt-3" on:update={reorder}> <Sortable sortableClass="mt-3" on:update={reorder}>
{#each references as reference (reference.id)} {#each value ?? [] as aFile (aFile.id)}
<!--This div helps the sorting thing--> <!--This div helps the sorting thing-->
<!-- <div> <div>
<PreviewFile <PreviewFile
record={reference} file={aFile}
hasDelete={true} hasDelete={true}
on:remove={removeReference} on:remove_file={removeFile}
></PreviewFile> ></PreviewFile>
</div> </div>
{/each} {/each}
</Sortable> </Sortable>
{/if} --> {/if}
<FileDialog bind:this={browseModal} on:insert={insert}></FileDialog> <FileDialog
bind:this={browseModal}
on:insert_files={insertFiles}
on:replace_files={replaceFiles}
></FileDialog>
@@ -1,91 +1,80 @@
<script> <script>
import Icon from "../../common/Icon.svelte"; import Icon from "../../common/Icon.svelte";
import {createEventDispatcher, getContext} from "svelte"; import { createEventDispatcher, getContext } from "svelte";
import Preview from "../../files/Preview.svelte"; import Preview from "../../files/Preview.svelte";
import {previewTitle} from "./../Preview"; import { previewTitle } from "./../Preview";
import {fileurl, htmlurl} from "../../files/imageserver.js" import { fileurl, htmlurl } from "../../files/imageserver.js";
import Status from "./../Status.svelte"; import Status from "./../Status.svelte";
import Dropdown from "../../common/Dropdown.svelte"; import Dropdown from "../../common/Dropdown.svelte";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const channel = getContext("channel"); const channel = getContext("channel");
export let record; export let file;
export let hasDelete = false; export let hasDelete = false;
export let hasInsert = false; export let hasInsert = false;
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
let cardTitle = previewTitle(channel.schemas, record);
let imagePresets = Object.keys(channel.imageFilters); let imagePresets = Object.keys(channel.imageFilters);
function remove(e) { function remove(e) {
e.preventDefault(); e.preventDefault();
dispatch("remove", record.id); dispatch("remove_file", file.id);
} }
function insert(e, preset) { function insert(e, preset) {
e.preventDefault(); e.preventDefault();
let html = htmlurl(channel, record, preset) // let html = htmlurl(channel, record, preset);
let url = !preset ? `/${record._file.path}` : `/templates/${preset}/${record._file.path}`; // let url = !preset
dispatch("editor-insert", { // ? `/${record._file.path}`
html: html, // : `/templates/${preset}/${record._file.path}`;
url: channel.filesUrl + url, // dispatch("editor-insert", {
originalUrl: channel.filesUrl + "/" + record._file.path, // html: html,
record: record // url: channel.filesUrl + url,
}); // originalUrl: channel.filesUrl + "/" + record._file.path,
// record: record,
// });
} }
</script> </script>
<div class="preview-file"> <div class="preview-file">
<div style="display: flex;align-items: center;gap: 10px;"> <div style="display: flex;align-items: center;gap: 10px;">
<div class="image"> <div class="image">
<Preview {record} size="small"/> <Preview {file} size="small" />
</div> </div>
<div class="title"> <div class="title">
<div> <div>
<a {file.filename}
class="record-title"
href="{channel.lucentUrl}/records/{record.id}"
>
{cardTitle}
</a>
<small class="d-block">
from {schema.label}
{#if record.status === "draft"}
<Status status={record.status}/>
{/if}
</small>
</div> </div>
</div> </div>
</div> </div>
<div style="display: flex;gap:4px; align-items: center; margin-right: 10px;"> <div
style="display: flex;gap:4px; align-items: center; margin-right: 10px;"
>
{#if hasInsert} {#if hasInsert}
<div class="reference-action"> <div class="reference-action">
<Dropdown> <Dropdown>
<div slot="button"> <div slot="button">
<Icon icon="photo-film"/> <Icon icon="photo-film" />
</div> </div>
<button class="dropdown-item button" on:click={e => insert(e,null)}>original</button> <button
class="dropdown-item button"
on:click={(e) => insert(e, null)}>original</button
>
{#each imagePresets as preset} {#each imagePresets as preset}
<button class="dropdown-item button" on:click={e => insert(e,preset)}>{preset}</button> <button
class="dropdown-item button"
on:click={(e) => insert(e, preset)}>{preset}</button
>
{/each} {/each}
</Dropdown> </Dropdown>
</div> </div>
{/if} {/if}
{#if hasDelete} {#if hasDelete}
<div class="reference-action"> <div class="reference-action">
<button <button class="button" on:click={remove}>
class="button" <Icon icon="trash-can" />
on:click={remove}
>
<Icon icon="trash-can"/>
</button> </button>
</div> </div>
{/if} {/if}
</div> </div>
</div> </div>
@@ -12,22 +12,9 @@
<div class="{colorClass} field-content"> <div class="{colorClass} field-content">
<div class="d-flex align-items-center text-center flex-wrap"> <div class="d-flex align-items-center text-center flex-wrap">
{#each edges[field.name] as edgeRecord} {#each edges[field.name] as edgeRecord}
{#if edgeRecord._file?.path} <div class="ms-2">
<div <PreviewCardSmall record={edgeRecord} />
class="ms-2 " </div>
style="max-width:64px;overflow:hidden;white-space: nowrap;text-overflow: ellipsis;"
>
<Preview
record={edgeRecord}
size="small"
showFilename={true}
/>
</div>
{:else}
<div class="ms-2 ">
<PreviewCardSmall record={edgeRecord}/>
</div>
{/if}
{/each} {/each}
</div> </div>
</div> </div>
@@ -43,7 +30,6 @@
<!-- {/if} --> <!-- {/if} -->
<style> <style>
.field-content { .field-content {
max-height: 200px; max-height: 200px;
overflow-y: scroll; overflow-y: scroll;
-1
View File
@@ -4,7 +4,6 @@ namespace Lucent\Channel;
use Lucent\Channel\Data\UserCommand; use Lucent\Channel\Data\UserCommand;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use Lucent\Schema\FilesSchema;
use Lucent\Schema\Schema; use Lucent\Schema\Schema;
final class Channel final class Channel
-43
View File
@@ -1,43 +0,0 @@
<?php
namespace Lucent\Commands;
use Illuminate\Console\Command;
use Lucent\Primitive\Collection;
use Lucent\Schema\FilesSchema;
class GenerateFileSchema extends Command
{
protected $signature = 'lucent:generate:file {name}';
protected $description = 'Generate a lucent file schema';
public function handle()
{
$name = $this->argument('name');
$schema = new FilesSchema(
name: $name,
label: $name,
fields: Collection::make(),
disk: "lucent",
path: $name,
groups: []
);
$json = json_encode($schema, JSON_PRETTY_PRINT);
$configDir = base_path(config('lucent.schemas_path'));
$schemaPath = $configDir . "/" . $name . '.json';
if (file_exists($schemaPath)) {
$this->error("The schema file already exists.");
return 0;
}
file_put_contents($schemaPath, $json);
$this->info("The schema file has been created.");
}
}
+8 -26
View File
@@ -7,6 +7,7 @@ use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use Lucent\Channel\ChannelService; use Lucent\Channel\ChannelService;
use Lucent\File\FileRepo;
use Lucent\File\FileService; use Lucent\File\FileService;
use Lucent\Query\Query; use Lucent\Query\Query;
use Lucent\Schema\FilesSchema; use Lucent\Schema\FilesSchema;
@@ -26,35 +27,16 @@ class RebuildThumbnails extends Command
parent::__construct(); parent::__construct();
} }
public function handle(ChannelService $channelService): int public function handle(): void
{ {
$channelService->channel->schemas $this->info("Rebuilding thumbnails ");
->filter( $files = FileRepo::query()->get();
fn(Schema $schema) => get_class($schema) === FilesSchema::class, $disk = $this->fileService->loadDisk();
) foreach ($files as $file) {
->map([$this, "rebuildThumbnails"]);
return 0;
}
public function rebuildThumbnails(FilesSchema $schema): void
{
$this->info("Rebuilding thumbnails for " . $schema->name);
$records = $this->query
->limit(0)
->filter(["schema" => $schema->name])
->run()->records;
$disk = $this->fileService->loadDisk($schema->disk);
foreach ($records as $record) {
try { try {
$this->fileService->createTemplates( $this->fileService->createTemplates($disk, $file->path);
$disk,
$record->_file->path,
);
} catch (Exception $e) { } catch (Exception $e) {
echo "File " . echo "File " . $file->filename . " could not be rebuilt \n";
$record->_file->originalName .
" could not be rebuilt \n";
} }
} }
} }
+16 -9
View File
@@ -71,19 +71,23 @@ class SetupDatabase extends Command
) { ) {
$table->uuid("id")->primary(); $table->uuid("id")->primary();
$table->string("schema"); $table->string("schema");
$table->integer("version");
$table->string("status"); $table->string("status");
$table->jsonb("data"); $table->jsonb("data");
$table->jsonb("_sys"); $table->timestampTz("createdAt");
$table->timestampTz("updatedAt");
$table->string("createdBy");
$table->string("updatedBy");
$table->text("search")->default(""); $table->text("search")->default("");
// $table->index(["schema", "_sys->updatedAt", "status"]); $table->index(["schema", "updatedAt", "status"]);
$table->index("search"); $table->index("search");
}); });
DB::statement( // DB::statement(
"CREATE INDEX ON " . // "CREATE INDEX ON " .
$this->prefix . // $this->prefix .
'records (schema, ((_sys->>\'updatedAt\')), status)', // 'records (schema, ((_sys->>\'updatedAt\')), status)',
); // );
} }
if (!$schema->hasTable($this->prefix . "edges")) { if (!$schema->hasTable($this->prefix . "edges")) {
@@ -140,8 +144,11 @@ class SetupDatabase extends Command
$table->uuid("recordId"); $table->uuid("recordId");
$table->string("schema"); $table->string("schema");
$table->jsonb("data"); $table->jsonb("data");
$table->jsonb("_sys"); $table->integer("version");
$table->jsonb("_file"); $table->timestampTz("createdAt");
$table->timestampTz("updatedAt");
$table->string("createdBy");
$table->string("updatedBy");
$table->jsonb("_edges"); $table->jsonb("_edges");
}); });
} }
-27
View File
@@ -1,27 +0,0 @@
<?php
namespace Lucent\Commands;
use Illuminate\Console\Command;
use Lucent\Database\Database;
class UpgradeFiles122 extends Command
{
protected $signature = 'lucent:upgrade:files_1_2_2 {schema} {disk}';
protected $description = 'Upgrade to the new filesystem';
public function handle()
{
$schema = $this->argument('schema');
$disk = $this->argument('disk');
$db = Database::make();
$records = $db->table("lucent_records")->where("schema", $schema)->get();
foreach ($records as $record) {
$array = json_decode($record->_file, true);
$array["disk"] = $disk;
$db->table("lucent_records")->where("id", $record->id)->update(["_file" => json_encode($array)]);
}
}
}
+6
View File
@@ -2,6 +2,7 @@
namespace Lucent\File; namespace Lucent\File;
use Illuminate\Database\Query\Builder;
use Lucent\Data\File as DataFile; use Lucent\Data\File as DataFile;
use Lucent\Database\Database; use Lucent\Database\Database;
use Lucent\Data\File; use Lucent\Data\File;
@@ -15,6 +16,11 @@ class FileRepo
Database::make()->table("lucent_files")->insert($file->toDB()); Database::make()->table("lucent_files")->insert($file->toDB());
} }
public static function query(): Builder
{
return Database::make()->table("lucent_files");
}
/** /**
* @return File[] * @return File[]
*/ */
+2 -25
View File
@@ -14,7 +14,6 @@ use Lucent\Id\Id;
use Lucent\LucentException; use Lucent\LucentException;
use Lucent\Data\File as DataFile; use Lucent\Data\File as DataFile;
use Lucent\Record\QueryRecord; use Lucent\Record\QueryRecord;
use Lucent\Schema\FilesSchema;
use Spatie\ImageOptimizer\OptimizerChainFactory; use Spatie\ImageOptimizer\OptimizerChainFactory;
class FileService class FileService
@@ -25,15 +24,8 @@ class FileService
public Logger $logger, public Logger $logger,
) {} ) {}
public function getPath(QueryRecord $file): string
{
return $this->channelService->channel->filesUrl .
"/" .
$file->_file->path;
}
public function createFromUrl( public function createFromUrl(
FilesSchema $schema, string $recordId,
string $url, string $url,
): FileUploadResult { ): FileUploadResult {
$pathinfo = pathinfo($url); $pathinfo = pathinfo($url);
@@ -44,7 +36,7 @@ class FileService
$file = "/tmp/" . $pathinfo["basename"]; $file = "/tmp/" . $pathinfo["basename"];
file_put_contents($file, $contents); file_put_contents($file, $contents);
$uploadedFile = new UploadedFile($file, $pathinfo["basename"]); $uploadedFile = new UploadedFile($file, $pathinfo["basename"]);
return $this->upload($schema, $uploadedFile); return $this->upload($recordId, $uploadedFile);
} }
public function upload(string $recordId, UploadedFile $file): DataFile public function upload(string $recordId, UploadedFile $file): DataFile
@@ -130,21 +122,6 @@ class FileService
return Storage::disk(config("lucent.disk")); return Storage::disk(config("lucent.disk"));
} }
private function checkDuplicate(
string $schemaName,
string $checksum,
int $filesize,
): string {
$record = Database::make()
->table("lucent_records")
->where("schema", $schemaName)
->where("_file->checksum", $checksum)
->where("_file->size", $filesize)
->first();
return $record->id ?? "";
}
public function createTemplates(Filesystem $disk, string $path): void public function createTemplates(Filesystem $disk, string $path): void
{ {
$originalImage = $this->imageManager->make($disk->get($path)); $originalImage = $this->imageManager->make($disk->get($path));
+1 -2
View File
@@ -58,7 +58,6 @@ class RecordController extends Controller
$users = $this->accountService->all(); $users = $this->accountService->all();
$schema = $this->channelService->getSchema($schemaName)->get(); $schema = $this->channelService->getSchema($schemaName)->get();
$urlParams = $request->all(); $urlParams = $request->all();
$sort = data_get($urlParams, "sort") ?? $schema->sortBy; $sort = data_get($urlParams, "sort") ?? $schema->sortBy;
$filter = data_get($urlParams, "filter") ?? []; $filter = data_get($urlParams, "filter") ?? [];
@@ -86,8 +85,8 @@ class RecordController extends Controller
->childrenFields($schema?->visible ?? []) ->childrenFields($schema?->visible ?? [])
->childrenDepth(1) ->childrenDepth(1)
->parentsDepth(0) ->parentsDepth(0)
->runWithCount();
->runWithCount();
$records = $graph->getRootRecords()->toArray(); $records = $graph->getRootRecords()->toArray();
$data = [ $data = [
-2
View File
@@ -89,8 +89,6 @@ class LucentServiceProvider extends ServiceProvider
RemoveOrphanEdges::class, RemoveOrphanEdges::class,
SetupDatabase::class, SetupDatabase::class,
GenerateCollectionSchema::class, GenerateCollectionSchema::class,
GenerateFileSchema::class,
UpgradeFiles122::class,
]); ]);
} }
+3 -1
View File
@@ -78,6 +78,7 @@ final class Query
->table("lucent_records") ->table("lucent_records")
->whereIn("id", $edgesIds) ->whereIn("id", $edgesIds)
->whereIn("status", $this->options->status) ->whereIn("status", $this->options->status)
->get() ->get()
->toArray(); ->toArray();
} }
@@ -295,7 +296,8 @@ final class Query
public function orderByQuery(Builder $query): Builder public function orderByQuery(Builder $query): Builder
{ {
foreach ($this->options->sort as $item) { foreach ($this->options->sort as $item) {
$field = str_replace(".", "->", ltrim($item, "-")); $field = str_replace("_sys.", "", ltrim($item, "-"));
$field = str_replace(".", "->", $field);
$dir = str_starts_with($item, "-") ? "desc" : "asc"; $dir = str_starts_with($item, "-") ? "desc" : "asc";
if ($field) { if ($field) {
$query->orderBy($field, $dir); $query->orderBy($field, $dir);
+18 -14
View File
@@ -2,24 +2,25 @@
namespace Lucent\Record; namespace Lucent\Record;
use Carbon\Carbon;
use Lucent\LucentException; use Lucent\LucentException;
class QueryRecord class QueryRecord
{ {
function __construct( function __construct(
public string $id, public string $id,
public string $schema, public string $schema,
public Status $status, public Status $status,
public System $_sys,
public RecordData $data, public RecordData $data,
public bool $isRoot, public Carbon $createdAt,
public ?FileData $_file = null, public Carbon $updatedAt,
public array $_children = [], public string $createdBy,
public array $_parents = [], public string $updatedBy,
) public int $version,
{ public bool $isRoot,
} public array $_children = [],
public array $_parents = [],
) {}
public static function fromRecord(Record $record): QueryRecord public static function fromRecord(Record $record): QueryRecord
{ {
@@ -27,10 +28,13 @@ class QueryRecord
id: $record->id, id: $record->id,
schema: $record->schema, schema: $record->schema,
status: $record->status, status: $record->status,
_sys: $record->_sys,
data: $record->data, data: $record->data,
createdAt: $record->createdAt,
updatedAt: $record->updatedAt,
createdBy: $record->createdBy,
updatedBy: $record->updatedBy,
version: $record->version,
isRoot: false, isRoot: false,
_file: $record->_file,
); );
} }
} }
+41 -37
View File
@@ -2,36 +2,46 @@
namespace Lucent\Record; namespace Lucent\Record;
use Carbon\Carbon;
use JsonSerializable; use JsonSerializable;
use stdClass; use stdClass;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class Record implements JsonSerializable class Record implements JsonSerializable
{ {
function __construct( function __construct(
public string $id, public string $id,
public string $schema, public string $schema,
public Status $status, public Status $status,
public System $_sys, public Carbon $createdAt,
public Carbon $updatedAt,
public string $createdBy,
public string $updatedBy,
public int $version,
public RecordData $data, public RecordData $data,
public ?FileData $_file = null, ) {}
)
private function indexValues(array $arrObject)
{ {
} return trim(
Str::lower(
collect($arrObject)
private function indexValues(array $arrObject){ ->map(function ($value) {
if (is_array($value)) {
return trim(Str::lower(collect($arrObject) return $this->indexValues($value ?? []);
->map(function($value){ }
if(is_array($value)){ return str_replace(
return $this->indexValues($value ?? []); ["\r", "\n"],
} "",
return str_replace(array("\r", "\n"), '', strip_tags((string)$value)); strip_tags((string) $value),
}) );
->values()->join(" ")." ". $this->_file?->originalName ?? "")); })
->values()
->join(" ") .
" " .
"",
),
);
} }
public function toDB(): array public function toDB(): array
@@ -41,8 +51,11 @@ class Record implements JsonSerializable
"id" => $this->id, "id" => $this->id,
"status" => $this->status->value, "status" => $this->status->value,
"schema" => $this->schema, "schema" => $this->schema,
"_sys" => json_encode($this->_sys), "createdAt" => $this->createdAt->toDateTimeString(),
"_file" => json_encode($this->_file), "updatedAt" => $this->updatedAt->toDateTimeString(),
"createdBy" => $this->createdBy,
"updatedBy" => $this->updatedBy,
"version" => $this->version,
"data" => json_encode($this->data), "data" => json_encode($this->data),
"search" => $searchIndex, "search" => $searchIndex,
]; ];
@@ -50,30 +63,21 @@ class Record implements JsonSerializable
public static function fromDB(stdClass $data): Record public static function fromDB(stdClass $data): Record
{ {
$file = json_decode($data->_file, true);
if (!empty($file)) {
$file = FileData::fromArray($file);
} else {
$file = null;
}
return new Record( return new Record(
id: $data->id, id: $data->id,
schema: $data->schema, schema: $data->schema,
status: Status::from($data->status), status: Status::from($data->status),
_sys: System::fromArray(json_decode($data->_sys, true)), createdAt: Carbon::parse($data->createdAt),
updatedAt: Carbon::parse($data->updatedAt),
createdBy: $data->createdBy,
updatedBy: $data->updatedBy,
version: $data->version,
data: new RecordData(json_decode($data->data, true)), data: new RecordData(json_decode($data->data, true)),
_file: $file,
); );
} }
public function jsonSerialize(): static public function jsonSerialize(): static
{ {
return $this; return $this;
} }
} }
+23 -11
View File
@@ -2,6 +2,7 @@
namespace Lucent\Record; namespace Lucent\Record;
use Carbon\Carbon;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Lucent\Account\AuthService; use Lucent\Account\AuthService;
@@ -16,7 +17,7 @@ use Lucent\Record\InputData\EdgeInputData;
use Lucent\Record\InputData\RecordInputData; use Lucent\Record\InputData\RecordInputData;
use Lucent\Revision\RevisionService; use Lucent\Revision\RevisionService;
use Lucent\Schema\FieldInterface; use Lucent\Schema\FieldInterface;
use Lucent\Schema\Schema; use Lucent\Data\Schema;
use Lucent\Schema\Type; use Lucent\Schema\Type;
use Lucent\Schema\Validator\Validator; use Lucent\Schema\Validator\Validator;
use Lucent\Schema\Validator\ValidatorException; use Lucent\Schema\Validator\ValidatorException;
@@ -115,9 +116,12 @@ readonly class RecordService
id: $newRecordId, id: $newRecordId,
schema: $data->schemaName, schema: $data->schemaName,
status: $data->status, status: $data->status,
_sys: System::newRecord($this->authService->currentUserId()), version: 1,
createdBy: $this->authService->currentUserId(),
updatedBy: $this->authService->currentUserId(),
createdAt: Carbon::now(),
updatedAt: Carbon::now(),
data: $formattedData, data: $formattedData,
_file: !empty($file) ? FileData::fromArray(toArray($file)) : null,
); );
if ($data->status === Status::PUBLISHED) { if ($data->status === Status::PUBLISHED) {
@@ -173,9 +177,12 @@ readonly class RecordService
id: $record->id, id: $record->id,
schema: $record->schema, schema: $record->schema,
status: $status, status: $status,
_sys: $record->_sys->update($this->authService->currentUserId()), version: $record->version + 1,
createdBy: $record->createdBy,
updatedBy: $this->authService->currentUserId(),
createdAt: $record->createdAt,
updatedAt: Carbon::now(),
data: $record->data->merge($formattedData), data: $record->data->merge($formattedData),
_file: $record->_file,
); );
RecordRepo::update($newRecord); RecordRepo::update($newRecord);
@@ -205,12 +212,12 @@ readonly class RecordService
$record->schema, $record->schema,
new RecordData($data), new RecordData($data),
); );
if ($status === Status::PUBLISHED) { if ($status === Status::PUBLISHED) {
$errors = $this->recordValidator->check( $errors = $this->recordValidator->check(
$record->schema, $record->schema,
$formattedData, $formattedData,
); );
if ($errors->isNotEmpty()) { if ($errors->isNotEmpty()) {
$this->recordValidator->throwException($errors); $this->recordValidator->throwException($errors);
} }
@@ -220,9 +227,12 @@ readonly class RecordService
id: $record->id, id: $record->id,
schema: $record->schema, schema: $record->schema,
status: $status, status: $status,
_sys: $record->_sys->update($this->authService->currentUserId()), version: $record->version + 1,
createdBy: $record->createdBy,
updatedBy: $this->authService->currentUserId(),
createdAt: $record->createdAt,
updatedAt: Carbon::now(),
data: $record->data->merge($formattedData), data: $record->data->merge($formattedData),
_file: $record->_file,
); );
RecordRepo::update($newRecord); RecordRepo::update($newRecord);
@@ -278,7 +288,6 @@ readonly class RecordService
data: $record->data->toArray(), data: $record->data->toArray(),
status: Status::DRAFT, status: Status::DRAFT,
), ),
file: $record->_file,
edges: $newEdgesData, edges: $newEdgesData,
); );
} }
@@ -337,9 +346,12 @@ readonly class RecordService
id: Id::new(), id: Id::new(),
schema: $schema->name, schema: $schema->name,
status: Status::DRAFT, status: Status::DRAFT,
_sys: System::newRecord($this->authService->currentUserId()), version: 1,
createdBy: $this->authService->currentUserId(),
updatedBy: $this->authService->currentUserId(),
createdAt: Carbon::now(),
updatedAt: Carbon::now(),
data: $formattedData, data: $formattedData,
_file: null,
); );
} }
} }
-62
View File
@@ -1,62 +0,0 @@
<?php
namespace Lucent\Record;
use Carbon\Carbon;
use Lucent\Schema\Schema;
readonly class System
{
function __construct(
public int $version,
public string $createdBy,
public string $updatedBy,
public string $createdAt,
public string $updatedAt,
)
{
}
public static function fromArray(array $data): System
{
return new System(
version: data_get($data, "version"),
createdBy: data_get($data, "createdBy"),
updatedBy: data_get($data, "updatedBy"),
createdAt: data_get($data, "createdAt"),
updatedAt: data_get($data, "updatedAt"),
);
}
public static function newRecord(string $userId): System
{
$now = Carbon::now()->toJson();
return new System(
version: 1,
createdBy: $userId,
updatedBy: $userId,
createdAt: $now,
updatedAt: $now,
);
}
public function update(string $userId): System
{
$now = Carbon::now()->toJson();
return new System(
version: $this->version + 1,
createdBy: $this->createdBy,
updatedBy: $userId,
createdAt: $this->createdAt,
updatedAt: $now,
);
}
}
+17 -21
View File
@@ -2,51 +2,47 @@
namespace Lucent\Revision; namespace Lucent\Revision;
use Carbon\Carbon;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Lucent\Edge\Edge; use Lucent\Edge\Edge;
use Lucent\Record\FileData;
use Lucent\Record\Record; use Lucent\Record\Record;
use Lucent\Record\RecordData; use Lucent\Record\RecordData;
use Lucent\Record\System;
readonly class Revision readonly class Revision
{ {
/** /**
* @param string $id * @param string $id
* @param string $recordId * @param string $recordId
* @param string $schema * @param string $schema
* @param System $_sys
* @param RecordData $data * @param RecordData $data
* @param list<Edge> $_edges * @param list<Edge> $_edges
* @param FileData|null $_file
*/ */
function __construct( function __construct(
public string $id, public string $id,
public string $recordId, public string $recordId,
public string $schema, public string $schema,
public System $_sys, public Carbon $createdAt,
public Carbon $updatedAt,
public string $createdBy,
public string $updatedBy,
public int $version,
public RecordData $data, public RecordData $data,
public array $_edges, public array $_edges,
public ?FileData $_file = null, ) {}
)
{
}
public static function fromRecord(Record $record, array $edges): Revision public static function fromRecord(Record $record, array $edges): Revision
{ {
return new Revision( return new Revision(
id: (string)Str::uuid(), id: (string) Str::uuid(),
recordId: $record->id, recordId: $record->id,
schema: $record->schema, schema: $record->schema,
_sys: $record->_sys, createdAt: $record->createdAt,
updatedAt: $record->updatedAt,
createdBy: $record->createdBy,
updatedBy: $record->updatedBy,
version: $record->version,
data: $record->data, data: $record->data,
_edges: $edges, _edges: $edges,
_file: $record->_file
); );
} }
} }
+27 -27
View File
@@ -5,15 +5,12 @@ namespace Lucent\Revision;
use Lucent\Database\Database; use Lucent\Database\Database;
use Lucent\Edge\Edge; use Lucent\Edge\Edge;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use Lucent\Record\FileData;
use Lucent\Record\RecordData; use Lucent\Record\RecordData;
use Lucent\Record\System;
use PhpOption\Option; use PhpOption\Option;
use stdClass; use stdClass;
class RevisionRepo class RevisionRepo
{ {
private string $table = "lucent_revisions"; private string $table = "lucent_revisions";
public function create(Revision $revision): string public function create(Revision $revision): string
@@ -23,17 +20,17 @@ class RevisionRepo
return $revision->id; return $revision->id;
} }
/** /**
* @return Collection<Revision> * @return Collection<Revision>
**/ **/
public function getByRecordId(string $rid): Collection public function getByRecordId(string $rid): Collection
{ {
$revisions = Database::make()->table($this->table) $revisions = Database::make()
->table($this->table)
->where("recordId", $rid) ->where("recordId", $rid)
->get() ->get()
->map([$this, 'fromDB']) ->map([$this, "fromDB"])
->sortByDesc("_sys.version") ->sortByDesc("version")
->toArray(); ->toArray();
return new Collection($revisions); return new Collection($revisions);
@@ -41,29 +38,31 @@ class RevisionRepo
public function cleanupRecord(string $rid, int $numKeep): void public function cleanupRecord(string $rid, int $numKeep): void
{ {
$revisionIds = Database::make()->table($this->table) $revisionIds = Database::make()
->table($this->table)
->where("recordId", $rid) ->where("recordId", $rid)
->orderBy("_sys->version", "desc") ->orderBy("version", "desc")
->limit(100) ->limit(100)
->skip($numKeep) ->skip($numKeep)
->get() ->get()
->pluck("id"); ->pluck("id");
Database::make()->table($this->table) Database::make()
->table($this->table)
->whereIn("id", $revisionIds) ->whereIn("id", $revisionIds)
->delete(); ->delete();
} }
/** /**
* @return Option<Revision> * @return Option<Revision>
*/ */
public function getByRecordIdAndVersion(string $rid, int $version): Option public function getByRecordIdAndVersion(string $rid, int $version): Option
{ {
$res = Database::make()
$res = Database::make()->table($this->table) ->table($this->table)
->where("recordId", $rid) ->where("recordId", $rid)
->where('_sys->version', $version)->first(); ->where("version", $version)
->first();
if (empty($res)) { if (empty($res)) {
return none(); return none();
@@ -78,8 +77,11 @@ class RevisionRepo
"id" => $revision->id, "id" => $revision->id,
"recordId" => $revision->recordId, "recordId" => $revision->recordId,
"schema" => $revision->schema, "schema" => $revision->schema,
"_sys" => json_encode($revision->_sys), "createdAt" => $revision->createdAt->toDateTimeString(),
"_file" => json_encode($revision->_file), "updatedAt" => $revision->updatedAt->toDateTimeString(),
"createdBy" => $revision->createdBy,
"updatedBy" => $revision->updatedBy,
"version" => $revision->version,
"data" => json_encode($revision->data), "data" => json_encode($revision->data),
"_edges" => json_encode($revision->_edges), "_edges" => json_encode($revision->_edges),
]; ];
@@ -87,24 +89,22 @@ class RevisionRepo
public function fromDB(stdClass $data): Revision public function fromDB(stdClass $data): Revision
{ {
$file = json_decode($data->_file, true); $edges = array_map(
if (!empty($file)) { fn($e) => Edge::fromArray($e),
json_decode($data->_edges ?? "[]", true),
$file = FileData::fromArray($file); );
} else {
$file = null;
}
$edges = array_map(fn($e) => Edge::fromArray($e), json_decode($data->_edges ?? "[]", true));
return new Revision( return new Revision(
id: $data->id, id: $data->id,
recordId: $data->recordId, recordId: $data->recordId,
schema: $data->schema, schema: $data->schema,
_sys: System::fromArray(json_decode($data->_sys, true)), createdBy: $data->createdBy,
updatedBy: $data->updatedBy,
createdAt: $data->createdAt,
updatedAt: $data->updatedAt,
version: $data->version,
data: new RecordData(json_decode($data->data, true)), data: new RecordData(json_decode($data->data, true)),
_edges: $edges, _edges: $edges,
_file: $file
); );
} }
} }
+9 -4
View File
@@ -5,16 +5,21 @@ namespace Lucent\Schema;
final class Nullable final class Nullable
{ {
public function __construct( public function __construct(
public bool $nullable, public bool $nullable,
public mixed $value, public mixed $value,
public mixed $default, public mixed $default,
) ) {}
{
public static function make(
bool $nullable,
mixed $value,
mixed $default,
): Nullable {
return new self($nullable, $value, $default);
} }
public function value(): mixed public function value(): mixed
{ {
if (!empty($this->value)) { if (!empty($this->value)) {
return $this->value; return $this->value;
} }
+10
View File
@@ -12,6 +12,16 @@ class SchemaService
public function fromArray(array $schemaArr): Schema public function fromArray(array $schemaArr): Schema
{ {
$schemaArr["fields"] = [
[
"ui" => "text",
"name" => "name",
"label" => "Name",
"required" => true,
],
...$schemaArr["fields"],
];
return new Schema( return new Schema(
name: $schemaArr["name"], name: $schemaArr["name"],
label: $schemaArr["label"], label: $schemaArr["label"],
+3 -45
View File
@@ -8,11 +8,8 @@ readonly class System
public string $name, public string $name,
public string $label, public string $label,
public string $ui, public string $ui,
public bool $files, public bool $files,
) ) {}
{
}
public static function getById(string $id): System public static function getById(string $id): System
{ {
@@ -26,7 +23,7 @@ readonly class System
{ {
return [ return [
"_sys.status" => new System( "_sys.status" => new System(
name: "status", name: "_sys.status",
label: "Status", label: "Status",
ui: "status", ui: "status",
files: false, files: false,
@@ -55,45 +52,6 @@ readonly class System
ui: "datetime", ui: "datetime",
files: false, files: false,
), ),
"_file.path" => new System(
name: "_file.path",
label: "Filename",
ui: "text",
files: true,
),
"_file.mime" => new System(
name: "_file.mime",
label: "Mime Type",
ui: "text",
files: true,
),
"_file.width" => new System(
name: "_file.width",
label: "Dimensions",
ui: "text",
files: true,
),
"_file.size" => new System(
name: "_file.size",
label: "Size",
ui: "text",
files: true,
),
"_file.originalName" => new System(
name: "_file.originalName",
label: "Original Name",
ui: "text",
files: true,
),
"_file.checksum" => new System(
name: "_file.checksum",
label: "Checksum",
ui: "text",
files: true,
),
]; ];
} }
} }
+6 -8
View File
@@ -11,7 +11,6 @@ class File implements FieldInterface, MinMaxInterface
{ {
public FieldInfo $info; public FieldInfo $info;
/** /**
* @param string[] $collections * @param string[] $collections
*/ */
@@ -20,17 +19,18 @@ class File implements FieldInterface, MinMaxInterface
public string $label, public string $label,
public string $mime = "", public string $mime = "",
public string $help = "", public string $help = "",
public ?int $min = null, public ?int $min = null,
public ?int $max = null, public ?int $max = null,
public array $collections = [], public array $collections = [],
public string $group = "", public string $group = "",
) ) {
{
$this->info = new FieldInfo("file", "File", FieldType::FILE); $this->info = new FieldInfo("file", "File", FieldType::FILE);
} }
public function format(array $input, array $output): array public function format(array $input, array $output): array
{ {
$value = $input[$this->name] ?? [];
$output[$this->name] = $value;
return $output; return $output;
} }
@@ -51,6 +51,4 @@ class File implements FieldInterface, MinMaxInterface
return count($value) < $this->min; return count($value) < $this->min;
} }
} }
+15 -12
View File
@@ -16,35 +16,38 @@ class Slug implements FieldInterface, RequiredInterface
public function __construct( public function __construct(
public string $name, public string $name,
public string $label, public string $label,
public bool $required = false, public bool $required = false,
public bool $nullable = false, public bool $nullable = false,
public ?int $min = null, public ?int $min = null,
public ?int $max = null, public ?int $max = null,
public string $default = "", public string $default = "",
public string $help = "", public string $help = "",
public bool $readonly = false, public bool $readonly = false,
public string $source = "", public string $source = "",
public string $group = "", public string $group = "",
) ) {
{
$this->info = new FieldInfo("slug", "Slug", FieldType::STRING); $this->info = new FieldInfo("slug", "Slug", FieldType::STRING);
} }
public function format(array $input, array $output): array public function format(array $input, array $output): array
{ {
$value = !empty($input[$this->name]) ? (string)$input[$this->name] : null; $value = !empty($input[$this->name])
if(empty($value)){ ? (string) $input[$this->name]
: null;
if (empty($value)) {
$value = Str::slug($input[$this->source]); $value = Str::slug($input[$this->source]);
} }
$output[$this->name] = (new Nullable($this->nullable, $value, ""))->value(); $output[$this->name] = new Nullable(
$this->nullable,
$value,
"",
)->value();
return $output; return $output;
} }
public function failRequired(mixed $value): bool public function failRequired(mixed $value): bool
{ {
return empty(trim($value)); return empty(trim($value));
} }
} }
+17 -18
View File
@@ -3,7 +3,7 @@
use PhpOption\None; use PhpOption\None;
use PhpOption\Some; use PhpOption\Some;
if (!function_exists('some')) { if (!function_exists("some")) {
/** /**
* @template T * @template T
* @param T $value * @param T $value
@@ -15,51 +15,50 @@ if (!function_exists('some')) {
} }
} }
if (!function_exists("none")) {
if (!function_exists('none')) {
function none(): None function none(): None
{ {
return None::create(); return None::create();
} }
} }
if (!function_exists('toArray')) { if (!function_exists("toArray")) {
function toArray(mixed $data): array function toArray(mixed $data): array
{ {
return \json_decode(\json_encode($data), true); return \json_decode(\json_encode($data), true);
} }
} }
if (!function_exists("make_dir_r")) {
if (!function_exists('make_dir_r')) {
function make_dir_r(string $path): void function make_dir_r(string $path): void
{ {
is_dir($path) || mkdir($path, 0777, true); is_dir($path) || mkdir($path, 0777, true);
} }
} }
if (!function_exists("schemas_path")) {
if (!function_exists('schemas_path')) {
function schemas_path(): string function schemas_path(): string
{ {
return storage_path("lucent/lucent.schemas.json"); return storage_path("lucent/lucent.schemas.json");
} }
} }
if (!function_exists('lucent_file')) { if (!function_exists("lucent_file")) {
function lucent_file(\Lucent\Record\QueryRecord $record): string function lucent_file(\Lucent\Data\File $file): string
{ {
$path = $record->_file->path; $path = $file->path;
return app()->make(\Lucent\Channel\ChannelService::class)->channel->disks[$record->_file->disk] ."/". $path; return app()->make(\Lucent\Channel\ChannelService::class)->channel
->filesUrl .
"/" .
$path;
} }
} }
if (!function_exists('lucent_image')) { if (!function_exists("lucent_image")) {
function lucent_image(\Lucent\Record\QueryRecord $record, string $template): string function lucent_image(\Lucent\Data\File $file, string $template): string
{ {
$path = $record->_file->path; $path = $file->path;
return app()->make(\Lucent\Channel\ChannelService::class)->channel->disks[$record->_file->disk] . "/templates/$template/$path"; return app()->make(\Lucent\Channel\ChannelService::class)->channel
->filesUrl . "/templates/$template/$path";
} }
} }