This commit is contained in:
2023-10-02 23:10:49 +03:00
commit c6cb488379
255 changed files with 18731 additions and 0 deletions
@@ -0,0 +1,54 @@
<script>
export let recordGraph;
export let record;
export let schema;
export let isCreateMode;
export let active = "_default";
let tabs = schema.fields.filter((f) => f.ui === "tab");
let mainTab = {
label: "Main",
name: "_default",
};
let graphTab = {
label: "Graph",
name: "_graph",
};
if (isCreateMode) {
tabs = [mainTab, ...tabs];
} else {
tabs = [mainTab, ...tabs, graphTab];
}
function showGraph(e) {
e.preventDefault();
active = "_graph";
}
function changeTab(e, tabName) {
e.preventDefault();
if (tabName == "_graph") {
showGraph(e);
} else {
active = tabName;
}
}
</script>
{#if tabs.length > 1}
<ul class="nav nav-pills mb-4 justify-content-center">
{#each tabs as tab}
<li class="nav-item">
<button
on:click={(e) => changeTab(e, tab.name)}
class="nav-link"
class:active={active === tab.name}
aria-current="page"
>
{tab.label}
</button>
</li>
{/each}
</ul>
{/if}
+237
View File
@@ -0,0 +1,237 @@
<script>
import {afterUpdate, getContext, onMount} from "svelte";
import {isEqual} from "lodash";
import Manager from "./Manager.svelte";
import EditHeader from "./EditHeader.svelte"
import StatusSelect from "./StatusSelect.svelte"
import FilePreview from "./FilePreview.svelte"
import ContentTabs from "./ContentTabs.svelte"
import FormField from "./FormField.svelte"
import Graph from "./Graph.svelte"
import Info from "./Info.svelte"
import ErrorAlert from "../common/ErrorAlert.svelte"
const channel = getContext("channel");
export let schema;
export let title;
export let record;
export let graph = {
records: [],
edges: []
};
export let recordHistory;
export let isCreateMode;
export let users;
let originalContent;
let activeContentTab = "_default";
let recordGraph = null;
$: hasUnsavedData = false;
$: validationErrors = null;
$: errorMessage = validationErrors
? `Record submission failed. ${
Object.entries(validationErrors).length
} error(s)`
: null;
let activeFields = schema.fields.filter(
(f) => f.name !== "id"
);
let tabname = "_default";
let fieldToTabs = schema.fields.reduce((c, f) => {
if (f.ui === "tab") {
tabname = f.name;
return c;
}
c[tabname] = [...(c[tabname] ?? []), f.name];
return c;
}, []);
onMount(() => {
setOriginalContent();
});
function setOriginalContent() {
originalContent = {
data: JSON.parse(JSON.stringify(record.data)),
_sys: JSON.parse(JSON.stringify(record._sys)),
_file: JSON.parse(JSON.stringify(record._file)),
edges: JSON.parse(JSON.stringify(graph.edges)),
};
}
afterUpdate(() => {
hasUnsavedData = checkUnsavedData();
});
function beforeUnload(e) {
// Cancel the event as stated by the standard.
// e.preventDefault();
// console.log(hasUnsavedData);
if (hasUnsavedData) {
return (e.returnValue =
"You have unsaved changes. Are you sure you want to exit?");
}
// Chrome requires returnValue to be set.
// e.returnValue = "";
delete e["returnValue"];
// more compatibility
// return true;
return "...";
}
function checkUnsavedData() {
if (isCreateMode) {
return false;
}
return !isEqual(originalContent, {
data: record.data,
_sys: record._sys,
_file: record._file,
edges: graph.edges,
});
}
function save(e) {
e.preventDefault();
console.log("SAVE: Attempt");
validationErrors = null;
errorMessage = "";
return new Promise(function (resolve, reject) {
if (!hasUnsavedData && !isCreateMode) {
resolve(null);
return;
}
if (!record) {
resolve(null);
return;
}
// remove trashed edges
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id) ?? null;
axios
.post(channel.lucentUrl + "/records", {
record: record,
edges: graph.edges,
isCreateMode: isCreateMode,
})
.then(function (response) {
console.log("SAVE: SAVED");
if (isCreateMode) {
window.location = channel.lucentUrl + "/records/" + record.id;
} else {
record = response.data.records[0] ?? null;
if (!record) {
// means trashed
hasUnsavedData = false;
window.location = channel.lucentUrl;
return;
}
graph = response.data;
setOriginalContent();
}
resolve(null);
})
.catch(function (error) {
// setOriginalContent();
if (error.response) {
if (typeof error.response.data.error === "string") {
errorMessage = error.response.data.error;
} else {
validationErrors = error.response.data.error;
console.log(validationErrors)
}
}
resolve(null);
// msgSuccess = null;
// msgError = error.response.data.error;
// submitted = false;
});
});
}
</script>
<svelte:window on:beforeunload={beforeUnload}/>
<div class="wrapper-normal transparent">
<Manager managerRecords={recordHistory} {graph}/>
<EditHeader {schema} {record} {isCreateMode} {graph} bind:activeContentTab/>
{#if !["_graph", "_info"].includes(activeContentTab)}
<div
style="position:fixed;bottom:0;left:0px;width:100%;background:rgba(255,255,255,.7);z-index:10"
>
<div
class="d-flex mt-4 mb-3 align-items-center justify-content-center"
>
<StatusSelect bind:status={record._sys.status} {schema}/>
{#if isCreateMode}
<button
class="ms-2 btn btn-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="ms-2 btn btn-primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Save
</button>
{/if}
</div>
</div>
{/if}
<ErrorAlert message={errorMessage}/>
<div class=" mt-4" style="margin-bottom:150px">
<ContentTabs
{schema}
{isCreateMode}
bind:active={activeContentTab}
{record}
bind:recordGraph
/>
{#if !["_graph", "_info"].includes(activeContentTab)}
<FilePreview {record} {schema}/>
<!-- <fieldset disabled="disabled"> -->
{#each activeFields as field (field.name)}
{#if fieldToTabs[activeContentTab].includes(field.name)}
<FormField
bind:data={record.data}
bind:graph={graph}
{field}
{schema}
{record}
{validationErrors}
{isCreateMode}
/>
{/if}
{/each}
<!-- </fieldset> -->
{:else if activeContentTab === "_graph"}
<Graph {graph} {record}/>
{:else if activeContentTab === "_info"}
<Info bind:record {users} {schema}/>
{/if}
</div>
</div>
+85
View File
@@ -0,0 +1,85 @@
<script>
import {getContext} from "svelte";
import Icon from "../common/Icon.svelte";
import {previewTitle} from "./Preview";
const channel = getContext("channel");
export let schema;
export let graph;
export let record;
export let isCreateMode;
export let activeContentTab;
function clone(e) {
e.preventDefault();
axios.post(channel.lucentUrl + "/records/clone/" + record.id).then(response => {
window.location = channel.lucentUrl + "/records/" + response.data.id;
}).catch(error => {
});
}
</script>
<h3 class="header-normal mt-5 mb-0">
<a
class="text-muted d-block text-decoration-none fs-6 mb-1"
href="{channel.lucentUrl}/content/{schema.name}"
>{schema.label.toUpperCase()}</a
>
<span class="text-dark d-block">
{#if !isCreateMode}
{previewTitle(channel.schemas, record, graph)}
{:else}
New Record
{/if}
</span>
{#if !isCreateMode}
<div class="dropdown d-inline-block">
<button
class="btn btn-link btn-sm"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Icon icon="ellipsis"/>
</button>
<div class="dropdown-menu">
<!--{#if channel.previewTargets.length > 0}-->
<!-- <h6 class="dropdown-header">Preview targets</h6>-->
<!-- {#each channel.previewTargets as previewTarget}-->
<!-- <a-->
<!-- class="dropdown-item"-->
<!-- target="_blank"-->
<!-- rel="noreferrer"-->
<!-- href="{previewTarget.url}?id={record.data-->
<!-- .id}&schema={record._sys.schema}"-->
<!-- >{previewTarget.label}</a-->
<!-- >-->
<!-- {/each}-->
<!--{/if}-->
<h6 class="dropdown-header">Record Actions</h6>
<a
class="dropdown-item"
href="{channel.lucentUrl}/records/new?schema={schema.name}"
>Create new</a
>
{#if !isCreateMode}
<a
class="dropdown-item"
on:click={clone}
href={channel.lucentUrl}
>
Clone
</a>
{/if}
<a
on:click|preventDefault={(e) =>
(activeContentTab = "_info")}
class="dropdown-item"
href="{channel.lucentUrl}">Revisions</a
>
</div>
</div>
{/if}
</h3>
@@ -0,0 +1,55 @@
<script>
import Preview from "../files/Preview.svelte";
import {fileurl} from "../files/imageserver"
export let record;
export let schema;
</script>
{#if schema.type === "files"}
<div class="row mb-4">
<div class="col" style="max-width:276px">
<Preview {record} size="large"/>
</div>
<div class="col">
<ul class="list-group ">
<li class="list-group-item border-primary">
<span class="text-muted">Filename</span>
<span>{record._file.path}</span>
</li>
<li class="list-group-item border-primary">
<span class="text-muted">Original name</span>
<span>{record._file.originalName}</span>
</li>
<li class="list-group-item border-primary">
<span class="text-muted">Mime type</span>
<span>{record._file.mime}</span>
</li>
{#if record._file.width}
<li class="list-group-item border-primary">
<span class="text-muted">Dimensions</span>
<span>{record._file.width}x{record._file.height}</span>
</li>
{/if}
<li class="list-group-item border-primary">
<span class="text-muted">File size</span>
<span>{(record._file.size / 1024).toFixed(1)}kB</span>
</li>
<li class="list-group-item border-primary">
<span class="text-muted">Checksum</span>
<span>{record._file.checksum}</span>
</li>
<li class="list-group-item border-primary">
<span class="text-muted">Download</span>
<a href="{fileurl(record)}">{record._file.path}</a>
</li>
</ul>
</div>
</div>
{/if}
<style>
.list-group {
font-size: 14px;
}
</style>
+109
View File
@@ -0,0 +1,109 @@
<script>
import Text from "./elements/Text.svelte";
import Reference from "./elements/Reference.svelte";
import ReferenceInline from "./elements/ReferenceInline.svelte";
import Block from "./block/Block.svelte";
import Color from "./elements/Color.svelte";
import Checkbox from "./elements/Checkbox.svelte";
import Number from "./elements/Number.svelte";
import Url from "./elements/Url.svelte";
import Date from "./elements/Date.svelte";
import UUID from "./elements/UUID.svelte";
import File from "./elements/File.svelte";
import Textarea from "./elements/Textarea.svelte";
import Datetime from "./elements/Datetime.svelte";
import RichEditor from "./elements/RichEditor.svelte";
import Json from "./elements/JSON.svelte";
import FieldHeader from "./elements/FieldHeader.svelte";
import ReferenceTable from "./elements/ReferenceTable.svelte";
const formElements = {
text: Text,
textarea: Textarea,
rich: RichEditor,
color: Color,
checkbox: Checkbox,
number: Number,
url: Url,
date: Date,
datetime: Datetime,
uuid: UUID,
json: Json,
};
export let field;
export let data;
export let schema;
export let record;
export let graph;
export let validationErrors;
export let isCreateMode;
let formElement = formElements[field.info.name];
const id = `field-${field.name}-${record.id}`;
</script>
<div class="card editor-field">
<FieldHeader {schema} {field} {id}/>
{#if field.info.name === "reference" && field.layout === "inline"}
<ReferenceInline
bind:graph
{record}
{schema}
{field}
{validationErrors}
/>
{:else if field.info.name === "reference" && field.layout === "table"}
<ReferenceTable
bind:graph
{record}
{schema}
{field}
{validationErrors}
/>
{:else if field.info.name === "reference"}
<Reference
bind:graph
{record}
{schema}
{field}
{validationErrors}
/>
{:else if field.info.name === "file"}
<File bind:graph {record} {field} {validationErrors}/>
{:else if field.info.name === "block"}
<Block
bind:graph
bind:value={data[field.name]}
{record}
{id}
{field}
{validationErrors}
/>
{:else if field.info.name === "text"}
<Text
bind:value={data[field.name]}
{field}
{id}
{validationErrors}
{isCreateMode}
/>
{:else if field.info.name === "textarea"}
<Textarea
bind:value={data[field.name]}
{field}
{validationErrors}
{isCreateMode}
{id}
/>
{:else}
<svelte:component
this={formElement}
bind:value={data[field.name]}
{schema}
{field}
{validationErrors}
{isCreateMode}
{id}
/>
{/if}
</div>
+126
View File
@@ -0,0 +1,126 @@
<script>
import PreviewCardSmall from "./PreviewCardSmall.svelte";
import PreviewCard from "./PreviewCard.svelte";
import Icon from "../common/Icon.svelte";
import Preview from "../files/Preview.svelte";
import {getContext} from "svelte";
const channel = getContext("channel");
export let graph;
export let record;
let parentEdgesByField = graph.edges
.filter((edge) => edge.source !== record.id && edge.depth === 0)
.reduce((carry, edge) => {
let schemaField = edge.sourceSchema + edge.field;
let arecord = graph.records.find((n) => {
return n.id === edge.source;
});
if (!carry[schemaField]) {
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
carry[schemaField] = {
field: schema.fields.find((f) => f.name === edge.field),
schema: schema,
nodes: [],
};
}
if (arecord) {
carry[schemaField].nodes.push(arecord);
}
return carry;
}, {});
let childrenEdgesByField = graph.edges
.filter((edge) => edge.source === record.id && edge.depth === 0)
.reduce((carry, edge) => {
let schemaField = edge.targetSchema + edge.field;
if (!carry[schemaField]) {
carry[schemaField] = {
field: channel.schemas
.find((s) => s.name === record._sys.schema)
.fields.find((f) => f.name === edge.field),
nodes: [],
};
}
let arecord = graph.records.find((n) => {
return n.id === edge.target;
});
if (arecord) {
carry[schemaField].nodes.push(arecord);
}
return carry;
}, {});
</script>
{#each Object.entries(parentEdgesByField) as [fieldName, fieldData]}
<div class="lx-card mt-3">
<div class="text-center mb-3 d-flex justify-content-center align-items-center text-uppercase ">
<span>{fieldData.schema.label}</span>
<Icon icon="angle-right" width="12" height="12"/>
<span>{fieldData.field.label}</span>
</div>
<div class="d-flex justify-content-center text-center flex-wrap">
{#each fieldData.nodes as node}
{#if node._file?.path}
<div class="ms-2 mb-2" style="max-height:64px;">
<Preview record={node} size="small"/>
</div>
{:else}
<div class="ms-2 mb-2">
<PreviewCardSmall {graph} record={node}/>
</div>
{/if}
{/each}
</div>
<!-- <div class="text-center mt-3 d-block">{fieldData.field.label}</div>-->
</div>
{/each}
{#if Object.entries(parentEdgesByField).length > 0}
<div class="text-center my-4">
<Icon icon="angles-down" width="32" height="32"/>
</div>
{/if}
<div style="max-width:400px;margin:0 auto;">
<PreviewCard {graph} record={record}/>
</div>
{#if Object.entries(childrenEdgesByField).length > 0}
<div class="text-center my-4">
<Icon icon="angles-down" width="32" height="32"/>
</div>
{/if}
{#each Object.entries(childrenEdgesByField) as [fieldName, fieldData]}
<div class="lx-card mt-3">
<div class="text-center mb-5 d-block">{fieldData.field.label}</div>
<div class="d-flex justify-content-center text-center flex-wrap">
{#each fieldData.nodes as node}
{#if fieldData.field.info.ui === "file"}
<div
class="ms-2 mb-2"
style="max-width:64px;overflow:hidden;white-space: nowrap;text-overflow: ellipsis;"
>
<Preview
record={node}
size="small"
showFilename={true}
/>
</div>
{:else}
<div class="ms-2 mb-2">
<PreviewCardSmall {graph} record={node}/>
</div>
{/if}
{/each}
</div>
</div>
{/each}
<style>
</style>
+240
View File
@@ -0,0 +1,240 @@
<script>
import {friendlyDate} from "../../helpers";
import Avatar from "../account/Avatar.svelte";
import {usernameById} from "../account/users";
import {isEqual} from "lodash";
import Status from "./Status.svelte";
import Icon from "../common/Icon.svelte";
import RevisionCell from "./revisions/RevisionCell.svelte";
export let record;
export let users;
export let schema;
let rollbackError = "";
$: revisions = [];
$: fieldsWithDiff = [];
$: selectedRevision = null;
$: recordEdges = {};
$: selectedRevisionEdges = {};
axios
.get(`/records/${record.id}/revisions`)
.then((response) => {
revisions = response.data;
})
.catch((error) => {
console.log(error);
});
function getEdgesByField(edges) {
return schema.fields
.filter((f) => ["file", "reference"].includes(f.ui))
.reduce((c, f) => {
let fieldEdges = edges
.filter((e) => e.field === f.name)
.map((e) =>
record._children.find((child) => child.id === e.to)
);
c[f.name] = fieldEdges;
return c;
}, {});
}
function compare(e, revision) {
e.preventDefault();
selectedRevision = revision;
fieldsWithDiff = schema.fields.filter((f) => {
return !isEqual(selectedRevision.data[f.name], record.data[f.name]);
});
}
function rollback(e) {
e.preventDefault();
rollbackError = "";
axios
.post(
`/records/${record.id}/rollback/${selectedRevision._sys.version}`
)
.then((response) => {
window.location.reload();
})
.catch((error) => {
const firstError = error.response.data.error;
rollbackError =
firstError.fieldLabel + ": " + firstError.message;
});
}
</script>
<div class="lx-card ">
<div class="row">
<div class="col-8">
<div>
<span class="label text-end text-muted">record id </span>
<small>{record.id}</small>
</div>
<div>
<span class="label text-end text-muted">current version </span>
{record._sys.version}
</div>
<div>
<span class="label text-end text-muted"> created </span>
<Avatar
name={usernameById(users, record._sys.createdBy)}
side={24}
/>
{friendlyDate(record._sys.createdAt)}
</div>
<div>
<span class="label text-end text-muted">updated </span>
<Avatar
name={usernameById(users, record._sys.updatedBy)}
side={24}
/>
{friendlyDate(record._sys.updatedAt)}
</div>
</div>
<div class="col-4">
<span class="label d-block text-muted "
>Rules for this schema
</span>
<small>
Revisions are retained for {schema.revisionRetentionDays} days
<br/>
Each record maintains the last {schema.revisionRetentionNumber}
versions
</small>
</div>
</div>
</div>
<div class="lx-card mt-4">
{#if schema.revisionRetentionDays > 0}
<div class="header-small mb-3">Revisions</div>
{#each revisions as revision}
{#if revision._sys.version != record._sys.version}
<div
class="row p-2 rounded"
class:active={revision._sys.version ===
selectedRevision?._sys.version}
>
<div class="col-2">
<Status status={revision._sys.status}/>
</div>
<div class="col-2">version {revision._sys.version}</div>
<div class="col-5">
<Avatar
name={usernameById(users, revision._sys.updatedBy)}
side={24}
/>
{friendlyDate(revision._sys.updatedAt)}
</div>
<div class="col-3 text-center">
<button
disabled={revision._sys.version ===
selectedRevision?._sys.version}
class="btn btn-sm btn-outline-primary"
on:click={(e) => compare(e, revision)}
>Compare
</button
>
</div>
</div>
{/if}
{/each}
{:else}
<div class="card-body">
<span>Revisions are not enabled for this Schema</span>
</div>
{/if}
</div>
{#if selectedRevision}
<div class="mt-4">
{#if fieldsWithDiff.length > 0}
<p class="text-center fw-bold mb-3 mt-5">
If you choose to rollback to this revision
</p>
<button
on:click={rollback}
class="btn btn-primary mb-5 d-block mx-auto"
>
Rollback to version {selectedRevision._sys.version}
</button>
{#if rollbackError}
<span class="d-block text-danger mt-3">{rollbackError}</span>
{/if}
<div class="mt-3">
{#each fieldsWithDiff as field}
<!-- <div
class=" lx-card d-flex p-4 mb-4"
style="overflow:hidden"
> -->
<!-- <div class="d-block" style="width:200px;">
{field.label}
</div> -->
<div
class="lx-card row p-4 mb-4 w-100"
style="overflow:hidden"
>
<div class="col-5">
<RevisionCell
edges={recordEdges}
{field}
side={record.data[field.name]}
colorClass="text-danger"
/>
</div>
<div class="col-2">
<div
class="h-100 d-flex align-items-center justify-content-center text-secondary"
>
<span class="me-1">{field.label}</span>
<Icon
icon="angle-right"
width="12"
height="12"
/>
</div>
</div>
<div class="col-5">
<RevisionCell
edges={selectedRevisionEdges}
{field}
side={selectedRevision.data[field.name]}
colorClass="text-success"
/>
</div>
</div>
<!-- </div> -->
{/each}
</div>
{:else}
<div class=" lx-card text-center">
<span>Nothing will change</span>
</div>
{/if}
</div>
{/if}
<style>
.label {
width: 180px;
margin-right: 10px;
margin-bottom: 4px;
display: inline-block;
}
td {
vertical-align: inherit;
white-space: normal;
max-width: none;
}
.active {
background-color: #eee;
border: 1px solid #ccc;
}
</style>
+225
View File
@@ -0,0 +1,225 @@
<script>
import {afterUpdate, createEventDispatcher, onMount,} from "svelte";
import {isEqual} from "lodash";
import FormField from "./FormField.svelte";
import FilePreview from "./FilePreview.svelte";
import ContentTabs from "./ContentTabs.svelte";
import StatusSelect from "./StatusSelect.svelte";
import ErrorAlert from "../common/ErrorAlert.svelte";
const dispatch = createEventDispatcher();
export let schema;
export let schemas;
export let record;
export let graph = {
records: [],
edges: []
};
export let isCreateMode;
let originalContent;
let activeContentTab = "_default";
let hasUnsavedData = false;
$: validationErrors = null;
$: errorMessage = validationErrors
? `Record submission failed. ${
Object.entries(validationErrors).length
} error(s)`
: null;
let activeFields = schema.fields.filter(
(f) => f.trashed === false && f.name !== "id"
);
let tabname = "_default";
let fieldToTabs = schema.fields.reduce((c, f) => {
if (f.ui === "tab") {
tabname = f.name;
return c;
}
c[tabname] = [...(c[tabname] ?? []), f.name];
return c;
}, []);
onMount(() => {
setOriginalContent();
});
function setOriginalContent() {
originalContent = {
data: JSON.parse(JSON.stringify(record.data)),
_sys: JSON.parse(JSON.stringify(record._sys)),
_file: JSON.parse(JSON.stringify(record._file)),
edges: JSON.parse(JSON.stringify(graph.edges)),
};
}
afterUpdate(() => {
hasUnsavedData = checkUnsavedData();
});
function beforeUnload(e) {
// Cancel the event as stated by the standard.
// e.preventDefault();
// console.log(hasUnsavedData);
if (hasUnsavedData) {
return (e.returnValue =
"You have unsaved changes. Are you sure you want to exit?");
}
// Chrome requires returnValue to be set.
// e.returnValue = "";
delete e["returnValue"];
// more compatibility
// return true;
return "...";
}
function checkUnsavedData() {
if (isCreateMode) {
return false;
}
return !isEqual(originalContent, {
data: record.data,
_sys: record._sys,
_file: record._file,
edges: graph.edges,
});
}
function cancel(e) {
e.preventDefault();
dispatch("cancel");
}
function save(e) {
e.preventDefault();
console.log("SAVE: Attempt");
validationErrors = null;
errorMessage = "";
return new Promise(function (resolve, reject) {
if (!hasUnsavedData && !isCreateMode) {
resolve(null);
return;
}
if (!record) {
resolve(null);
return;
}
// remove trashed edges
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id) ?? [];
axios
.post("/records", {
record: record,
edges: graph.edges,
isCreateMode: isCreateMode,
})
.then(function (response) {
console.log("SAVE: SAVED INLINE");
record = response.data.records[0];
graph = response.data;
if (!isCreateMode) {
setOriginalContent();
}
dispatch("inlinesaved", {
records: [record],
});
resolve(null);
})
.catch(function (error) {
// setOriginalContent();
if (error.response) {
if (typeof error.response.data.error === "string") {
errorMessage = error.response.data.error;
} else {
validationErrors = error.response.data.error;
}
}
resolve(null);
// msgSuccess = null;
// msgError = error.response.data.error;
// submitted = false;
});
});
}
</script>
<svelte:window on:beforeunload={beforeUnload}/>
<div class="inline-edit my-4">
<ErrorAlert message={errorMessage}/>
<div class=" mt-1">
<ContentTabs
{schema}
{isCreateMode}
bind:active={activeContentTab}
{record}
/>
<FilePreview {record} {schema}/>
<!-- <fieldset disabled="disabled"> -->
{#each activeFields as field (field.name)}
{#if fieldToTabs[activeContentTab].includes(field.name)}
<FormField
bind:data={record.data}
bind:graph={graph}
{field}
{schema}
{schemas}
{record}
{validationErrors}
{isCreateMode}
/>
{/if}
{/each}
<!-- </fieldset> -->
</div>
<div>
<div class="d-flex mt-3 align-items-center justify-content-center">
{#if schema.hasDrafts}
<StatusSelect bind:status={record._sys.status} {schema}/>
{/if}
{#if isCreateMode}
<button
class="ms-2 btn btn-primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Add
</button>
{:else}
<button
disabled={!hasUnsavedData}
class="ms-2 btn btn-primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Save
</button>
{/if}
<button class="ms-2 btn btn-link" on:click={cancel}>
cancel
</button>
</div>
</div>
</div>
<style>
.inline-edit {
padding: 44px;
background-color: #eee;
border-radius: 32px;
}
</style>
+34
View File
@@ -0,0 +1,34 @@
<script>
import Icon from "../common/Icon.svelte";
import PreviewCardSmall from "./PreviewCardSmall.svelte";
export let managerRecords;
export let graph;
</script>
{#if managerRecords.length > 0}
<div
class="record-history d-flex justify-content-center align-items-center w-100 mb-4 mt-4"
>
{#each managerRecords.reverse() as arecord, i}
{#if i !== 0}
<Icon icon="angle-right"/>
{/if}
<div class="mx-3 p-0 my-0">
<PreviewCardSmall record={arecord} {graph}/>
</div>
{/each}
</div>
{/if}
<style>
.record-history {
/* background-color: #fff; */
padding: 15px 10px;
border-radius: 32px;
line-height: 12px;
}
</style>
+9
View File
@@ -0,0 +1,9 @@
<script>
</script>
<div class="wrapper-normal ">
<div class="header-normal">
Record Not Found
</div>
</div>
+44
View File
@@ -0,0 +1,44 @@
import Mustache from "mustache";
import {stripHtml} from "../../helpers";
export function previewTitle(schemas, record, graph) {
let schema = schemas.find((aschema) => aschema.name === record?._sys.schema);
if (!schema?.titleTemplate) {
return noTemplate(schema, record);
}
let recordData = record.data;
let template = Mustache.parse(schema.titleTemplate);
let referencePreviews = template
.filter(segment => segment[0] == "name") // keep only template tags
.map((segment) => segment[1]) // map to fieldNames
.filter(fieldName => { // keep only references
let schemaField = schema.fields.find(f => f.name === fieldName)
return schemaField?.info.name === "reference";
}).reduce((carry, field) => { // map to records
let edge = graph.edges.find(edge => edge.source === record.id && edge.field === field)
let referenceRecord = graph.records.find(rec => rec.id === edge?.target)
carry[field] = previewTitle(schemas, referenceRecord, graph);
return carry;
}, {});
recordData = {...recordData, ...referencePreviews}
let render = Mustache.render(schema.titleTemplate, recordData);
if (!render || render === "") {
return noTemplate(schema, record);
}
return stripHtml(render.slice(0, 300));
}
function noTemplate(schema, record) {
if (schema?.type === "files") {
return record._file.path;
}
return stripHtml(
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name]
).slice(0, 300);
}
@@ -0,0 +1,81 @@
<script>
import Icon from "../common/Icon.svelte";
import { getContext, createEventDispatcher } from "svelte";
import Preview from "../files/Preview.svelte";
import { previewTitle } from "./Preview";
import Status from "./Status.svelte";
const dispatch = createEventDispatcher();
const channel = getContext("channel");
export let graph;
export let record;
export let classes = "";
export let hasDelete = false;
let schema = channel.schemas.find((aschema) => aschema.name === record._sys.schema);
let cardTitle = previewTitle(channel.schemas, record, graph);
function remove(e) {
e.preventDefault();
dispatch("remove", record.id);
}
</script>
<div
class="card mb-2 bg-light {classes}"
style="border-color:{schema.color ?? '#ccc'}; border-width: 1px;"
>
<div class="card-body d-flex">
{#if schema.type === "files"}
<div style="max-width:94px;margin-right:15px">
<Preview {record} size="small" />
</div>
{/if}
<div class="overflow-hidden">
<a
class="title-link m-0 fs-5 text-decoration-none text-dark d-block"
href="{channel.lucentUrl}/records/{record.id}"
title={cardTitle}
>
{cardTitle}
</a>
<small class="text-muted">
{schema.label}
</small>
<small class="text-muted">
{#if record._sys.status === "draft"}
<Status status={record._sys.status} />
{/if}
</small>
</div>
</div>
{#if hasDelete}
<div class="position-absolute end-0" style="top:5px">
<button
class="trash-button text-dark btn btn-sm btn-link"
on:click={remove}
><Icon icon="trash-can" />
</button>
</div>
{/if}
</div>
<style>
.button-file {
width: 64px;
height: 65px;
}
.card .trash-button {
display: none;
}
.card:hover .trash-button {
display: block;
}
.title-link {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>
@@ -0,0 +1,228 @@
<script>
import Icon from "../common/Icon.svelte";
import {createEventDispatcher, onMount} from "svelte";
import Preview from "../files/Preview.svelte";
import InlineEdit from "./InlineEdit.svelte";
import Reference from "../content/elements/Reference.svelte";
import File from "../content/elements/File.svelte";
const dispatch = createEventDispatcher();
export let isFirst;
export let isLast;
export let toDelete = false;
export let schemas;
export let record;
let editRecord;
let schema = schemas.find((aschema) => aschema.name === record._sys.schema);
$: editMode = false;
$: expanded = false;
function editInline(e) {
e.preventDefault();
axios
.get("/records/editInline/" + record.id)
.then((response) => {
record = response.data;
editRecord = response.data;
editMode = true;
})
.catch((error) => {
console.log(error);
});
}
function moveup(e) {
e.preventDefault();
dispatch("moveup");
}
function movedn(e) {
e.preventDefault();
dispatch("movedn");
}
function handleInlinesaved(e) {
e.preventDefault();
dispatch("inlinesaved", e.detail);
editMode = false;
}
function remove(e) {
e.preventDefault();
dispatch("remove", record.id);
}
function trash(e) {
e.preventDefault();
dispatch("trash", record.id);
}
function undo(e) {
e.preventDefault();
dispatch("undoremove", record.id);
}
function cancel(e) {
e.preventDefault();
editMode = false;
}
onMount(() => {
editMode = false;
});
function deleteFromChannel(e) {
e.preventDefault();
axios
.post("/records/status/trashed", [record])
.then((response) => {
dispatch("remove", record.id);
})
.catch((error) => {
console.log(error);
});
}
</script>
<div>
{#if toDelete}
<div class="lx-card bg-danger bg-opacity-10 text-center">
<p>Item was removed from the current record.</p>
<p>
<button
class="btn btn-sm btn-outline border border-1 border-dark"
on:click={undo}>Undo
</button
>
<button
class="btn btn-sm btn-danger "
on:click={deleteFromChannel}
>Delete completely from channel
</button
>
</p>
<button class="btn btn-sm btn-link" on:click={remove}
>Dismiss Message
</button
>
</div>
{:else if editMode === true}
<InlineEdit
{schema}
{schemas}
record={editRecord}
isCreateMode={false}
on:cancel={cancel}
on:inlinesaved={handleInlinesaved}
/>
{:else}
<div class="lx-card mt-4 bg-primary bg-opacity-10">
<div class="actions">
<small class="text-muted">{schema.label}</small>
<button
class="btn btn-sm btn-link"
on:click|preventDefault={editInline}
>
<Icon icon="pencil" width={12} height={12}/>
</button>
<button
class="btn btn-sm btn-link"
on:click={(e) => (expanded = !expanded)}
>
{#if expanded}
<Icon icon="compress" width={12} height={12}/>
{:else}
<Icon icon="expand" width={12} height={12}/>
{/if}
</button>
<div class="dropdown d-inline-block">
<button
class="btn btn-link btn-sm"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Icon icon="ellipsis"/>
</button>
<div class="dropdown-menu">
<a
class="dropdown-item"
href="/records/{record.id}"
target="_blank"
>Edit in new tab
</a>
<button class="dropdown-item" on:click={trash}>
Remove
</button>
<div class="text-center mt-3">
<!-- <a class="dropdown-item" href="#">Clone</a> -->
{#if !isFirst}
<button
class="btn btn-sm btn-outline-primary border-0"
on:click|preventDefault={moveup}
>
<Icon icon="circle-chevron-up"/>
</button>
{/if}
{#if !isLast}
<button
class="btn btn-sm btn-outline-primary border-0"
on:click|preventDefault={movedn}
>
<Icon icon="circle-chevron-down"/>
</button>
{/if}
</div>
</div>
</div>
</div>
<div class="inline-preview" class:expanded>
{#if schema.type === "files"}
<Preview {record} size="small"/>
{/if}
{#each schema.fields.filter((f) => !(f.trashed || ["tab"].includes(f.ui) || ["id"].includes(f.name))) as field}
<span class="text-muted d-block mt-2" style="font-size:13px"
>{field.label}</span
>
{#if field.ui === "reference"}
<Reference {record} {schemas} {field}/>
{:else if field.ui === "file"}
<File {record} {field}/>
{:else}
{@html record.data[field.name]}
{/if}
{/each}
</div>
</div>
{/if}
</div>
<style>
.lx-card {
position: relative;
}
.lx-card .inline-preview {
max-height: 120px;
overflow: hidden;
}
.lx-card .inline-preview.expanded {
max-height: none;
}
.lx-card .actions {
top: 10px;
right: 44px;
position: absolute;
/* visibility: hidden; */
}
/* .lx-card:hover .actions {
visibility: visible;
} */
</style>
@@ -0,0 +1,39 @@
<script>
import {previewTitle} from "./Preview";
import {getContext} from "svelte";
const channel = getContext("channel");
export let record;
export let graph;
$: schema = channel.schemas.find((aschema) => aschema.name === record._sys.schema);
$: title = previewTitle(channel.schemas, record, graph);
</script>
{#if record?.data}
<a
href="{channel.lucentUrl}/records/{record.id}"
class="text-decoration-none rounded py-1 px-2 d-inline-block"
{title}
style="border:2px solid {!schema.color
? '#999'
: schema.color}!important;white-space: nowrap;"
>
{title}
</a>
{/if}
<style>
a {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
color: #333;
}
a:hover {
opacity: 0.5;
/* color: #fff; */
}
</style>
+10
View File
@@ -0,0 +1,10 @@
<script>
import { getStatus } from "./StatusText";
export let status;
let statusObj = getStatus(status);
</script>
<span class="badge text-bg-{statusObj.bg}" style="max-width:84px"
>{statusObj.text}</span
>
@@ -0,0 +1,46 @@
<script>
import Status from "./Status.svelte";
import {getStatus, getStatusList} from "./StatusText";
export let status;
export let schema;
let dropdown;
$: currentStatus = getStatus(status);
const statusList = Object.values(getStatusList());
function updateStatus(e, statusValue) {
// e.preventDefault();
status = statusValue;
dropdown.click();
}
</script>
<!-- Example split danger button -->
<div class="btn-group dropup">
<button type="button" class="btn btn-{currentStatus.bg}"
>{currentStatus.text}</button
>
<button
bind:this={dropdown}
type="button"
class="btn btn-{currentStatus.bg} dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<div class="dropdown-menu">
<div class="dropdown-header">Change status to</div>
{#each statusList as astatus}
{#if astatus.value !== status}
<button
type="button"
class="dropdown-item my-2 rounded w-100 bg-{astatus.bg} text-{astatus.color}"
on:click={(e) => updateStatus(e, astatus.value)}
>
{astatus.text}
</button>
{/if}
{/each}
</div>
</div>
+28
View File
@@ -0,0 +1,28 @@
export function getStatus(status) {
const statusList = getStatusList();
return statusList[status];
}
export function getStatusList(){
return {
published: {
value: "published",
text: "Published",
bg: "success",
color: "white",
},
trashed: {
value: "trashed",
text: "Trashed",
bg: "danger",
color: "white",
},
draft: {
value: "draft",
text: "Draft",
bg: "warning",
color: "dark",
},
};
}
+113
View File
@@ -0,0 +1,113 @@
<script>
import Mustache from "mustache";
import { imgurl } from "../files/imageserver";
import { uniqBy } from "lodash";
import { onMount, getContext } from "svelte";
import { Network } from "vis-network/esnext";
import "vis-network/styles/vis-network.css";
const channelurl = getContext("channelurl");
export let schemas;
export let recordGraph;
let network;
let allEdges = recordGraph._graph.edges.map((e) => {
e.fieldLabel = schemas
.find((s) => s.uid === e.fromSchema)
.fields.find((f) => f.name === e.field).label;
return e;
});
let nodes = recordGraph._graph.nodes.map((r) => {
let nodeOptions = {
id: r.data.id,
label: renderTitle(
schemas.find((s) => s.uid === r._sys.schema),
r
),
borderWidth: 0,
color: {
background:
r.data.id === recordGraph.data.id ? "#0b5d1e" : "#eeeeee",
},
font: {
multi: true,
color: r.data.id === recordGraph.data.id ? "#fff" : "#333",
},
};
nodeOptions.shape = "box";
if (r._file?.path) {
nodeOptions.shape = "image";
nodeOptions.image = imgurl(r, 64, 64, "crop");
}
return nodeOptions;
});
nodes = uniqBy(nodes, (n) => n.id);
// create an array with edges
let networkEdges = allEdges.map((e) => {
return {
from: e.from,
to: e.to,
label: e.fieldLabel,
arrows: {
to: {
enabled: true,
type: "arrow",
scaleFactor: 0.5,
},
},
font: { align: "middle" },
};
});
let data = {
nodes: nodes,
edges: networkEdges,
};
let options = {
physics: false,
layout: {
hierarchical: {
enabled: true,
nodeSpacing: 150,
treeSpacing: 200,
direction: "UD",
sortMethod: "directed",
},
},
};
onMount(() => {
let networkInstance = new Network(network, data, options);
networkInstance.on("doubleClick", function (params) {
if (params.nodes[0]) {
window.location = channelurl + "/records/" + params.nodes[0];
}
});
});
function renderTitle(schema, record) {
const template = `${schema.titleTemplate}\n <i>${schema.label}</i>`;
Mustache.parse(template);
let recordData = { ...record, ...record.data };
return Mustache.render(template, recordData);
}
</script>
<div class="lx-card">
<div class="network-container" bind:this={network} />
</div>
<style>
.network-container {
width: 100%;
height: 80vh;
max-height: 800px;
}
</style>
@@ -0,0 +1,35 @@
<script>
import BlockButtons from "./BlockButtons.svelte";
import BlockElements from "./BlockElements.svelte";
import {flip} from "svelte/animate";
import {quintOut} from 'svelte/easing';
export let record;
export let field;
export let value = [];
export let schemas;
export let graph;
</script>
<div class="inline-card-wrapper">
<BlockButtons
bind:blockData={value}
/>
</div>
{#each value as blockItemData (blockItemData.id)}
<div class="block-field-wrapper" animate:flip="{{delay: 250, duration: 250, easing: quintOut}}">
<BlockElements
bind:block={blockItemData}
{record}
{field}
{schemas}
bind:graph
/>
<BlockButtons
bind:blockData={value}
/>
</div>
{/each}
@@ -0,0 +1,69 @@
<script>
import Icon from "../../common/Icon.svelte";
import {randomId} from "../../../helpers";
export let blockId;
export let blockData;
$: showOptions = false;
let validUis = ["text", "textarea", "rich", "reference"];
function createBlock(e, validUI) {
e.preventDefault();
blockData = [...blockData, {
ui: validUI,
id: randomId(),
key: "",
value: null
}];
showOptions = false;
}
</script>
<button
type="button"
class:is-first={!blockId}
class=" btn btn-lg btn-link text-decoration-none block-buttons"
on:click|preventDefault={(e) => (showOptions = !showOptions)}
>
<Icon width={24} height={24} icon="circle-plus"/>
</button>
{#if showOptions}
<div class="bg-light lx-card d-flex">
{#each validUis as validUi}
<div class="me-2">
<button
class="btn btn-sm btn-primary"
on:click={(e) => createBlock(e, validUi)}
>{validUi}
</button>
</div>
{/each}
</div>
{/if}
<style>
:global(.block-field-wrapper) {
display: flex;
flex-direction: column;
}
:global(.block-field-wrapper .block-buttons) {
visibility: hidden;
}
:global(.block-field-wrapper:hover .block-buttons) {
visibility: visible;
}
.block-buttons {
/* padding: 0 5px; */
display: inline-block;
z-index: 1;
margin: 10px auto 0;
}
</style>
@@ -0,0 +1,44 @@
<script>
import Text from "./elements/Text.svelte";
import Textarea from "./elements/Textarea.svelte";
import Rich from "./elements/Rich.svelte";
import Reference from "./elements/Reference.svelte";
export let record;
export let field;
export let schemas;
export let graph;
export let block;
</script>
<div class="card editor-field bg-light lx-card d-flex">
<span class="text-muted d-block fs-6 mb-1">{block.ui}</span>
{#if block.ui === "text"}
<Text
bind:block={block}
/>
{:else if block.ui === "textarea"}
<Textarea
bind:block={block}
/>
{:else if block.ui === "rich"}
<Rich
bind:block={block}
/>
{:else if block.ui === "reference"}
<Reference
{record}
{field}
{schemas}
bind:graph
bind:block={block}
/>
{/if}
</div>
@@ -0,0 +1,92 @@
<script>
import {uniq, uniqBy} from "lodash";
import PreviewCard from "../../PreviewCard.svelte";
import {sortByField} from "../../../edges/sortEdges";
import ReferenceInlineButtons from "../../elements/ReferenceInlineButtons.svelte"
import Sortable from "../../../libs/Sortable.svelte";
export let block;
export let record;
export let field;
export let schemas;
export let graph;
$: references = graph.edges
.filter((edge) => edge.field === field.name && block.value?.includes(edge.target))
.map((edge) => {
return graph.records.find((increc) => increc.data.id === edge.target && record.data.id === edge.source);
}).filter((rec) => (rec?.data?.id ? true : false)) ?? [];
let collections = schemas.filter((aschema) =>
field.collections.includes(aschema.name)
);
function removeReference(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === field.name)
);
block.value = graph.edges.filter(edge => edge.field === field.name && block.value?.includes(edge.target)).map((edge) => edge.target) ?? [];
}
function reorder(e) {
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, field.name);
}
function insert(e) {
e.preventDefault();
const recordsToInsert = e.detail.records;
const action = e.detail.action;
let newEdges = recordsToInsert.map((r) => {
return {
schema: r._sys.schema,
target: r.data.id,
source: record.data.id,
field: field.name,
rank: ""
};
});
let replacedEdges = graph.edges;
let newBlockValue = [];
if (action === "replace") {
newBlockValue = newEdges.map(edge => edge.target)
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
} else {
newBlockValue = [...block.value ?? [], ...newEdges.map(edge => edge.target)]
}
block.value = uniq(newBlockValue);
graph.records = uniqBy([...graph.records, ...recordsToInsert], (r) => r.data.id);
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.target + edge.field);
}
</script>
<div class="inline-card-wrapper">
<ReferenceInlineButtons
{field}
buttonClass="mt-2"
recordId={null}
schemas={collections}
on:insert={insert}
on:save={insert}
/>
</div>
{#if references.length > 0}
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
{#each references as reference (reference.data.id)}
<div class="col mb-3">
<PreviewCard
classes="h-100"
{schemas}
record={reference}
hasDelete={true}
on:remove={removeReference}
/>
</div>
{/each}
</Sortable>
{/if}
@@ -0,0 +1,10 @@
<script>
import Tinymce from "../../../libs/Tinymce.svelte";
export let block;
let additionalConfig = {};
</script>
<div class="mb-0">
<Tinymce bind:value={block.value} {additionalConfig}/>
</div>
@@ -0,0 +1,13 @@
<script>
export let block;
</script>
<div class="mb-0">
<input
type="text"
id={block.id}
class="form-control"
bind:value={block.value}
autocomplete="off"
/>
</div>
@@ -0,0 +1,39 @@
<script>
import {onMount} from "svelte";
export let block;
let thisEl;
function resize(e) {
let el;
if (e.target) {
el = e.target;
} else {
el = e;
}
el.style.overflow = "hidden";
el.style.height = "1px";
el.style.height = +el.scrollHeight + "px";
}
onMount(() => {
resize(thisEl);
});
</script>
<div class="mb-0">
<textarea
bind:value={block.value}
bind:this={thisEl}
on:input={resize}
id={block.id}
class="form-control"
autocomplete="off"></textarea>
</div>
<style>
textarea {
resize: none;
}
</style>
@@ -0,0 +1,112 @@
<script>
import {createEventDispatcher, getContext} from "svelte";
import Index from "../../content/Index.svelte";
const dispatch = createEventDispatcher();
const channel = getContext("channel");
$: data = {};
let isOpen = false;
let selectedRecords = [];
// onMount(() => {
// load();
// });
export function open(schema) {
isOpen = true;
load(schema);
}
export function close() {
isOpen = false;
selectedRecords = [];
}
function load(schema) {
axios
.get(channel.lucentUrl + "/content/" + schema)
.then((response) => {
data = response.data;
})
.catch((error) => console.log(error));
}
function insert(e) {
e.preventDefault();
dispatch("insert", {
records: selectedRecords,
action: "insert",
});
}
function replace(e) {
e.preventDefault();
dispatch("insert", {
records: selectedRecords,
action: "replace",
});
}
</script>
{#if data.schema}
<div
class="modal fade show"
tabindex="-1"
class:d-block={isOpen}
aria-modal="true"
role="dialog"
style="background: rgba(100,100,100,.6);"
>
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<div class="d-flex align-items-center">
<button
type="button"
class="btn btn-primary me-1"
on:click={insert}
disabled={selectedRecords.length === 0}
>
Insert
</button>
<button
type="button"
class="btn btn-outline-primary me-3"
on:click={replace}
disabled={selectedRecords.length === 0}
>
Replace
</button>
{#if selectedRecords.length > 0}
<span class="">
{selectedRecords.length} records selected
</span>
{/if}
</div>
<button
on:click|preventDefault={(e) => (isOpen = false)}
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
/>
</div>
<div class="modal-body">
<Index {...data} bind:selected={selectedRecords}/>
</div>
</div>
</div>
</div>
{/if}
<style>
.modal-dialog {
width: auto;
max-width: 100%;
}
.modal-content {
margin: 40px auto;
width: auto;
}
</style>
@@ -0,0 +1,56 @@
<script>
import { getErrorMessage } from "./errorMessage";
export let id;
export let field;
export let value;
export let isCreateMode;
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
</script>
<div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
class:is-invalid={errorMessage}
bind:group={value}
id="{id}-1"
value={true}
disabled={field.readonly && !isCreateMode}
/>
<label class="form-check-label" for="{id}-1">Yes</label>
</div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
id="{id}-2"
class:is-invalid={errorMessage}
bind:group={value}
value={false}
disabled={field.readonly && !isCreateMode}
/>
<label class="form-check-label" for="{id}-2">No</label>
</div>
{#if field.nullable}
<div class="form-check form-check-inline">
<input
class="form-check-input"
class:is-invalid={errorMessage}
id="{id}-3"
type="radio"
bind:group={value}
value={null}
disabled={field.readonly && !isCreateMode}
/>
<label class="form-check-label" for="{id}-3">Don't Know</label>
</div>
{/if}
</div>
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
@@ -0,0 +1,37 @@
<script>
import { getErrorMessage } from "./errorMessage";
export let field;
export let value;
export let isCreateMode;
export let validationErrors;
export let id;
$: errorMessage = getErrorMessage(validationErrors, field.name);
</script>
<div class="mb-0">
<div class="input-group ">
<div style="width:64px;">
<input
type="color"
{id}
class="form-control form-control-color"
disabled={field.readonly && !isCreateMode}
bind:value
/>
</div>
<input
type="text"
class:is-invalid={errorMessage}
{id}
class="form-control"
bind:value
readonly={field.readonly && !isCreateMode}
/>
</div>
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -0,0 +1,63 @@
<script>
import {getContext} from "svelte";
import {debounce} from "lodash";
import {previewTitle} from "../Preview";
const channel = getContext("channel");
export let field;
export let value;
export let search;
$: options = [];
export const update = debounce((e) => {
axios
.get("/records/suggestions", {
params: {
schema: field.optionsFrom,
field: field.optionsField,
value: search,
ui: field.ui,
},
})
.then((response) => {
options = response.data;
})
.catch((error) => {
console.log(error);
});
}, 500);
function select(e, option) {
e.preventDefault();
value = option.data[field.optionsField];
search = "";
}
</script>
{#if field.optionsFrom}
{#each options as option (option.id)}
<div on:click={(e) => select(e, option)}>
<span class="dropdown-item">
{previewTitle(channel.schemas, option)}
<small class="text-muted "
>{option.data[field.optionsField]}</small
>
</span>
</div>
{:else}
{#if search && field.optionsSuggest}
<div
on:click={(e) => {
value = search;
search = "";
}}
>
<span class="dropdown-item">
Add "{search}"
</span>
</div>
{:else}
No results
{/if}
{/each}
{/if}
@@ -0,0 +1,113 @@
<script>
import Datalist from "./Datalist.svelte";
import {onMount} from "svelte";
import flatpickr from "flatpickr";
import "flatpickr/dist/flatpickr.css";
import "flatpickr/dist/themes/light.css";
import {getErrorMessage} from "./errorMessage";
import Icon from "../../common/Icon.svelte";
export let field;
export let value;
export let id;
export let isCreateMode;
export let validationErrors;
$: search = "";
$: listMode = field.optionsFrom && !(field.readonly && !isCreateMode);
$: errorMessage = getErrorMessage(validationErrors, field.name);
let list;
let pickerInput;
let pickerInstance;
let flatpickrOptions = {
enableTime: false,
allowInput: true,
dateFormat: "Y-m-d",
};
if (field.min) {
flatpickrOptions.minDate = field.min;
}
if (field.max) {
flatpickrOptions.maxDate = field.max;
}
onMount(() => {
if (!field.readonly || isCreateMode) {
if (listMode) {
flatpickrOptions.clickOpens = false;
}
pickerInstance = flatpickr(pickerInput, flatpickrOptions);
}
});
</script>
<div class="mb-0">
{#if listMode}
<div class="dropdown d-flex">
<input
type="search"
{id}
on:keyup={list.update}
on:focus={list.update}
class="form-control dropdown-toggle"
class:is-invalid={errorMessage}
bind:value={search}
bind:this={pickerInput}
placeholder="Search for options"
data-bs-toggle="dropdown"
autocomplete="off"
aria-expanded="false"
readonly={field.readonly && !isCreateMode}
/>
<button
class="btn btn-light ms-1"
on:click|preventDefault={(e) => pickerInstance.open()}
>
<Icon icon="calendar"/>
</button>
<ul class="dropdown-menu w-100">
{#if field.optionsFrom}
<Datalist
{field}
bind:this={list}
bind:value
bind:search
/>
{/if}
</ul>
</div>
{#if value}
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
<div class="d-flex align-items-center ">
{value}
<button
on:click|preventDefault={(e) => (value = "")}
type="button"
class="btn-close btn-sm ms-1"
style="font-size:10px"
aria-label="Close"
/>
</div>
</span>
{/if}
{:else}
<input
type="text"
{id}
class="form-control"
class:is-invalid={errorMessage}
bind:value
bind:this={pickerInput}
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
{/if}
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -0,0 +1,123 @@
<script>
import Datalist from "./Datalist.svelte";
import {onMount} from "svelte";
import flatpickr from "flatpickr";
import "flatpickr/dist/flatpickr.css";
import "flatpickr/dist/themes/light.css";
import {getErrorMessage} from "./errorMessage";
import Icon from "../../common/Icon.svelte";
export let field;
export let value;
export let schema;
export let isCreateMode;
export let validationErrors;
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
$: search = "";
$: listMode = field.optionsFrom && !(field.readonly && !isCreateMode);
$: errorMessage = getErrorMessage(validationErrors, field.name);
export let id;
let list;
let pickerInput;
let pickerInstance;
let flatpickrOptions = {
enableTime: false,
allowInput: true,
altInput: true,
altFormat: "Y-m-d H:i:S",
dateFormat: "Z",
enableTime: true,
time_24hr: true,
enableSeconds: true,
};
if (field.min) {
flatpickrOptions.minDate = field.min;
}
if (field.max) {
flatpickrOptions.maxDate = field.max;
}
onMount(() => {
if (!field.readonly || isCreateMode) {
if (listMode) {
flatpickrOptions.clickOpens = false;
}
pickerInstance = flatpickr(pickerInput, flatpickrOptions);
}
});
</script>
<div class="mb-0">
{#if listMode}
<div class="dropdown d-flex">
<input
type="search"
{id}
on:keyup={list.update}
on:focus={list.update}
class="form-control dropdown-toggle"
class:is-invalid={errorMessage}
bind:value={search}
bind:this={pickerInput}
placeholder="Search for options"
data-bs-toggle="dropdown"
autocomplete="off"
aria-expanded="false"
readonly={field.readonly && !isCreateMode}
/>
<button
class="btn btn-light ms-1"
on:click|preventDefault={(e) => pickerInstance.open()}
>
<Icon icon="calendar"/>
</button>
<ul class="dropdown-menu w-100">
{#if field.optionsFrom}
<Datalist
{field}
bind:this={list}
bind:value
bind:search
/>
{/if}
</ul>
</div>
{#if value}
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
<div class="d-flex align-items-center ">
{value}
<button
on:click|preventDefault={(e) => (value = "")}
type="button"
class="btn-close btn-sm ms-1"
style="font-size:10px"
aria-label="Close"
/>
</div>
</span>
{/if}
{:else}
<input
type="text"
{id}
class="form-control"
class:is-invalid={errorMessage}
bind:value
bind:this={pickerInput}
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
{/if}
<small class=" text-primary opacity-50"
>Dates are displayed according to your timezone: {timezone}</small
>
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -0,0 +1,27 @@
<script>
import {getContext} from "svelte";
const channel = getContext("channel");
export let field;
export let schema;
export let id;
</script>
<div class="mb-1">
<div class="d-flex justify-content-between">
<div>
<label for={id} class="form-label"
>{field.label}</label
>
{#if field.help}
<small class=" text-primary opacity-50">{field.help}</small>
{/if}
</div>
<a
tabindex="-1"
class="text-decoration-none"
href="{channel.lucentUrl}/schemas/{schema.name}/fields/edit/{field.name}"
><code class="text-primary opacity-50">{field.name}</code>
</a>
</div>
</div>
@@ -0,0 +1,116 @@
<script>
import {getContext} from "svelte";
import {uniqBy} from "lodash";
import {sortByField} from "../../edges/sortEdges";
import PreviewCard from "../PreviewCard.svelte";
import Sortable from "../../libs/Sortable.svelte";
import BrowseModal from "./BrowseModal.svelte";
const channel = getContext("channel");
export let field;
export let record;
export let graph
let browseModal;
$: references = graph?.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id == edge.target && record.id == edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
);
function removeReference(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === field.name)
);
}
function openBrowseModal(e, schema) {
e.preventDefault();
browseModal.open(schema);
}
async function reorder(e) {
graph.edges = await sortByField(e.detail.from, e.detail.to, graph.edges, field.name);
}
function insert(e) {
e.preventDefault();
browseModal.close();
const recordsToInsert = e.detail.records;
const action = e.detail.action;
let newEdges = recordsToInsert.map((r) => {
return {
target: r.id,
source: record.id,
sourceSchema: record._sys.schema,
targetSchema: r._sys.schema,
field: field.name,
rank: ""
};
});
let replacedEdges = graph.edges ?? [];
if (action === "replace") {
replacedEdges = replacedEdges.filter((e) => e.field !== field.name);
}
graph.records = uniqBy([...graph.records, ...recordsToInsert], (r) => r.id);
graph.edges = uniqBy([...replacedEdges, ...newEdges], (e) => e.target + e.field);
}
</script>
<div class="mb-0">
{#if field.collections.length === 1}
<button
class="btn btn-outline-primary"
on:click={(e) => openBrowseModal(e, collections[0].name)}
>
Browse
</button>
{:else}
<div class="dropdown d-inline-block">
<button
class="btn btn-outline-primary btn-sm"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Browse
</button>
<ul class="dropdown-menu">
{#each collections as collection}
<li>
<!-- {`${channelurl}/content/${collection.name}?parent=${record.id}&parentfield=${field.name}`} -->
<a
class="dropdown-item"
on:click={(e) =>
openBrowseModal(e, collection.name)}
href="/">{collection.label}</a
>
</li>
{/each}
</ul>
</div>
{/if}
</div>
{#if references.length > 0}
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
{#each references as reference (reference.id)}
<div class="col mb-3">
<PreviewCard
classes="h-100"
record={reference}
hasDelete={true}
on:remove={removeReference}
/>
</div>
{/each}
</Sortable>
{/if}
<BrowseModal bind:this={browseModal} on:insert={insert}/>
@@ -0,0 +1,24 @@
<script>
import Codemirror from "../../libs/Codemirror.svelte";
import { getErrorMessage } from "./errorMessage";
export let value;
export let field;
export let isCreateMode;
// export let id;
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
</script>
<div class="mb-3">
<Codemirror bind:value editable={!field.readonly || isCreateMode} />
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -0,0 +1,93 @@
<script>
import Datalist from "./Datalist.svelte";
import {getErrorMessage} from "./errorMessage";
export let field;
export let value;
export let schemas;
export let validationErrors;
export let isCreateMode;
export let id;
$: search = "";
$: errorMessage = getErrorMessage(validationErrors, field.name);
let list;
function fixDecimals(e) {
const number = e.currentTarget.value;
const formattedNumber = formatNumber(number);
value = isNaN(formattedNumber) ? null : formattedNumber;
}
function formatNumber(number) {
return parseFloat(number).toFixed(field.decimals);
}
$: listMode = field.optionsFrom && !(field.readonly && !isCreateMode);
</script>
<div class="mb-0">
{#if listMode}
<div class="dropdown">
<input
type="number"
{id}
on:keyup={list.update}
on:focus={list.update}
bind:value={search}
placeholder="Search for options"
class="form-control dropdown-toggle"
class:is-invalid={errorMessage}
data-bs-toggle="dropdown"
autocomplete="off"
aria-expanded="false"
readonly={field.readonly && !isCreateMode}
/>
<ul class="dropdown-menu w-100">
{#if field.optionsFrom}
<Datalist
{field}
bind:this={list}
{schemas}
bind:value
bind:search
/>
{/if}
</ul>
</div>
{#if value}
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
<div class="d-flex align-items-center ">
{value}
<button
on:click|preventDefault={(e) => (value = "")}
type="button"
class="btn-close btn-sm ms-1"
style="font-size:10px"
aria-label="Close"
/>
</div>
</span>
{/if}
{:else}
<input
type="number"
{id}
class="form-control"
class:is-invalid={errorMessage}
on:change={fixDecimals}
bind:value
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
{/if}
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -0,0 +1,92 @@
<script>
import {getContext} from "svelte";
import {uniqBy} from "lodash";
import PreviewCard from "../PreviewCard.svelte";
import {getErrorMessage} from "./errorMessage";
import {sortByField} from "../../edges/sortEdges";
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
import Sortable from "../../libs/Sortable.svelte";
const channel = getContext("channel");
export let record;
export let field;
export let graph;
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
$: references = graph.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id == edge.target && record.id == edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
);
function removeReference(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === field.name)
);
}
function reorder(e) {
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, field.name);
}
function insert(e) {
e.preventDefault();
const recordsToInsert = e.detail.records;
const action = e.detail.action;
let newEdges = recordsToInsert.map((r) => {
return {
target: r.id,
source: record.id,
sourceSchema: record._sys.schema,
targetSchema: r._sys.schema,
field: field.name,
rank: ""
};
});
let replacedEdges = graph.edges;
if (action === "replace") {
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
}
graph.records = uniqBy([...graph.records, ...recordsToInsert], (r) => r.id);
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.target + edge.field);
}
</script>
{#if errorMessage}
<div class="invalid-feedback d-block mb-3">
{errorMessage}
</div>
{/if}
<div class="inline-card-wrapper">
<ReferenceInlineButtons
{field}
buttonClass="mt-2"
recordId={null}
schemas={collections}
on:insert={insert}
on:save={insert}
/>
</div>
{#if references.length > 0}
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
{#each references as reference (reference.id)}
<div class="col mb-3">
<PreviewCard
classes="h-100"
record={reference}
hasDelete={true}
on:remove={removeReference}
/>
</div>
{/each}
</Sortable>
{/if}
@@ -0,0 +1,167 @@
<script>
import {getContext} from "svelte";
import {uniqBy} from "lodash";
import PreviewCardInline from "../PreviewCardInline.svelte";
import {getErrorMessage} from "./errorMessage";
import {sortByField} from "../../edges/sortEdges";
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
import {flip} from "svelte/animate";
import {quintOut} from 'svelte/easing';
const channel = getContext("channel");
export let field;
export let record;
export let graph;
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
$: references = graph.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id == edge.target && record.id == edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
);
function handleInlinesaved(e) {
const updatedRecord = e.detail.records[0];
graph.edges = graph.edges.map((child) => {
if (child.source === updatedRecord.id) {
return updatedRecord;
}
return child;
});
}
function removeReference(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === field.name)
);
}
function trashReference(e) {
e.preventDefault();
graph.edges = graph.edges.map((edge) => {
if (edge.target === e.detail && edge.field === field.name) {
edge._isTrashed = true;
}
return edge;
});
}
function undoRemoveReference(e) {
e.preventDefault();
graph.edges = graph.edges.map((edge) => {
if (edge.target === e.detail && edge.field === field.name) {
delete edge._isTrashed;
}
return edge;
});
}
function insert(e) {
e.preventDefault();
const recordsToInsert = e.detail.records;
const insertAfter = e.detail.after ?? null;
const action = e.detail.action;
let newEdges = recordsToInsert.map((r) => {
return {
target: r.id,
source: record.id,
sourceSchema: record._sys.schema,
targetSchema: r._sys.schema,
field: field.name,
rank: ""
};
});
let replacedEdges = graph.edges;
if (action === "replace") {
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
}
graph.records = uniqBy([...graph.records, ...recordsToInsert], (r) => r.id);
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.target + edge.field);
if (!insertAfter) {
graph.edges = uniqBy(
[...newEdges, ...replacedEdges],
(edge) => edge.target + edge.field
);
} else {
let isAfter = false;
let beforeAfter = replacedEdges.reduce(
(c, edge) => {
if (isAfter) {
c.after.push(edge);
} else {
c.before.push(edge);
}
if (isAfter === false && edge.target === insertAfter) {
isAfter = true;
}
return c;
},
{before: [], after: []}
);
graph.edges = uniqBy(
[...beforeAfter.before, ...newEdges, ...beforeAfter.after],
(e) => e.target + e.field
);
}
}
function move(e, from, to) {
graph.edges = sortByField(from, to, graph.edges, field.name);
}
</script>
{#if errorMessage}
<div class="invalid-feedback d-block mb-3">
{errorMessage}
</div>
{/if}
<div class="inline-card-wrapper">
<ReferenceInlineButtons
{field}
recordId={null}
schemas={collections}
on:insert={insert}
on:save={insert}
/>
</div>
{#if references.length > 0}
{#each references as reference, i (reference.id)}
<div class="inline-card-wrapper" animate:flip="{{delay: 250, duration: 250, easing: quintOut}}">
<PreviewCardInline
isFirst={i === 0}
isLast={i + 1 === references.length}
bind:record={reference}
toDelete={graph.edges.find(
(edge) =>
edge.field === field.name && edge.target === reference.id
)._isTrashed}
on:inlinesaved={handleInlinesaved}
on:moveup={(e) => move(e, i, i - 1)}
on:movedn={(e) => move(e, i, i + 1)}
on:remove={removeReference}
on:undoremove={undoRemoveReference}
on:trash={trashReference}
/>
<ReferenceInlineButtons
{field}
recordId={reference.id}
schemas={collections}
on:insert={insert}
on:save={insert}
/>
</div>
{/each}
{/if}
@@ -0,0 +1,139 @@
<script>
import { createEventDispatcher } from "svelte";
import Icon from "../../common/Icon.svelte";
import InlineEdit from "../InlineEdit.svelte";
import BrowseModal from "./BrowseModal.svelte";
const dispatch = createEventDispatcher();
// export let field;
// export let buttonLabel = "";
// export let buttonClass = "";
export let schemas;
export let recordId;
$: showOptions = false;
let browseModal;
let inLineCreateRecord;
function openBrowseModal(e, schema) {
e.preventDefault();
browseModal.open(schema);
}
function save(e) {
e.preventDefault();
console.log("Save inline");
inLineCreateRecord = null;
dispatch("save", {
records: e.detail.records,
after: recordId,
});
}
function insert(e) {
e.preventDefault();
browseModal.close();
showOptions = false;
dispatch("insert", {
records: e.detail.records,
after: recordId,
});
}
function createInlineReference(e, schemaUId) {
e.preventDefault();
axios
.get("/records/newInline?schema=" + schemaUId)
.then((response) => {
inLineCreateRecord = response.data;
showOptions = false;
})
.catch((error) => {
console.log(error);
});
}
</script>
{#if schemas.length > 1}
<button
type="button"
class:is-first={!recordId}
class=" btn btn-lg btn-link text-decoration-none inline-card-button"
on:click|preventDefault={(e) => (showOptions = !showOptions)}
>
<Icon width={24} height={24} icon="circle-plus" />
</button>
{#if showOptions}
<div class="bg-light lx-card d-flex">
{#each schemas as schema}
<div
class="lx-card p-4 text-center me-4"
style="max-width: 250px;"
>
<p>{schema.label}</p>
<div class="mb-2">
<button
class="btn btn-sm btn-primary"
on:click={(e) =>
createInlineReference(e, schema.name)}
>New
</button>
<button
class="btn btn-sm btn-outline-primary"
on:click={(e) => openBrowseModal(e, schema.name)}
><Icon icon="magnifying-glass" /></button
>
</div>
</div>
{/each}
</div>
{/if}
{:else}
<div class="pb-2 text-start">
<div class="mb-2">
<button
class="btn btn-sm btn-primary"
on:click={(e) => createInlineReference(e, schemas[0].name)}
>New
</button>
<button
class="btn btn-sm btn-outline-primary"
on:click={(e) => openBrowseModal(e, schemas[0].name)}
><Icon icon="magnifying-glass" /></button
>
</div>
</div>
{/if}
{#if inLineCreateRecord}
<InlineEdit
{...inLineCreateRecord}
on:cancel={(e) => (inLineCreateRecord = null)}
on:inlinesaved={save}
/>
{/if}
<BrowseModal bind:this={browseModal} on:insert={insert} />
<style>
:global(.inline-card-wrapper) {
display: flex;
flex-direction: column;
}
:global(.inline-card-wrapper .inline-card-button) {
visibility: hidden;
}
:global(.inline-card-wrapper .inline-card-button.is-first) {
visibility: visible;
}
:global(.inline-card-wrapper:hover .inline-card-button) {
visibility: visible;
}
.inline-card-button {
/* padding: 0 5px; */
display: inline-block;
z-index: 1;
margin: 10px auto 0;
}
</style>
@@ -0,0 +1,155 @@
<script>
import {getContext} from "svelte";
import {uniqBy} from "lodash";
import {previewTitle} from "../Preview";
import {getErrorMessage} from "./errorMessage";
import {sortByField} from "../../edges/sortEdges";
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
import Sortable from "../../libs/Sortable.svelte";
import RenderField from "../../content/RenderField.svelte";
import Icon from "../../common/Icon.svelte";
const channel = getContext("channel");
export let field;
export let record;
export let graph;
export let schema;
export let children;
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
$: references = graph.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id == edge.target && record.id == edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
);
let collection = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
)[0];
function removeReference(e, recordId) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === recordId && edge.field === field.name)
);
}
function reorder(e) {
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, field.name);
}
function insert(e) {
e.preventDefault();
const recordsToInsert = e.detail.records;
const action = e.detail.action;
let newEdges = recordsToInsert.map((r) => {
return {
target: r.id,
source: record.id,
sourceSchema: record._sys.schema,
targetSchema: r._sys.schema,
field: field.name,
rank: ""
};
});
let replacedEdges = graph.edges;
if (action === "replace") {
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
}
graph.records = uniqBy([...graph.records, ...recordsToInsert], (r) => r.id);
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.target + edge.field);
}
$:visibleColumns = [];
// $: visibleColumns = collection.fields
// .filter((f) => f.ui !== "tab" && !f.trashed)
// .filter((f) => {
// return collection.visible.includes(f.name);
// });
</script>
{#if errorMessage}
<div class="invalid-feedback d-block mb-3">
{errorMessage}
</div>
{/if}
<div class="inline-card-wrapper">
<ReferenceInlineButtons
{field}
buttonClass="mt-2"
recordId={null}
schemas={collections}
on:insert={insert}
on:save={insert}
/>
</div>
{#if references.length > 0}
<div class="lx-table rounded">
<table class="">
<thead class="table-light">
<tr>
<th/>
{#each visibleColumns as field}
<th
class="field-ui-{field.ui}"
scope="col"
title={field.help}
data-bs-toggle="tooltip"
data-bs-placement="top">{field.label}</th
>
{/each}
<th/>
</tr>
</thead>
<Sortable isTable={true} on:update={reorder}>
{#each references as record (record.id)}
<tr>
<td class="">
<div class="">
<div class="d-flex align-items-center">
<a
class="me-2 text-decoration-none text-dark fs-6"
href="/records/{record.id}"
target="_blank"
>
{previewTitle(channel.schemas, record)}
</a>
</div>
</div>
</td>
{#each visibleColumns as field, index}
<td class="field-ui-{field.ui}">
<RenderField
{record}
{graph}
schema={collection}
{field}
/>
</td>
{/each}
<td>
<button
class="trash-button text-dark btn btn-sm btn-link"
on:click={(e) =>
removeReference(e, record.id)}
>
<Icon icon="trash-can"/>
</button>
</td>
</tr>
{/each}
</Sortable>
</table>
</div>
{/if}
@@ -0,0 +1,24 @@
<script>
import Tinymce from "../../libs/Tinymce.svelte";
import {getErrorMessage} from "./errorMessage";
export let value;
export let field;
export let isCreateMode;
export let schema;
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
let additionalConfig = {
readonly: field.readonly && !isCreateMode,
};
</script>
<div class="mb-0">
<Tinymce bind:value {additionalConfig} {schema}/>
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -0,0 +1,78 @@
<script>
import Datalist from "./Datalist.svelte";
import {getErrorMessage} from "./errorMessage";
export let field;
export let value;
export let isCreateMode;
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
$: search = "";
export let id;
let list;
$: listMode = field.optionsFrom && !(field.readonly && !isCreateMode);
</script>
<div class="mb-0">
{#if listMode}
<div class="dropdown">
<input
type="search"
{id}
on:keyup={list.update}
on:focus={list.update}
class="form-control dropdown-toggle"
class:is-invalid={errorMessage}
bind:value={search}
placeholder="Search for options"
data-bs-toggle="dropdown"
autocomplete="off"
aria-expanded="false"
readonly={field.readonly && !isCreateMode}
/>
<div class="dropdown-menu w-100">
{#if field.optionsFrom}
<Datalist
{field}
bind:this={list}
bind:value
bind:search
/>
{/if}
</div>
</div>
{#if value}
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
<div class="d-flex align-items-center ">
{value}
<button
on:click|preventDefault={(e) => (value = "")}
type="button"
class="btn-close btn-sm ms-1"
style="font-size:10px"
aria-label="Close"
/>
</div>
</span>
{/if}
{:else}
<input
type="text"
{id}
class="form-control"
class:is-invalid={errorMessage}
bind:value
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
{/if}
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -0,0 +1,53 @@
<script>
import { onMount } from "svelte";
import { getErrorMessage } from "./errorMessage";
export let field;
export let value;
export let isCreateMode;
export let validationErrors;
let thisEl;
$: errorMessage = getErrorMessage(validationErrors, field.name);
export let id;
function resize(e) {
let el;
if (e.target) {
el = e.target;
} else {
el = e;
}
el.style.overflow = "hidden";
el.style.height = "1px";
el.style.height = +el.scrollHeight + "px";
}
onMount(() => {
resize(thisEl);
});
</script>
<div class="mb-0">
<textarea
bind:value
bind:this={thisEl}
{id}
class="form-control"
on:input={resize}
on:focus={resize}
rows="2"
class:is-invalid={errorMessage}
readonly={field.readonly && !isCreateMode}
/>
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
<style>
textarea {
resize: none;
}
</style>
@@ -0,0 +1,49 @@
<script>
import { v4 as uuidv4 } from "uuid";
import { getContext } from "svelte";
import Icon from "../../common/Icon.svelte";
import { getErrorMessage } from "./errorMessage";
const channelurl = getContext("channelurl");
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
export let field;
export let value;
export let id;
export let isCreateMode;
let readonly = field.readonly && !isCreateMode;
function generateId(e) {
e.preventDefault();
value = uuidv4();
}
</script>
<div class="mb-0">
<div class="d-flex justify-content-between">
<input
type="text"
{id}
class="form-control"
class:is-invalid={errorMessage}
bind:value
autocomplete="off"
{readonly}
/>
{#if !readonly}
<button
class="btn btn-primary ms-2"
title="Generate a new UUIDv4"
on:click={generateId}
>
<Icon icon="dice" />
</button>
{/if}
</div>
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -0,0 +1,30 @@
<script>
import { uniqueId } from "lodash";
import { getContext } from "svelte";
const channelurl = getContext("channelurl");
export let field;
export let value;
export let schema;
let id = uniqueId();
</script>
<div class="mb-0">
<div class="d-flex justify-content-between">
<label for={id} class="form-label">{field.label}</label>
<a
class="text-decoration-none"
href="{channelurl}/schemas/{schema.name}/fields/edit/{field.name}"
><code class="text-primary opacity-50">{field.name}</code></a
>
</div>
<input
type="url"
{id}
class="form-control"
bind:value
placeholder="https://www.example.com"
/>
{#if field.help}
<small class=" text-primary opacity-50">{field.help}</small>
{/if}
</div>
@@ -0,0 +1,5 @@
export function getErrorMessage(validationErrors, fieldName) {
return validationErrors && validationErrors[fieldName]
? validationErrors[fieldName].message
: null;
}
@@ -0,0 +1,51 @@
<script>
import Preview from "../../files/Preview.svelte";
import PreviewCardSmall from "../PreviewCardSmall.svelte";
export let field;
export let side;
export let edges;
export let colorClass;
</script>
{#if ["reference", "file"].includes(field.ui)}
<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}
{/each}
</div>
</div>
{:else if field.ui === "json"}
<div class="{colorClass} field-content" style="white-space: break-spaces;">
{JSON.stringify(side, null, 2) ?? ""}
</div>
{:else if field.ui === "rich"}
<div class="{colorClass} field-content">{@html side ?? ""}</div>
{:else}
<div class="{colorClass} field-content">{JSON.stringify(side) ?? ""}</div>
{/if}
<!-- {/if} -->
<style>
.field-content {
max-height: 200px;
overflow-y: scroll;
}
</style>