refactor edit and edges

This commit is contained in:
2024-03-25 21:26:21 +02:00
parent e74e1e7956
commit 02224eb580
83 changed files with 3569 additions and 818 deletions
@@ -0,0 +1,22 @@
<script>
export let field;
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>
<span
tabindex="-1"
class="text-decoration-none"
><code class="text-primary opacity-50">{field.name}</code>
</span>
</div>
</div>
+122
View File
@@ -0,0 +1,122 @@
<script>
import {afterUpdate, createEventDispatcher, onMount} from "svelte";
import {isEqual} from "../deepEquality.js";
import ContentTabs from "../ContentTabs.svelte"
import ErrorAlert from "../../common/ErrorAlert.svelte"
import FormField from "./FormField.svelte";
import ReferenceField from "./ReferenceField.svelte";
import SaveButtons from "./SaveButtons.svelte";
import EditHeader from "../EditHeader.svelte";
const dispatch = createEventDispatcher();
function save(){
dispatch("save");
}
export let title = null;
export let schema;
export let record;
export let data;
export let status = null;
export let graph = [];
export let isCreateMode;
let originalContent;
let activeContentTab = "";
$: hasUnsavedData = false;
$: validationErrors = null;
$: errorMessage = validationErrors
? `Record submission failed. ${
Object.entries(validationErrors).length
} error(s)`
: null;
export function setOriginalData(){
originalContent = {
data: JSON.parse(JSON.stringify(data)),
status: status,
edges: JSON.parse(JSON.stringify(graph.map(r => r.edge))),
};
hasUnsavedData = checkUnsavedData();
}
onMount(()=>{
setOriginalData()
})
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: data,
status: status,
edges: graph.map(r => r.edge),
});
}
</script>
<svelte:window on:beforeunload={beforeUnload}/>
<div >
<EditHeader {schema} {record} {isCreateMode} {title}/>
<div class=" mt-4" style="margin-bottom:150px">
<SaveButtons on:save={save} status={status} {hasUnsavedData} />
<ErrorAlert message={errorMessage}/>
<ContentTabs
{schema}
bind:active={activeContentTab}
/>
<!-- <fieldset disabled="disabled"> -->
{#each schema.fields as field (field.name)}
{#if activeContentTab === field.group}
{#if ["reference", "file"].includes(field.info.name)}
<ReferenceField
bind:graph={graph}
{field}
{record}
/>
{:else}
<FormField
bind:data={data}
{field}
{validationErrors}
{isCreateMode}
/>
{/if}
{/if}
{/each}
<!-- </fieldset> -->
</div>
</div>
@@ -0,0 +1,50 @@
<script>
import Text from "./fields/Text.svelte";
import Slug from "./fields/Slug.svelte";
import Color from "./fields/Color.svelte";
import Checkbox from "./fields/Checkbox.svelte";
import Number from "./fields/Number.svelte";
import Date from "./fields/Date.svelte";
import UUID from "./fields/UUID.svelte";
import Textarea from "./fields/Textarea.svelte";
import Datetime from "./fields/Datetime.svelte";
import RichEditor from "./fields/RichEditor.svelte";
import Json from "./fields/JSON.svelte";
import Markdown from "./fields/Markdown.svelte";
import FieldHeader from "./FieldHeader.svelte";
const formElements = {
text: Text,
slug: Slug,
textarea: Textarea,
rich: RichEditor,
color: Color,
checkbox: Checkbox,
number: Number,
date: Date,
datetime: Datetime,
uuid: UUID,
json: Json,
markdown: Markdown,
};
export let field;
export let data;
export let validationErrors;
export let isCreateMode;
let formElement = formElements[field.info.name];
const uniqueId = `field-${field.name}-id`;
</script>
<div class="card editor-field">
<FieldHeader {field} id={uniqueId}/>
<svelte:component
this={formElement}
bind:value={data[field.name]}
{field}
{validationErrors}
{isCreateMode}
id={uniqueId}
/>
</div>
@@ -0,0 +1,17 @@
<script>
import File from "./references/Reference.svelte";
import FieldHeader from "./FieldHeader.svelte";
export let field;
export let record;
export let graph;
// export let validationErrors;
// export let isCreateMode;
const id = `field-${field.name}-${record.id}`;
</script>
<div class="card editor-field">
<FieldHeader {field} {id}/>
<File bind:graph {record} {field} />
</div>
@@ -0,0 +1,47 @@
<script>
import StatusSelect from "./StatusSelect.svelte"
import {createEventDispatcher} from "svelte";
export let status;
export let isCreateMode;
export let hasUnsavedData;
const dispatch = createEventDispatcher();
function save(){
dispatch("save");
}
</script>
<div class="record-status-bar">
<div
class="d-flex mt-3 mb-3 align-items-center justify-content-center"
>
<StatusSelect bind:status={status}/>
{#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>
@@ -0,0 +1,47 @@
<script>
import {getStatus, getStatusList} from "../StatusText.js";
export let status = "draft";
let dropdown;
$: currentStatus = getStatus(status);
const statusList = Object.values(getStatusList());
function updateStatus(e, statusValue) {
// e.preventDefault();
status = statusValue;
dropdown.click();
}
</script>
{#if status}
<!-- Example split danger button -->
<div class="d-flex justify-content-between">
<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>
</div>
{/if}
@@ -0,0 +1,56 @@
<script>
import { getErrorMessage } from "../form.js";
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 "../form.js";
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,70 @@
<script>
import {getContext} from "svelte";
import {debounce} from "lodash";
import {previewTitle} from "../../Preview.js";
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)}
on:keypress={(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 = "";
}}
on:keypress={(e) => {
value = search;
search = "";
}}
>
<span class="dropdown-item">
Add "{search}"
</span>
</div>
{:else}
No results
{/if}
{/each}
{/if}
@@ -0,0 +1,112 @@
<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 "../form.js";
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"
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,121 @@
<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 "../form.js";
import Icon from "../../../common/Icon.svelte";
export let field;
export let value;
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"
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,24 @@
<script>
import Codemirror from "../../../libs/Codemirror.svelte";
import { getErrorMessage } from "../form.js";
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,24 @@
<script>
import Codemirror from "../../../libs/CodemirrorMarkdown.svelte";
import { getErrorMessage } from "../form.js";
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,92 @@
<script>
import Datalist from "./Datalist.svelte";
import { getErrorMessage } from "../form.js";
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"
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,24 @@
<script>
import Tinymce from "../../../libs/Tinymce.svelte";
import { getErrorMessage } from "../form.js";
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,39 @@
<script>
export let field;
export let value;
export let search;
function select(e, option) {
e.preventDefault();
value = option;
search = "";
}
</script>
{#if field.selectOptions}
{#if Array.isArray(field.selectOptions)}
{#each field.selectOptions as suggestion (suggestion)}
<div
on:click={(e) => select(e, suggestion)}
on:keypress={(e) => select(e, suggestion)}
>
<span class="dropdown-item">
{suggestion}
</span>
</div>
{/each}
{:else}
{#each Object.entries(field.selectOptions) as [k, v] (k)}
<div
on:click={(e) => select(e, k)}
on:keypress={(e) => select(e, k)}
>
<span class="dropdown-item">
{v}
</span>
</div>
{/each}
{/if}
{/if}
@@ -0,0 +1,30 @@
<script>
import { getErrorMessage } from "../form.js";
export let field;
export let value;
export let isCreateMode;
export let validationErrors;
let thisEl;
$: errorMessage = getErrorMessage(validationErrors, field.name);
export let id;
</script>
<div class="mb-0">
<input
type="text"
{id}
class="form-control"
class:is-invalid={errorMessage}
bind:value
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -0,0 +1,122 @@
<script>
import Datalist from "./Datalist.svelte";
import Selectlist from "./Selectlist.svelte";
import { getErrorMessage } from "../form.js";
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"
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 if field.selectOptions}
<div class="dropdown">
<input
type="search"
{id}
class="form-control dropdown-toggle"
class:is-invalid={errorMessage}
bind:value={search}
placeholder="Search for options"
data-bs-toggle="dropdown"
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
<div class="dropdown-menu w-100">
<Selectlist
{field}
bind:value
bind:search
/>
</div>
</div>
{#if value}
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
<div class="d-flex align-items-center ">
{#if Array.isArray(field.selectOptions)}
{value}
{:else}
{field.selectOptions[value]}
{/if}
<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 "../form.js";
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 "../form.js";
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>
+3
View File
@@ -0,0 +1,3 @@
export function getErrorMessage(validationErrors, fieldName) {
return validationErrors?.find((e) => e.fieldName === fieldName)?.message;
}
@@ -0,0 +1,114 @@
<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;
height: 100%;
}
</style>
@@ -0,0 +1,36 @@
<script>
import {getContext} from "svelte";
import Form from "../Form.svelte";
import OffCanvas from "../../../common/OffCanvas.svelte";
export let field;
export let edge;
let form;
let offCanvas;
const channel = getContext("channel");
let schema = channel.schemas.find(s => s.name === field.data);
export function openEdit() {
offCanvas.show();
}
function save(e){
e.preventDefault();
console.log("yo")
}
</script>
<OffCanvas bind:this={offCanvas}>
<Form
bind:this={form}
data={edge.data}
{schema}
isCreateMode={false}
on:save={save}
/>
</OffCanvas>
@@ -0,0 +1,98 @@
<script>
import {getContext} from "svelte";
import PreviewCard from "../../PreviewCard.svelte";
import Sortable from "../../../libs/Sortable.svelte";
import BrowseModal from "./BrowseModal.svelte";
import {insertEdges} from "./reference.js";
import {sortByField} from "../../../edges/sortEdges.js";
const channel = getContext("channel");
export let field;
export let record;
export let graph;
let browseModal;
$: currentReferences = graph.filter((queryRecord)=> field.name === queryRecord.edge.field) ?? [];
let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
);
function removeReference(e) {
e.preventDefault();
graph = graph.filter(
(queryRecord) => !(queryRecord.edge.target === e.detail && queryRecord.edge.field === field.name)
);
}
function openBrowseModal(e, schema) {
e.preventDefault();
browseModal.open(schema);
}
async function reorder(e) {
graph = await sortByField(e.detail.source, e.detail.target, graph, field.name);
}
function insert(e) {
e.preventDefault();
browseModal.close();
const recordsToInsert = e.detail.records;
const action = e.detail.action;
graph = insertEdges(graph, record, recordsToInsert, field.name, action);
}
</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 currentReferences.length > 0}
<Sortable sortableClass="mt-3" on:update={reorder}>
{#each currentReferences as reference (reference.record.id)}
<div class="mb-1">
<PreviewCard
record={reference.record}
hasDelete={true}
editable={!!field?.data}
{field}
edge={reference.edge}
on:remove={removeReference}
/>
</div>
{/each}
</Sortable>
{/if}
<BrowseModal bind:this={browseModal} on:insert={insert}/>
@@ -0,0 +1,161 @@
<script>
import {getContext} from "svelte";
import {uniqBy, debounce} from "lodash";
import {previewTitle} from "../../Preview.js";
import {getErrorMessage} from "../errorMessage.js";
import {sortByField} from "../../../edges/sortEdges.js";
import ReferenceInlineButtons from "../../elements/ReferenceInlineButtons.svelte";
import Sortable from "../../../libs/Sortable.svelte";
import RenderField from "../../../content/RenderField.svelte";
import Icon from "../../../common/Icon.svelte";
import Datalist from "../fields/Datalist.svelte";
import {insertEdges} from "./reference.js";
const channel = getContext("channel");
export let field;
export let id;
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 search = ""
$: searchOptions = []
function removeReference(e, recordId) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === recordId && edge.field === field.name)
);
}
function saveNew(e, newValue) {
e.preventDefault();
axios
.post(channel.lucentUrl + "/records", {
isCreateMode: true,
record: {
schema: field.collections[0],
status: "published",
data: {
[field.searchField]: newValue
}
},
})
.then((response) => {
searchOptions = [];
insert(e,response.data.records[0]);
console.log(response)
})
.catch((error) => {
searchOptions = [];
console.log(error);
});
}
function insert(e, insertRecord) {
e.preventDefault();
graph = insertEdges(graph,record,[insertRecord],field.name,e.detail.action);
}
const updateResults = debounce((e) => {
axios
.get(channel.lucentUrl + "/records/suggestions", {
params: {
schema: field.collections[0],
field: field.searchField,
value: search,
ui: "text",
},
})
.then((response) => {
searchOptions = response.data;
})
.catch((error) => {
searchOptions = [];
console.log(error);
});
}, 500);
</script>
{#if errorMessage}
<div class="invalid-feedback d-block mb-3">
{errorMessage}
</div>
{/if}
<input
type="search"
{id}
on:keyup={updateResults}
class="form-control dropdown-toggle"
class:is-invalid={errorMessage}
bind:value={search}
placeholder={"Search for "+field.label}
data-bs-toggle="dropdown"
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
<div class="dropdown-menu w-100">
{#if searchOptions}
{#each searchOptions as option (option.id)}
<div
on:click={(e) => insert(e, option)}
on:keypress={(e) => insert(e, option)}
>
<span class="dropdown-item">
{previewTitle(channel.schemas, option)}
</span>
</div>
{:else}
Start typing...
{/each}
{/if}
{#if search }
<div
on:click={(e) => saveNew(e,search)}
on:keypress={(e) => saveNew(e,search)}
>
<span class="dropdown-item">
Add "{search}"
</span>
</div>
{/if}
</div>
{#if references.length > 0}
<div class="d-flex">
{#each references as record (record.id)}
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
<div class="d-flex align-items-center ">
{previewTitle(channel.schemas, record)}
<button
on:click|preventDefault={(e) => removeReference(e, record.id)}
type="button"
class="btn-close btn-sm ms-1"
style="font-size:10px"
aria-label="Close"
/>
</div>
</span>
{/each}
</div>
{/if}
@@ -0,0 +1,23 @@
import {uniqBy} from "lodash";
export function insertEdges(existingRecords, sourceRecord, targetRecords, fieldName, action = "") {
let newQueryRecords = targetRecords.map((r) => {
return {
record: r,
edge: {
target: r.id,
source: sourceRecord.id,
sourceSchema: sourceRecord.schema,
targetSchema: r.schema,
field: fieldName,
rank: ""
}
};
});
if (action === "replace") {
existingRecords = existingRecords.filter(queryRecord => queryRecord.edge.field !== fieldName)
}
existingRecords = [...existingRecords ?? [], ...newQueryRecords];
return uniqBy(existingRecords, (r) => r.record.id);
}