init
This commit is contained in:
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script>
|
||||
</script>
|
||||
|
||||
|
||||
<div class="wrapper-normal ">
|
||||
<div class="header-normal">
|
||||
Record Not Found
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user