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 ActionsOnSelected from "./ActionsOnSelected.svelte";
import Table from "./Table.svelte";
import {getContext} from "svelte";
import { getContext } from "svelte";
const axios = getContext("axios");
export let schema;
@@ -47,51 +47,49 @@
</script>
<div class="">
<div class="{inModal ? 'mt-0' : 'mt-5'}">
<h3 class="header-normal mb-5 ">
<div class={inModal ? "mt-0" : "mt-5"}>
<h3 class="header-normal mb-5">
{schema.label}
</h3>
{#if selected.length > 0 && !inModal && isWritable}
<ActionsOnSelected {schema} {selected} {filter}/>
<ActionsOnSelected {schema} {selected} {filter} />
{:else}
<Tools
bind:schema
bind:records
{systemFields}
{sortParam}
{sortField}
{operators}
{filter}
{graph}
{inModal}
{modalUrl}
{isWritable}
on:refresh={refresh}
bind:schema
bind:records
{systemFields}
{sortParam}
{sortField}
{operators}
{filter}
{graph}
{inModal}
{modalUrl}
{isWritable}
on:refresh={refresh}
/>
{/if}
<Table
{records}
{graph}
{schema}
{sortParam}
{sortField}
{systemFields}
{inModal}
{users}
{isWritable}
bind:selected
{records}
{graph}
{schema}
{sortParam}
{sortField}
{systemFields}
{inModal}
{users}
{isWritable}
bind:selected
/>
</div>
<Pagination
{limit}
{skip}
{total}
on:refresh={refresh}
{inModal}
{modalUrl}
{limit}
{skip}
{total}
on:refresh={refresh}
{inModal}
{modalUrl}
/>
</div>
+20 -13
View File
@@ -2,8 +2,8 @@
import RenderField from "./RenderField.svelte";
import Avatar from "../account/Avatar.svelte";
import Status from "../records/Status.svelte";
import {usernameById} from "../account/users";
import {friendlyDate} from "../../helpers";
import { usernameById } from "../account/users";
import { friendlyDate } from "../../helpers";
export let schema;
export let users;
@@ -12,7 +12,6 @@
export let sortParam;
export let sortField;
export let visibleColumns;
</script>
{#each visibleColumns as field, index}
@@ -20,7 +19,7 @@
class="field-ui-{field.info.name}"
class:is-sort={field.name === sortField.name}
>
<RenderField {record} {schema} {graph} {field}/>
<RenderField {record} {schema} {graph} {field} />
</td>
{/each}
{#if schema.visible?.includes("status")}
@@ -28,32 +27,40 @@
class="text-center"
class:is-sort={"-status" == sortParam || "status" == sortParam}
>
<Status status={record.status}/>
<Status status={record.status} />
</td>
{/if}
{#if schema.visible?.includes("_sys.createdBy")}
<td
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>
{/if}
{#if schema.visible?.includes("_sys.updatedBy")}
<td
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>
{/if}
{#if schema.visible?.includes("_sys.createdAt")}
<td class:is-sort={"-_sys.createdAt" == sortParam || "_sys.createdAt" == sortParam}>
{friendlyDate(record._sys.createdAt)}
<td
class:is-sort={"-_sys.createdAt" == sortParam ||
"_sys.createdAt" == sortParam}
>
{friendlyDate(record.createdAt)}
</td>
{/if}
{#if schema.visible?.includes("_sys.updatedAt")}
<td class:is-sort={"-_sys.updatedAt" == sortParam || "_sys.updatedAt" == sortParam}>
{friendlyDate(record._sys.updatedAt)}
<td
class:is-sort={"-_sys.updatedAt" == sortParam ||
"_sys.updatedAt" == sortParam}
>
{friendlyDate(record.updatedAt)}
</td>
{/if}
+60 -91
View File
@@ -1,13 +1,13 @@
<script>
import RecordRow from "./RecordRow.svelte";
import {previewTitle} from "../records/Preview";
import {usernameById} from "../account/users";
import {getContext} from "svelte";
import { previewTitle } from "../records/Preview";
import { usernameById } from "../account/users";
import { getContext } from "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 Preview from "../files/Preview.svelte";
import {fileurl} from "../files/imageserver.js";
import { fileurl } from "../files/imageserver.js";
const channel = getContext("channel");
@@ -23,107 +23,80 @@
export let selected = [];
function eventToggleAll(e) {
selected = toggleAll(e, records, selected)
selected = toggleAll(e, records, selected);
}
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>
<div class="table mt-5 ">
<div class="table mt-5">
<table>
<thead>
<tr>
{#if isWritable}
<th>
<Checkbox
<tr>
{#if isWritable}
<th>
<Checkbox
value=""
on:change={eventToggleAll}
indeterminate={selected.length > 0 && selected.length < records.length}
indeterminate={selected.length > 0 &&
selected.length < records.length}
checked={selected.length === records.length}
>
</Checkbox>
</th>
{/if}
></Checkbox>
</th>
{/if}
{#each visibleColumns as field}
<th
{#each visibleColumns as field}
<th
class="field-ui-{field.info.name ?? field.ui}"
class:is-sort={field.name === sortField.name}
scope="col"
title={field.help}
>{field.label}</th
>
{/each}
{#each systemFields.filter(c => schema.visible?.includes(c.name)) as sysField}
<th class:is-sort={sysField.name === sortField.name}>{sysField.label}</th>
{/each}
<th></th>
</tr>
title={field.help}>{field.label}</th
>
{/each}
{#each systemFields.filter( (c) => schema.visible?.includes(c.name), ) as sysField}
<th class:is-sort={sysField.name === sortField.name}
>{sysField.label}</th
>
{/each}
<th></th>
</tr>
</thead>
<tbody>
{#each records as record (record.id)}
<tr>
<td class="title-td">
<div
class="title-td-contents"
>
{#if isWritable}
<Checkbox
{#each records as record (record.id)}
<tr>
<td class="title-td">
<div class="title-td-contents">
{#if isWritable}
<Checkbox
on:change={() => select(record)}
checked={selected.find((r) => r.id === record.id)}
checked={selected.find(
(r) => r.id === record.id,
)}
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
href="{channel.lucentUrl}/records/{record.id}"
target={inModal ? "_blank" : "_self"}
href="{channel.lucentUrl}/records/{record.id}"
target={inModal ? "_blank" : "_self"}
>
{#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}
{previewTitle(channel.schemas, record, graph)}
</a>
{/if}
</div>
</td>
<RecordRow
</div>
</td>
<RecordRow
{record}
{graph}
{schema}
@@ -131,19 +104,15 @@
{sortParam}
{sortField}
{users}
/>
<td>
<Avatar
name={usernameById(
users,
record._sys.updatedBy
)}
side={24}
/>
</td>
</tr>
{/each}
<td>
<Avatar
name={usernameById(users, record.updatedBy)}
side={24}
/>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
@@ -1,6 +1,5 @@
<script>
import FilterFields from "./FilterFields.svelte";
import Uploader from "../../files/Uploader.svelte";
import Icon from "../../common/Icon.svelte";
import SortFields from "./SortFields.svelte";
import AppliedFilter from "./AppliedFilter.svelte";
+2 -8
View File
@@ -33,18 +33,12 @@
function insert(e) {
e.preventDefault();
dispatch("insert", {
records: selectedRecords,
action: "insert",
});
dispatch("insert_files", selectedRecords);
}
function replace(e) {
e.preventDefault();
dispatch("insert", {
records: selectedRecords,
action: "replace",
});
dispatch("replace_files", selectedRecords);
}
export function open(recordId) {
-15
View File
@@ -97,21 +97,6 @@
</div>
</div>
</td>
<!-- <RecordRow
{record}
{graph}
{schema}
{visibleColumns}
{sortParam}
{sortField}
{users}
/> -->
<!-- <td>
<Avatar
name={usernameById(users, record._sys.updatedBy)}
side={24}
/>
</td> -->
</tr>
{/each}
</tbody>
+24 -20
View File
@@ -1,25 +1,29 @@
export function sortByField(from, to, edges, fieldName, references) {
if (from === to) {
return edges;
}
let referenceIds = references.map(r => r.id);
let edgesTosort = edges?.filter((ed) => ed.field === fieldName && ed.depth === 1 && referenceIds.includes(ed.target)) ?? [];
let remainingEdge = edges?.filter((ed) => !(ed.field === fieldName && ed.depth === 1)) ?? [];
if (from === to) {
return edges;
}
let referenceIds = references.map((r) => r.id);
let edgesTosort =
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);
return [...remainingEdge, ...edgesTosort];
edgesTosort = array_move(edgesTosort, from, to);
return [...remainingEdge, ...edgesTosort];
}
function array_move(arr, old_index, new_index) {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) {
arr.push(undefined);
}
export function array_move(arr, old_index, new_index) {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) {
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>
import {formatDistanceToNow, parseJSON} from "date-fns";
import { formatDistanceToNow, parseJSON } from "date-fns";
import Avatar from "../account/Avatar.svelte";
import {previewTitle} from "../records/Preview";
import { previewTitle } from "../records/Preview";
import Preview from "../files/Preview.svelte";
import {usernameById} from "../account/users";
import {getContext} from "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}
);
let frieldlyUpdatedAt = formatDistanceToNow(parseJSON(record.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}
{#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><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}/>
<Avatar name={usernameById(users, record.updatedBy)} side={24} />
<div class="ms-2">
{frieldlyUpdatedAt}
</div>
+15 -23
View File
@@ -1,14 +1,13 @@
<script>
// https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387
import {onDestroy, onMount} from "svelte";
import {basicSetup, EditorView} from "codemirror";
import {autocompletion, completionKeymap} from "@codemirror/autocomplete";
import {Compartment, EditorState} from "@codemirror/state";
import {keymap} from "@codemirror/view";
import {indentWithTab} from "@codemirror/commands";
import {markdown} from "@codemirror/lang-markdown";
import {lintKeymap} from "@codemirror/lint";
import { onDestroy, onMount } from "svelte";
import { basicSetup, EditorView } from "codemirror";
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
import { Compartment, EditorState } from "@codemirror/state";
import { keymap } from "@codemirror/view";
import { indentWithTab } from "@codemirror/commands";
import { markdown } from "@codemirror/lang-markdown";
import { lintKeymap } from "@codemirror/lint";
let parentElement;
let codeMirrorView;
@@ -17,10 +16,10 @@
export function insertMedia(info) {
let insertText = "";
if (info.record._file.width > 0) {
insertText = `![${info.record._file.originalName}](${info.url})`;
if (info.file.width > 0) {
insertText = `![${info.file.filename}](${info.url})`;
} else {
insertText = `[${info.record._file.originalName}](${info.originalUrl})`;
insertText = `[${info.file.filename}](${info.originalUrl})`;
}
const cursor = codeMirrorView.state.selection.main.head;
const transaction = codeMirrorView.state.update({
@@ -29,7 +28,7 @@
insert: insertText,
},
// 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,
});
@@ -46,11 +45,7 @@
doc: value,
extensions: [
basicSetup,
keymap.of([
indentWithTab,
...lintKeymap,
...completionKeymap
]),
keymap.of([indentWithTab, ...lintKeymap, ...completionKeymap]),
language.of(markdown()),
markdown(),
autocompletion(),
@@ -63,17 +58,14 @@
}
}),
EditorView.lineWrapping,
EditorView.contentAttributes.of({spellcheck: "true"})
EditorView.contentAttributes.of({ spellcheck: "true" }),
],
});
codeMirrorView = new EditorView({
state,
parent: parentElement,
});
});
onDestroy(() => {
@@ -83,4 +75,4 @@
});
</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>
import {onDestroy, onMount} from "svelte";
import Trix from "trix"
import "trix/dist/trix.css"
import { onDestroy, onMount } from "svelte";
import Trix from "trix";
import "trix/dist/trix.css";
export let value = "";
export let field;
let editor;
function updateValue(e) {
value = e.target.value;
}
export function insertMedia(info){
if(info.record._file.width > 0){
var attachment = new Trix.Attachment({ content: info.html })
editor.editor.insertAttachment(attachment)
}else{
editor.editor.insertHTML(`<a href="${info.originalUrl}">${info.record._file.originalName}</a>`)
export function insertMedia(info) {
if (info.file.width > 0) {
var attachment = new Trix.Attachment({ content: info.html });
editor.editor.insertAttachment(attachment);
} else {
editor.editor.insertHTML(
`<a href="${info.originalUrl}">${info.file.filename}</a>`,
);
}
}
onMount(() => {
editor.addEventListener("trix-file-accept", (e) => {
e.preventDefault();
})
});
editor.addEventListener("trix-before-initialize", (e) => {
Trix.config.blockAttributes.heading1.tagName = 'h2';
const { toolbarElement } = e.target
const h1Button = toolbarElement.querySelector("[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>`)
})
})
Trix.config.blockAttributes.heading1.tagName = "h2";
const { toolbarElement } = e.target;
const h1Button = toolbarElement.querySelector(
"[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(() => {
// editor.removeEventListener("trix-before-initialize")
// })
Trix.config.blockAttributes.default.breakOnReturn = false
Trix.config.blockAttributes.default.breakOnReturn = false;
Trix.config.blockAttributes.heading3 = {
tagName: 'h3',
tagName: "h3",
terminal: true,
breakOnReturn: true,
group: false
}
group: false,
};
// console.log(Trix.config)
</script>
<div class="tox-wrapper">
<input id="x-{field.name}" {value} type="hidden">
<input id="x-{field.name}" {value} type="hidden" />
<trix-editor
bind:this={editor}
class=" content"
input="x-{field.name}"
role="textbox"
tabindex="0"
on:trix-change={updateValue}
bind:this={editor}
class=" content"
input="x-{field.name}"
role="textbox"
tabindex="0"
on:trix-change={updateValue}
></trix-editor>
</div>
+46 -63
View File
@@ -1,14 +1,13 @@
<script>
import {afterUpdate, getContext, onMount} from "svelte";
import {isEqual} from "lodash";
import { afterUpdate, getContext, onMount } from "svelte";
import { isEqual } from "lodash";
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 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();
@@ -45,10 +41,7 @@
function setOriginalContent() {
originalContent = {
data: JSON.parse(JSON.stringify(record.data)),
schema: record.schema,
status: record.status,
_sys: JSON.parse(JSON.stringify(record._sys)),
_file: JSON.parse(JSON.stringify(record._file)),
edges: JSON.parse(JSON.stringify(graph.edges)),
};
}
@@ -79,10 +72,7 @@
}
return !isEqual(originalContent, {
data: record.data,
schema: record.schema,
status: record.status,
_sys: record._sys,
_file: record._file,
edges: graph.edges,
});
}
@@ -104,7 +94,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 +107,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 +130,7 @@
errorMessage = error.response.data.error;
} else {
validationErrors = error.response.data.error;
console.log(validationErrors)
console.log(validationErrors);
}
}
resolve(null);
@@ -149,70 +142,60 @@
}
</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}/>
{#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>
+27 -32
View File
@@ -45,25 +45,20 @@
</script>
<div class="editor-field">
<FieldHeader {field} {id}/>
<FieldHeader {field} {id} />
{#if field.info.name === "reference" && field.layout === "tags"}
<ReferenceTags
bind:graph
{id}
{record}
{field}
{validationErrors}
/>
<ReferenceTags bind:graph {id} {record} {field} {validationErrors} />
{:else if field.info.name === "reference"}
<Reference
bind:graph
{id}
<Reference bind:graph {id} {record} {field} {validationErrors} />
{:else if field.info.name === "file"}
<!-- <File bind:graph {record} {field} {validationErrors} /> -->
<File
bind:value={data[field.name]}
{record}
{id}
{field}
{validationErrors}
/>
{:else if field.info.name === "file"}
<File bind:graph {record} {field} {validationErrors}/>
{:else if field.info.name === "text"}
<Text
bind:value={data[field.name]}
@@ -74,11 +69,11 @@
/>
{:else if field.info.name === "slug"}
<Slug
bind:value={data[field.name]}
{field}
{id}
{validationErrors}
{isCreateMode}
bind:value={data[field.name]}
{field}
{id}
{validationErrors}
{isCreateMode}
/>
{:else if field.info.name === "textarea"}
<Textarea
@@ -90,23 +85,23 @@
/>
{:else if field.info.name === "rich"}
<RichEditor
bind:value={data[field.name]}
{schema}
{field}
{validationErrors}
{isCreateMode}
bind:graph
{record}
bind:value={data[field.name]}
{schema}
{field}
{validationErrors}
{isCreateMode}
bind:graph
{record}
/>
{:else if field.info.name === "markdown"}
<Markdown
bind:value={data[field.name]}
{schema}
{field}
{validationErrors}
{isCreateMode}
bind:graph
{record}
bind:value={data[field.name]}
{schema}
{field}
{validationErrors}
{isCreateMode}
bind:graph
{record}
/>
{:else}
<svelte:component
+63 -80
View File
@@ -1,11 +1,11 @@
<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 { isEqual } from "lodash";
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 +30,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 +62,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 +71,7 @@
rollbackError = "";
axios
.post(
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision._sys.version}`
`${channel.lucentUrl}/records/${record.id}/rollback/${selectedRevision.version}`,
)
.then((response) => {
window.location.reload();
@@ -84,7 +84,7 @@
}
</script>
<div class="lx-card ">
<div class="lx-card">
<div class="row">
<div class="col-8">
<div>
@@ -93,29 +93,27 @@
</div>
<div>
<span class="label text-end text-muted">current version </span>
{record._sys.version}
{record.version}
</div>
<div>
<span class="label text-end text-muted"> created </span>
<Avatar
name={usernameById(users, record._sys.createdBy)}
side={24}
name={usernameById(users, record.createdBy)}
side={24}
/>
{friendlyDate(record._sys.createdAt)}
{friendlyDate(record.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.updatedBy)}
side={24}
/>
{friendlyDate(record._sys.updatedAt)}
{friendlyDate(record.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 +123,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}
{#if revision.version !== record.version}
<div
class="revision"
class:active={revision._sys.version ===
selectedRevision?._sys.version}
class="revision"
class:active={revision.version ===
selectedRevision?.version}
>
<div class="version">
<span>version {revision._sys.version}</span>
<span>version {revision.version}</span>
<Avatar
name={usernameById(users, revision._sys.updatedBy)}
side={24}
name={usernameById(users, revision.updatedBy)}
side={24}
/>
{friendlyDate(revision._sys.updatedAt)}
{friendlyDate(revision.updatedAt)}
</div>
<div class="col-3 text-center">
<button
disabled={revision._sys.version ===
selectedRevision?._sys.version}
class="button"
on:click={(e) => compare(e, revision)}
>Compare
</button
>
disabled={revision.version ===
selectedRevision?.version}
class="button"
on:click={(e) => compare(e, revision)}
>Compare
</button>
</div>
</div>
{/if}
@@ -169,15 +165,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"
>
Rollback to version {selectedRevision._sys.version}
<button on:click={rollback} class="button">
Rollback to version {selectedRevision.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 +182,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 +217,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 +234,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 +243,5 @@
{/each}
</div>
</div>
{/if}
</div>
+42 -50
View File
@@ -1,7 +1,12 @@
<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 FilePreview from "./FilePreview.svelte";
import ContentTabs from "./header/ContentTabs.svelte";
@@ -16,7 +21,7 @@
export let record;
export let graph = {
records: [],
edges: []
edges: [],
};
export let isCreateMode;
let originalContent;
@@ -25,13 +30,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) => {
@@ -53,8 +56,6 @@
data: JSON.parse(JSON.stringify(record.data)),
schema: record.schema,
status: record.status,
_sys: JSON.parse(JSON.stringify(record._sys)),
_file: JSON.parse(JSON.stringify(record._file)),
edges: JSON.parse(JSON.stringify(graph.edges)),
};
}
@@ -87,8 +88,6 @@
data: record.data,
schema: record.schema,
status: record.status,
_sys: record._sys,
_file: record._file,
edges: graph.edges,
});
}
@@ -114,8 +113,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 +151,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>
+21 -21
View File
@@ -1,33 +1,33 @@
import Mustache from "mustache";
import {stripHtml} from "../../helpers";
import { stripHtml } from "../../helpers";
export function previewTitle(schemas, record, graph) {
let schema = schemas.find((aSchema) => aSchema.name === record?.schema);
if (!schema?.cardTitle) {
return noTemplate(schema, record);
}
let schema = schemas.find((aSchema) => aSchema.name === record?.schema);
if (!schema?.cardTitle) {
return noTemplate(schema, record);
}
let recordData = record.data;
let render = Mustache.render(schema.cardTitle, recordData);
if (!render || render === "") {
return noTemplate(schema, record);
}
let recordData = record.data;
let render = Mustache.render(schema.cardTitle, recordData);
if (!render || render === "") {
return noTemplate(schema, record);
}
return stripHtml(render.slice(0, 300));
return stripHtml(render.slice(0, 300));
}
function noTemplate(schema, record) {
if (schema?.type === "files") {
return record._file.path;
}
if (schema?.type === "files") {
return file.path;
}
let title = stripHtml(
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name]
).slice(0, 300);
let title = stripHtml(
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name],
).slice(0, 300);
if(title.trim() === ""){
return "~Untitled~";
}
if (title.trim() === "") {
return "~Untitled~";
}
return title;
return title;
}
+24 -31
View File
@@ -1,10 +1,7 @@
<script>
import { sortByField } from "../../edges/sortEdges";
import { array_move } from "../../edges/sortEdges";
import Sortable from "../../libs/Sortable.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 FileDialog from "../../dialog/FileDialog.svelte";
import Uploader from "../../files/Uploader.svelte";
@@ -12,36 +9,28 @@
const channel = getContext("channel");
export let field;
export let record;
export let graph;
export let value = [];
let browseModal;
function removeReference(e) {
function removeFile(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === field.name),
);
value = value.filter((f) => !(f.id === e.detail));
}
async function reorder(e) {
graph.edges = await sortByField(
e.detail.source,
e.detail.target,
graph.edges,
field.name,
references,
);
value = await array_move(value, e.detail.source, e.detail.target);
}
function insert(e) {
function insertFiles(e) {
e.preventDefault();
browseModal.close();
graph = insertEdges(
graph,
record,
e.detail.records,
field.name,
e.detail.action,
);
value = [...(value ?? []), ...(e.detail ?? [])];
}
function replaceFiles(e) {
e.preventDefault();
browseModal.close();
value = e.detail ?? [];
}
function uploadComplete(e) {
@@ -61,18 +50,22 @@
<Uploader recordId={record.id} on:uploadComplete={uploadComplete} />
</div>
</div>
<!-- {#if references.length > 0}
{#if value.length > 0}
<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-->
<!-- <div>
<div>
<PreviewFile
record={reference}
file={aFile}
hasDelete={true}
on:remove={removeReference}
on:remove_file={removeFile}
></PreviewFile>
</div>
{/each}
</Sortable>
{/if} -->
<FileDialog bind:this={browseModal} on:insert={insert}></FileDialog>
{/if}
<FileDialog
bind:this={browseModal}
on:insert_files={insertFiles}
on:replace_files={replaceFiles}
></FileDialog>
@@ -1,91 +1,80 @@
<script>
import Icon from "../../common/Icon.svelte";
import {createEventDispatcher, getContext} from "svelte";
import { createEventDispatcher, getContext } from "svelte";
import Preview from "../../files/Preview.svelte";
import {previewTitle} from "./../Preview";
import {fileurl, htmlurl} from "../../files/imageserver.js"
import { previewTitle } from "./../Preview";
import { fileurl, htmlurl } from "../../files/imageserver.js";
import Status from "./../Status.svelte";
import Dropdown from "../../common/Dropdown.svelte";
const dispatch = createEventDispatcher();
const channel = getContext("channel");
export let record;
export let file;
export let hasDelete = 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);
function remove(e) {
e.preventDefault();
dispatch("remove", record.id);
dispatch("remove_file", file.id);
}
function insert(e, preset) {
e.preventDefault();
let html = htmlurl(channel, record, preset)
let url = !preset ? `/${record._file.path}` : `/templates/${preset}/${record._file.path}`;
dispatch("editor-insert", {
html: html,
url: channel.filesUrl + url,
originalUrl: channel.filesUrl + "/" + record._file.path,
record: record
});
// let html = htmlurl(channel, record, preset);
// let url = !preset
// ? `/${record._file.path}`
// : `/templates/${preset}/${record._file.path}`;
// dispatch("editor-insert", {
// html: html,
// url: channel.filesUrl + url,
// originalUrl: channel.filesUrl + "/" + record._file.path,
// record: record,
// });
}
</script>
<div class="preview-file">
<div style="display: flex;align-items: center;gap: 10px;">
<div class="image">
<Preview {record} size="small"/>
<Preview {file} size="small" />
</div>
<div class="title">
<div>
<a
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>
{file.filename}
</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}
<div class="reference-action">
<Dropdown>
<div slot="button">
<Icon icon="photo-film"/>
<Icon icon="photo-film" />
</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}
<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}
</Dropdown>
</div>
{/if}
{#if hasDelete}
<div class="reference-action">
<button
class="button"
on:click={remove}
>
<Icon icon="trash-can"/>
<button class="button" on:click={remove}>
<Icon icon="trash-can" />
</button>
</div>
{/if}
</div>
</div>
@@ -12,22 +12,9 @@
<div class="{colorClass} field-content">
<div class="d-flex align-items-center text-center flex-wrap">
{#each edges[field.name] as edgeRecord}
{#if edgeRecord._file?.path}
<div
class="ms-2 "
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}
<div class="ms-2">
<PreviewCardSmall record={edgeRecord} />
</div>
{/each}
</div>
</div>
@@ -43,7 +30,6 @@
<!-- {/if} -->
<style>
.field-content {
max-height: 200px;
overflow-y: scroll;