refactor edit and edges
This commit is contained in:
+2
-2
@@ -11,12 +11,12 @@
|
||||
"php": "^8.2",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"intervention/image": "^2.7",
|
||||
"phpoption/phpoption": "^1.9",
|
||||
"spatie/image-optimizer": "^1.6",
|
||||
"staudenmeir/laravel-cte": "^1.0",
|
||||
"ext-pdo": "*",
|
||||
"opis/json-schema": "^2.3",
|
||||
"symfony/yaml": "^7.0"
|
||||
"symfony/yaml": "^7.0",
|
||||
"spatie/laravel-data": "^4.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.8",
|
||||
|
||||
Generated
+1637
-1
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
<script>
|
||||
|
||||
import {onMount} from "svelte";
|
||||
import offcanvas from "bootstrap/js/src/offcanvas.js";
|
||||
export let title = "";
|
||||
let isHidden = true;
|
||||
let offcanvasEl;
|
||||
let offcanvasInstance;
|
||||
|
||||
export function show() {
|
||||
offcanvasInstance.show();
|
||||
}
|
||||
onMount(()=>{
|
||||
offcanvasInstance = new offcanvas(offcanvasEl);
|
||||
});
|
||||
|
||||
export function hide(e) {
|
||||
e.preventDefault();
|
||||
offcanvasInstance.hide();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={offcanvasEl} class="offcanvas offcanvas-end" data-bs-backdrop="static" tabindex="-1"
|
||||
aria-labelledby="offcanvasEditContent">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title">{title}</h5>
|
||||
<button type="button" on:click={hide} class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body" style="overflow: auto">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +1,10 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import Preview from "../files/Preview.svelte";
|
||||
import {selectRecord} from "./functions/recordSelect.js";
|
||||
import Preview from "../newPreview/Preview.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
|
||||
export let schema;
|
||||
export let records;
|
||||
export let isWritable;
|
||||
export let selected = [];
|
||||
@@ -17,42 +16,42 @@
|
||||
|
||||
</script>
|
||||
<div class="row" style="max-width:1000px">
|
||||
{#each records as record (record.id)}
|
||||
{#each records as queryRecord (queryRecord.record.id)}
|
||||
<div class="col-6 col-md-4">
|
||||
<div
|
||||
class="file-wrapper rounded p-2 mb-4 bg-light"
|
||||
class:selected={selected.includes(record)}
|
||||
class:selected={selected.includes(queryRecord)}
|
||||
>
|
||||
{#if isWritable}
|
||||
<div class="form-check">
|
||||
<input
|
||||
on:change={() => select(record)}
|
||||
on:change={() => select(queryRecord.record)}
|
||||
class="form-check-input "
|
||||
type="checkbox"
|
||||
checked={selected.find(
|
||||
(r) => r.id === record.id
|
||||
(r) => r.id === queryRecord.record.id
|
||||
)}
|
||||
value={record}
|
||||
value={queryRecord}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="d-flex justify-content-center">
|
||||
<Preview {record} size="medium"/>
|
||||
<Preview record={queryRecord.record} />
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
title={record._file.path}
|
||||
href="{channel.lucentUrl}/records/{queryRecord.record.id}"
|
||||
title={queryRecord.record._file.path}
|
||||
class="d-block text-center overflow-hidden text-nowrap my-2 "
|
||||
style="
|
||||
text-overflow: ellipsis;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
">{record._file.path}</a
|
||||
">{queryRecord.record._file.path}</a
|
||||
>
|
||||
<span
|
||||
class="lx-small-text text-muted d-block text-center"
|
||||
>{record._file.mime}</span
|
||||
>{queryRecord.record._file.mime}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
{#if isWritable}
|
||||
<div class="form-check">
|
||||
<input
|
||||
on:change={() => select(queryRecord)}
|
||||
on:change={() => select(queryRecord.record)}
|
||||
class="form-check-input "
|
||||
type="checkbox"
|
||||
checked={selected.find(
|
||||
@@ -89,11 +89,11 @@
|
||||
class="me-2 text-decoration-none text-dark fs-6"
|
||||
href="{channel.lucentUrl}/records/{queryRecord.record.id}"
|
||||
target={inModal ? "_blank" : "_self"}
|
||||
title={previewTitle(queryRecord)}
|
||||
title={previewTitle(queryRecord.record)}
|
||||
data-bs-toggle="tooltip" data-bs-placement="left"
|
||||
|
||||
>
|
||||
{previewTitle(queryRecord)}
|
||||
{previewTitle(queryRecord.record)}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export function sortByField(from, to, edges, fieldName) {
|
||||
export function sortByField(from, to, queryRecords, fieldName) {
|
||||
if (from === to) {
|
||||
return edges;
|
||||
return queryRecords;
|
||||
}
|
||||
let edgesTosort = edges?.filter((ed) => ed.field === fieldName && ed.depth === 1 ) ?? [];
|
||||
let remainingEdge = edges?.filter((ed) => !(ed.field === fieldName && ed.depth === 1)) ?? [];
|
||||
let edgesTosort = queryRecords?.filter((qr) => qr.edge.field === fieldName && qr.edge.depth === 1 ) ?? [];
|
||||
let remainingEdge = queryRecords?.filter((qr) => !(qr.edge.field === fieldName && qr.edge.depth === 1)) ?? [];
|
||||
|
||||
edgesTosort = array_move(edgesTosort,from, to);
|
||||
return [...remainingEdge, ...edgesTosort];
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export let sortableClass = "";
|
||||
// export let handle;
|
||||
export let isTable = false;
|
||||
export let sortableInstance;
|
||||
export let sortableInstance = null;
|
||||
const dispatch = createEventDispatcher();
|
||||
let sortableContainer;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
easing: "cubic-bezier(1, 0, 0, 1)",
|
||||
onUpdate: function (/**Event*/ evt) {
|
||||
// reorder(evt.oldIndex,evt.newIndex);
|
||||
console.log(evt)
|
||||
// console.log(evt)
|
||||
dispatch("update", {
|
||||
source: evt.oldIndex,
|
||||
target: evt.newIndex,
|
||||
|
||||
@@ -1,61 +1,65 @@
|
||||
<script>
|
||||
import { getContext, createEventDispatcher } from "svelte";
|
||||
import PreviewEdge from "./preview/PreviewEdge.svelte";
|
||||
import PreviewRecord from "./preview/PreviewRecord.svelte";
|
||||
import Icon from "../common/Icon.svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
import File from "./includes/File.svelte";
|
||||
import {previewTitle} from "../records/Preview.js";
|
||||
import {getContext} from "svelte";
|
||||
import Status from "../records/Status.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let classes = "";
|
||||
export let hasDelete = false;
|
||||
export let record
|
||||
export let edge
|
||||
export let graph
|
||||
export let field
|
||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||
export let record;
|
||||
|
||||
function remove(e) {
|
||||
e.preventDefault();
|
||||
dispatch("remove", record.id);
|
||||
export let edge = null;
|
||||
let schema = channel.schemas.find(s => s.name === record.schema);
|
||||
let types = ["inline", "card"];
|
||||
export let type = "inline";
|
||||
if (!types.includes(type)) {
|
||||
console.error("unknown preview type")
|
||||
}
|
||||
export let editable = false;
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<!-- Preview Edge-->
|
||||
<div
|
||||
|
||||
class="card mb-2 bg-light {classes}"
|
||||
style="border-color:{schema.color ?? '#ccc'}; border-width: 1px;"
|
||||
>
|
||||
<div class="card-body d-flex flex-md-column">
|
||||
{#if field.data}
|
||||
<PreviewEdge {record} {edge} {graph} {field} {classes}/>
|
||||
{:else}
|
||||
<PreviewRecord {record} {graph} {classes} />
|
||||
{/if}
|
||||
|
||||
</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>
|
||||
<div class="preview-card">
|
||||
{#if edge?.data}
|
||||
<div class="preview-card-edge">Edge Data</div>
|
||||
{/if}
|
||||
<div class="d-flex column-gap-3">
|
||||
{#if record._file}
|
||||
<div>
|
||||
<File {record}/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="d-flex flex-md-column " style="line-height: 22px">
|
||||
<span class="">{previewTitle(record)}</span>
|
||||
<span class="d-flex gap-1 text-muted">
|
||||
{#if record.status === "draft"}
|
||||
<Status status={record.status}/>
|
||||
{/if}
|
||||
{schema.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.card .trash-button {
|
||||
display: none;
|
||||
.preview-card {
|
||||
position: relative
|
||||
}
|
||||
|
||||
.card:hover .trash-button {
|
||||
display: block;
|
||||
.preview-card-edge {
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
background: #fff;
|
||||
padding: 0px 5px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 7px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
<!--{#if record._file && type === "inline"}-->
|
||||
<!--<!– <FilePreviewInline {record} {edge} {editable}/>–>-->
|
||||
<!--{:else if record._file && type === "card"}-->
|
||||
<!-- <FilePreviewCard {record} {edge} {editable}/>-->
|
||||
<!--{:else if type === "inline"}-->
|
||||
<!--<!– <DocPreviewCard {record} {edge} {editable}/>–>-->
|
||||
<!--{:else if type === "card"}-->
|
||||
<!--<!– <DocPreviewCard {record} {edge} {editable}/>–>-->
|
||||
<!--{/if}-->
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<script>
|
||||
import {imgurl} from "../../files/imageserver.js";
|
||||
import {getContext} from "svelte";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
const channel = getContext("channel");
|
||||
export let record;
|
||||
|
||||
|
||||
export let size = "tiny";
|
||||
let imageSide;
|
||||
let fileSide;
|
||||
let fontSize;
|
||||
if (size === "large") {
|
||||
imageSide = 256;
|
||||
fileSide = 32;
|
||||
fontSize = "20";
|
||||
} else if (size === "medium") {
|
||||
imageSide = 128;
|
||||
fileSide = 12;
|
||||
fontSize = "17";
|
||||
} else if (size === "small") {
|
||||
imageSide = 64;
|
||||
fileSide = 12;
|
||||
fontSize = "15";
|
||||
} else if (size === "tiny") {
|
||||
imageSide = 42;
|
||||
fileSide = 12;
|
||||
fontSize = "13";
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if record}
|
||||
{#if record._file.mime.startsWith("image")}
|
||||
<!-- href={imgurl(record)} -->
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
title={record._file.path}
|
||||
class="d-flex align-items-center justify-content-center "
|
||||
style="width:{imageSide}px;height:{imageSide}px"
|
||||
>
|
||||
<img
|
||||
class="rounded w-100"
|
||||
src={imgurl(record)}
|
||||
alt={record._file.path}
|
||||
/>
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
title={record._file.path}
|
||||
class="btn btn-outline-primary btn-sm d-flex align-items-center justify-content-center"
|
||||
style="width:{imageSide}px;height:{imageSide}px"
|
||||
>
|
||||
<Icon icon="file" width={fileSide} height={fileSide}/>
|
||||
<span class="ms-2" style="font-size:{fontSize}px"
|
||||
>.{record._file.path.split(".").pop()}</span
|
||||
>
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -1,30 +0,0 @@
|
||||
<script>
|
||||
|
||||
import PreviewRecord from "./PreviewRecord.svelte";
|
||||
import {previewTitle} from "../../records/Preview.js";
|
||||
import {getContext} from "svelte";
|
||||
import EdgeModal from "../../edges/EdgeModal.svelte";
|
||||
const channel = getContext("channel");
|
||||
export let record
|
||||
export let edge
|
||||
export let graph
|
||||
export let classes
|
||||
export let field
|
||||
|
||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||
let cardTitle = previewTitle({data:edge.data,schema:field.data}, graph);
|
||||
let modal;
|
||||
|
||||
function edit(e){
|
||||
e.preventDefault();
|
||||
modal.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<button class="btn btn-primary btn-sm" on:click={edit}>{cardTitle}</button>
|
||||
<PreviewRecord {record} {graph} {classes} />
|
||||
<EdgeModal bind:this={modal} />
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -1,55 +0,0 @@
|
||||
<script>
|
||||
|
||||
import Status from "../../records/Status.svelte";
|
||||
import Preview from "../../files/Preview.svelte";
|
||||
import {getContext} from "svelte";
|
||||
import {previewTitle} from "../../records/Preview.js";
|
||||
|
||||
const channel = getContext("channel");
|
||||
|
||||
export let classes
|
||||
export let record
|
||||
export let graph
|
||||
|
||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||
let cardTitle = previewTitle(record, graph);
|
||||
</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.status === "draft"}
|
||||
<Status status={record.status}/>
|
||||
{/if}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.title-link {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,5 @@
|
||||
<script>
|
||||
|
||||
export let schema;
|
||||
export let isCreateMode;
|
||||
export let active = "";
|
||||
|
||||
let tabs = schema.groups?.map((group) => {
|
||||
@@ -11,28 +9,12 @@
|
||||
label: "Main",
|
||||
name: "",
|
||||
};
|
||||
let graphTab = {
|
||||
label: "Graph",
|
||||
name: "_graph",
|
||||
};
|
||||
if (isCreateMode) {
|
||||
tabs = [mainTab, ...tabs];
|
||||
} else {
|
||||
tabs = [mainTab, ...tabs, graphTab];
|
||||
}
|
||||
|
||||
function showGraph(e) {
|
||||
e.preventDefault();
|
||||
active = "_graph";
|
||||
}
|
||||
tabs = [mainTab, ...tabs];
|
||||
|
||||
function changeTab(e, tabName) {
|
||||
e.preventDefault();
|
||||
if (tabName == "_graph") {
|
||||
showGraph(e);
|
||||
} else {
|
||||
active = tabName;
|
||||
}
|
||||
active = tabName;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,112 +1,43 @@
|
||||
<script>
|
||||
import {afterUpdate, getContext, onMount} from "svelte";
|
||||
import {isEqual} from "lodash";
|
||||
import {getContext} from "svelte";
|
||||
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"
|
||||
import Form from "./form/Form.svelte";
|
||||
import axios from "axios";
|
||||
|
||||
const channel = getContext("channel");
|
||||
|
||||
export let schema;
|
||||
export let record;
|
||||
export let graph = {
|
||||
records: [],
|
||||
edges: []
|
||||
};
|
||||
export let graph = [];
|
||||
export let recordHistory;
|
||||
export let isCreateMode;
|
||||
export let isWritable = false;
|
||||
export let users;
|
||||
let originalContent;
|
||||
let activeContentTab = "";
|
||||
$: hasUnsavedData = false;
|
||||
// export let isWritable = false;
|
||||
// export let users;
|
||||
$: validationErrors = null;
|
||||
$: errorMessage = validationErrors
|
||||
? `Record submission failed. ${
|
||||
Object.entries(validationErrors).length
|
||||
} error(s)`
|
||||
: null;
|
||||
|
||||
let activeFields = schema.fields.filter(
|
||||
(f) => f.name !== "id"
|
||||
);
|
||||
|
||||
|
||||
onMount(() => {
|
||||
setOriginalContent();
|
||||
});
|
||||
|
||||
function setOriginalContent() {
|
||||
originalContent = {
|
||||
data: JSON.parse(JSON.stringify(record.data)),
|
||||
schema: record.schema,
|
||||
status: record.status,
|
||||
_sys: JSON.parse(JSON.stringify(record._sys)),
|
||||
edges: JSON.parse(JSON.stringify(graph)),
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
schema: record.schema,
|
||||
status: record.status,
|
||||
_sys: record._sys,
|
||||
edges: graph,
|
||||
});
|
||||
}
|
||||
let form;
|
||||
|
||||
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);
|
||||
let replaceEdges = graph
|
||||
.map((queryRecord) => queryRecord.edge)
|
||||
.filter((edge) => !edge._isTrashed && edge.source === record.id);
|
||||
|
||||
axios
|
||||
.post(channel.lucentUrl + "/records", {
|
||||
record: record,
|
||||
edges: graph.edges,
|
||||
schemaName: record.schema,
|
||||
updateEdges: true,
|
||||
id: record.id,
|
||||
data: record.data,
|
||||
edges: replaceEdges,
|
||||
status: record.status,
|
||||
isCreateMode: isCreateMode,
|
||||
})
|
||||
.then(function (response) {
|
||||
@@ -115,15 +46,15 @@
|
||||
if (isCreateMode) {
|
||||
window.location.href = channel.lucentUrl + "/records/" + record.id;
|
||||
} else {
|
||||
record = response.data.rootRecords[0] ?? null;
|
||||
record = response.data.record ?? null;
|
||||
if (!record) {
|
||||
// means trashed
|
||||
hasUnsavedData = false;
|
||||
window.location = channel.lucentUrl;
|
||||
return;
|
||||
}
|
||||
graph = response.data;
|
||||
setOriginalContent();
|
||||
form.setOriginalData();
|
||||
graph = response.data.graph;
|
||||
|
||||
}
|
||||
|
||||
resolve(null);
|
||||
@@ -145,77 +76,24 @@
|
||||
}
|
||||
</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) && isWritable}
|
||||
<div class="shadow-lg record-status-bar">
|
||||
<div
|
||||
class="d-flex mt-3 mb-3 align-items-center justify-content-center"
|
||||
>
|
||||
<StatusSelect bind:status={record.status} {record} {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}/>
|
||||
<FilePreview {record} {schema}/>
|
||||
|
||||
<div class=" mt-4" style="margin-bottom:150px">
|
||||
<ContentTabs
|
||||
{schema}
|
||||
{isCreateMode}
|
||||
bind:active={activeContentTab}
|
||||
/>
|
||||
{#if !["_graph", "_info"].includes(activeContentTab)}
|
||||
<FilePreview {record} {schema}/>
|
||||
<!-- <fieldset disabled="disabled"> -->
|
||||
{#each activeFields as field (field.name)}
|
||||
{#if activeContentTab === field.group}
|
||||
<FormField
|
||||
bind:data={record.data}
|
||||
bind:graph={graph}
|
||||
{field}
|
||||
{schema}
|
||||
{record}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
<!-- </fieldset> -->
|
||||
{:else if activeContentTab === "_graph"}
|
||||
<Graph {graph} {record}/>
|
||||
{:else if activeContentTab === "_info"}
|
||||
<Info {record} {graph} {users} {schema}/>
|
||||
{/if}
|
||||
<Form
|
||||
bind:this={form}
|
||||
data={record.data}
|
||||
status={record.status}
|
||||
{graph}
|
||||
{schema}
|
||||
{record}
|
||||
{isCreateMode}
|
||||
on:save={save}
|
||||
/>
|
||||
<!-- <Graph {graph} {record}/>-->
|
||||
<!-- <Info {record} {graph} {users} {schema}/>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
import {getContext} from "svelte";
|
||||
import Icon from "../common/Icon.svelte";
|
||||
import {previewTitle} from "./Preview";
|
||||
import axios from "axios";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let schema;
|
||||
export let graph;
|
||||
export let record;
|
||||
export let title;
|
||||
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;
|
||||
window.location.href = channel.lucentUrl + "/records/" + response.data.id;
|
||||
}).catch(error => {
|
||||
|
||||
});
|
||||
@@ -22,25 +22,29 @@
|
||||
|
||||
<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}"
|
||||
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( record, graph)}
|
||||
{#if record}
|
||||
{previewTitle(record)}
|
||||
{:else}
|
||||
{ title}
|
||||
{/if}
|
||||
{:else}
|
||||
New Record
|
||||
{/if}
|
||||
</span>
|
||||
{#if !isCreateMode}
|
||||
{#if !isCreateMode && !!record}
|
||||
<div class="dropdown d-inline-block">
|
||||
<button
|
||||
class="btn btn-link btn-sm"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
class="btn btn-link btn-sm"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<Icon icon="ellipsis"/>
|
||||
</button>
|
||||
@@ -48,25 +52,25 @@
|
||||
|
||||
<h6 class="dropdown-header">Record Actions</h6>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
||||
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}
|
||||
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
|
||||
>
|
||||
<!-- <a-->
|
||||
<!-- on:click|preventDefault={(e) =>-->
|
||||
<!-- (activeContentTab = "_info")}-->
|
||||
<!-- class="dropdown-item"-->
|
||||
<!-- href="{channel.lucentUrl}">Revisions</a-->
|
||||
<!-- >-->
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
<script>
|
||||
import Text from "./elements/Text.svelte";
|
||||
import Slug from "./elements/Slug.svelte";
|
||||
import Text from "./form/fields/Text.svelte";
|
||||
import Slug from "./form/fields/Slug.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 Markdown from "./elements/Markdown.svelte";
|
||||
import FieldHeader from "./elements/FieldHeader.svelte";
|
||||
import Color from "./form/fields/Color.svelte";
|
||||
import Checkbox from "./form/fields/Checkbox.svelte";
|
||||
import Number from "./form/fields/Number.svelte";
|
||||
import Date from "./form/fields/Date.svelte";
|
||||
import UUID from "./form/fields/UUID.svelte";
|
||||
import File from "./form/references/File.svelte";
|
||||
import Textarea from "./form/fields/Textarea.svelte";
|
||||
import Datetime from "./form/fields/Datetime.svelte";
|
||||
import RichEditor from "./form/fields/RichEditor.svelte";
|
||||
import Json from "./form/fields/JSON.svelte";
|
||||
import Markdown from "./form/fields/Markdown.svelte";
|
||||
import FieldHeader from "./form/FieldHeader.svelte";
|
||||
import ReferenceTable from "./elements/ReferenceTable.svelte";
|
||||
import ReferenceTags from "./elements/ReferenceTags.svelte";
|
||||
import ReferenceTags from "./form/references/ReferenceTags.svelte";
|
||||
|
||||
const formElements = {
|
||||
text: Text,
|
||||
@@ -28,7 +27,6 @@
|
||||
color: Color,
|
||||
checkbox: Checkbox,
|
||||
number: Number,
|
||||
url: Url,
|
||||
date: Date,
|
||||
datetime: Datetime,
|
||||
uuid: UUID,
|
||||
@@ -48,7 +46,7 @@
|
||||
</script>
|
||||
|
||||
<div class="card editor-field">
|
||||
<FieldHeader {schema} {field} {id}/>
|
||||
<FieldHeader {field} {id}/>
|
||||
{#if field.info.name === "reference" && field.layout === "inline"}
|
||||
<ReferenceInline
|
||||
bind:graph
|
||||
@@ -72,16 +70,8 @@
|
||||
{field}
|
||||
{validationErrors}
|
||||
/>
|
||||
{:else if field.info.name === "reference"}
|
||||
<Reference
|
||||
bind:graph
|
||||
{id}
|
||||
{record}
|
||||
{field}
|
||||
{validationErrors}
|
||||
/>
|
||||
{:else if field.info.name === "file"}
|
||||
<File bind:graph {record} {field} {validationErrors}/>
|
||||
{:else if ["reference","file"].includes(field.info.name)}
|
||||
<File bind:graph {record} {field} />
|
||||
{:else if field.info.name === "block"}
|
||||
<Block
|
||||
bind:graph
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import FormField from "./FormField.svelte";
|
||||
import FilePreview from "./FilePreview.svelte";
|
||||
import ContentTabs from "./ContentTabs.svelte";
|
||||
import StatusSelect from "./StatusSelect.svelte";
|
||||
import StatusSelect from "./form/StatusSelect.svelte";
|
||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
|
||||
@@ -4,31 +4,17 @@ import {getContext} from "svelte";
|
||||
|
||||
export function previewTitle(record) {
|
||||
const channel = getContext("channel");
|
||||
let schema = channel.schemas.find((aSchema) => aSchema.name === record.record?.schema);
|
||||
let schema = channel.schemas.find((aSchema) => aSchema.name === record?.schema);
|
||||
|
||||
if (!schema?.titleTemplate) {
|
||||
return noTemplate(schema, record.record);
|
||||
return noTemplate(schema, record);
|
||||
}
|
||||
|
||||
let recordData = record.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, fieldName) => { // map to records
|
||||
let child = record._children[fieldName].find(c => c.record.id === record.record.id);
|
||||
carry[field] = previewTitle(child);
|
||||
return carry;
|
||||
}, {});
|
||||
recordData = {...recordData, ...referencePreviews}
|
||||
|
||||
let render = Mustache.render(schema.titleTemplate, recordData);
|
||||
let render = Mustache.render(schema.titleTemplate, record.data);
|
||||
|
||||
if (!render || render === "") {
|
||||
return noTemplate(schema, record.record);
|
||||
return noTemplate(schema, record);
|
||||
}
|
||||
|
||||
return stripHtml(render.slice(0, 300));
|
||||
@@ -42,7 +28,6 @@ function noTemplate(schema, record) {
|
||||
let title = stripHtml(
|
||||
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name]
|
||||
).slice(0, 300);
|
||||
|
||||
if (title === "") {
|
||||
return "Untitled";
|
||||
}
|
||||
|
||||
@@ -1,62 +1,84 @@
|
||||
<script>
|
||||
import Icon from "../common/Icon.svelte";
|
||||
|
||||
import { getContext, createEventDispatcher } from "svelte";
|
||||
import Preview from "../files/Preview.svelte";
|
||||
import { previewTitle } from "./Preview";
|
||||
import {getContext, createEventDispatcher} from "svelte";
|
||||
import {previewTitle} from "./Preview";
|
||||
import Status from "./Status.svelte";
|
||||
import Preview from "../newPreview/Preview.svelte";
|
||||
import EdgeData from "./form/references/EdgeData.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const channel = getContext("channel");
|
||||
export let graph;
|
||||
export let record;
|
||||
export let field;
|
||||
export let edge;
|
||||
export let editable = false;
|
||||
export let classes = "";
|
||||
export let hasDelete = false;
|
||||
|
||||
let edgeData;
|
||||
|
||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||
let cardTitle = previewTitle(record, graph);
|
||||
let cardTitle = previewTitle(record);
|
||||
|
||||
function remove(e) {
|
||||
e.preventDefault();
|
||||
dispatch("remove", record.id);
|
||||
}
|
||||
|
||||
function edit(e) {
|
||||
e.preventDefault();
|
||||
edgeData.openEdit();
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
||||
class="card mb-2 bg-light {classes}"
|
||||
style="border-color:{schema.color ?? '#ccc'}; border-width: 1px;"
|
||||
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>
|
||||
<div class="card-body">
|
||||
<Preview {record} type="card"/>
|
||||
{#if editable}
|
||||
<EdgeData bind:this={edgeData} {field} {edge}/>
|
||||
{/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.status === "draft"}
|
||||
<Status status={record.status} />
|
||||
{/if}
|
||||
</small>
|
||||
</div>
|
||||
<!-- <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.status === "draft"}-->
|
||||
<!-- <Status status={record.status} />-->
|
||||
<!-- {/if}-->
|
||||
<!-- </small>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
|
||||
{#if hasDelete}
|
||||
<div class="position-absolute end-0" style="top:5px">
|
||||
<div class="position-absolute d-flex end-0" style="top:5px">
|
||||
{#if editable}
|
||||
<button
|
||||
class="trash-button text-dark btn btn-sm btn-link"
|
||||
on:click={edit}
|
||||
>
|
||||
<Icon icon="pencil"/>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="trash-button text-dark btn btn-sm btn-link"
|
||||
on:click={remove}
|
||||
><Icon icon="trash-can" />
|
||||
class="trash-button text-dark btn btn-sm btn-link"
|
||||
on:click={remove}
|
||||
>
|
||||
<Icon icon="trash-can"/>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -66,9 +88,11 @@
|
||||
.card .trash-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card:hover .trash-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.title-link {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {getStatus, getStatusList} from "./StatusText";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let status = "draft";
|
||||
export let record;
|
||||
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="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>
|
||||
{#if channel.previewTarget}
|
||||
<a href="{channel.previewTargetUrl}?schema={schema.name}&id={record.id}" target="_blank" class="btn btn-info ms-3">
|
||||
Preview
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -4,8 +4,8 @@
|
||||
import {sortByField} from "../../../edges/sortEdges";
|
||||
import ReferenceInlineButtons from "../../elements/ReferenceInlineButtons.svelte"
|
||||
import Sortable from "../../../libs/Sortable.svelte";
|
||||
import {insertEdges} from "../../elements/reference";
|
||||
import BrowseModal from "../../elements/BrowseModal.svelte";
|
||||
import {insertEdges} from "../../form/references/reference.js";
|
||||
import BrowseModal from "../../form/references/BrowseModal.svelte";
|
||||
|
||||
|
||||
const channel = getContext("channel");
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {sortByField} from "../../../edges/sortEdges";
|
||||
import ReferenceInlineButtons from "../../elements/ReferenceInlineButtons.svelte"
|
||||
import Sortable from "../../../libs/Sortable.svelte";
|
||||
import {insertEdges} from "../../elements/reference";
|
||||
import {insertEdges} from "../../form/references/reference.js";
|
||||
|
||||
|
||||
const channel = getContext("channel");
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { deepEqual } from 'fast-equals';
|
||||
export function isEqual(obj1, obj2) {
|
||||
return deepEqual(obj1, obj2);
|
||||
// if (obj1 === obj2) return true;
|
||||
//
|
||||
// if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
||||
//
|
||||
// if(obj1.length !== obj2.length) return false;
|
||||
//
|
||||
// return obj1.every((elem, index) => {
|
||||
// return isEqual(elem, obj2[index]);
|
||||
// })
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
// if(typeof obj1 === "object" && typeof obj2 === "object" && obj1 !== null && obj2 !== null) {
|
||||
// if(Array.isArray(obj1) || Array.isArray(obj2)) return false;
|
||||
//
|
||||
// const keys1 = Object.keys(obj1)
|
||||
// const keys2 = Object.keys(obj2)
|
||||
//
|
||||
// if(keys1.length !== keys2.length || !keys1.every(key => keys2.includes(key))) return false;
|
||||
//
|
||||
// for(let key in obj1) {
|
||||
// if (!isEqual(obj1[key], obj2[key])) { return false; }
|
||||
// }
|
||||
//
|
||||
// return true;
|
||||
//
|
||||
// }
|
||||
//
|
||||
// return false;
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
<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";
|
||||
import Preview from "../../newPreview/Preview.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let field;
|
||||
export let record;
|
||||
export let graph
|
||||
|
||||
let browseModal;
|
||||
|
||||
$: currentReferences = graph._children[field.name] ?? [];
|
||||
|
||||
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.source, e.detail.target, graph.edges, field.name);
|
||||
}
|
||||
|
||||
function insert(e) {
|
||||
e.preventDefault();
|
||||
browseModal.close();
|
||||
const recordsToInsert = e.detail.records;
|
||||
console.log({recordsToInsert})
|
||||
const action = e.detail.action;
|
||||
let newEdges = recordsToInsert.map((r) => {
|
||||
return {
|
||||
target: r.id,
|
||||
source: record.id,
|
||||
sourceSchema: record.schema,
|
||||
targetSchema: r.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 currentReferences.length > 0}
|
||||
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
|
||||
{#each currentReferences as reference (reference.record.id)}
|
||||
<div class="col mb-3">
|
||||
<PreviewCard
|
||||
classes="h-100"
|
||||
record={reference.record}
|
||||
hasDelete={true}
|
||||
on:remove={removeReference}
|
||||
/>
|
||||
<!-- <Preview-->
|
||||
<!-- classes="h-100"-->
|
||||
<!-- record={reference.record}-->
|
||||
<!-- edge={reference.edge}-->
|
||||
<!-- hasDelete={true}-->
|
||||
<!-- {field}-->
|
||||
<!-- on:remove={removeReference}-->
|
||||
<!-- />-->
|
||||
</div>
|
||||
{/each}
|
||||
</Sortable>
|
||||
{/if}
|
||||
<BrowseModal bind:this={browseModal} on:insert={insert}/>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {insertEdges} from "./reference";
|
||||
import {insertEdges} from "../form/references/reference.js";
|
||||
import PreviewCard from "../PreviewCard.svelte";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import {getErrorMessage} from "../form/errorMessage.js";
|
||||
import {sortByField} from "../../edges/sortEdges";
|
||||
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
|
||||
import Sortable from "../../libs/Sortable.svelte";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {getContext} from "svelte";
|
||||
import {uniqBy} from "lodash";
|
||||
import PreviewCardInline from "../PreviewCardInline.svelte";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import {getErrorMessage} from "../form/errorMessage.js";
|
||||
import {sortByField} from "../../edges/sortEdges";
|
||||
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
|
||||
import {flip} from "svelte/animate";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
import InlineEdit from "../InlineEdit.svelte";
|
||||
import BrowseModal from "./BrowseModal.svelte";
|
||||
import BrowseModal from "../form/references/BrowseModal.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
// export let field;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {previewTitle} from "../Preview";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import {getErrorMessage} from "../form/errorMessage.js";
|
||||
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";
|
||||
import {insertEdges} from "./reference.js";
|
||||
import {insertEdges} from "../form/references/reference.js";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let field;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<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>
|
||||
@@ -1,24 +0,0 @@
|
||||
import {uniqBy} from "lodash";
|
||||
|
||||
export function insertEdges(graph, sourceRecord, targetRecords, fieldName, action = "") {
|
||||
let newEdges = targetRecords.map((r) => {
|
||||
return {
|
||||
target: r.id,
|
||||
source: sourceRecord.id,
|
||||
sourceSchema: sourceRecord.schema,
|
||||
targetSchema: r.schema,
|
||||
field: fieldName,
|
||||
depth: 1,
|
||||
rank: ""
|
||||
};
|
||||
});
|
||||
|
||||
let replacedEdges = graph.edges;
|
||||
if (action === "replace") {
|
||||
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
|
||||
}
|
||||
|
||||
graph.records = uniqBy([...graph.records, ...targetRecords], (r) => r.id);
|
||||
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.source + edge.target + edge.field + edge.depth);
|
||||
return graph;
|
||||
}
|
||||
@@ -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}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import { getErrorMessage } from "../form.js";
|
||||
export let id;
|
||||
export let field;
|
||||
export let value;
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import { getErrorMessage } from "../form.js";
|
||||
export let field;
|
||||
export let value;
|
||||
export let isCreateMode;
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {debounce} from "lodash";
|
||||
import {previewTitle} from "../Preview";
|
||||
import {previewTitle} from "../../Preview.js";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let field;
|
||||
+2
-2
@@ -4,8 +4,8 @@
|
||||
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";
|
||||
import { getErrorMessage } from "../form.js";
|
||||
import Icon from "../../../common/Icon.svelte";
|
||||
|
||||
export let field;
|
||||
export let value;
|
||||
+2
-2
@@ -4,8 +4,8 @@
|
||||
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";
|
||||
import { getErrorMessage } from "../form.js";
|
||||
import Icon from "../../../common/Icon.svelte";
|
||||
|
||||
export let field;
|
||||
export let value;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import Codemirror from "../../libs/Codemirror.svelte";
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import Codemirror from "../../../libs/Codemirror.svelte";
|
||||
import { getErrorMessage } from "../form.js";
|
||||
|
||||
|
||||
export let value;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import Codemirror from "../../libs/CodemirrorMarkdown.svelte";
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import Codemirror from "../../../libs/CodemirrorMarkdown.svelte";
|
||||
import { getErrorMessage } from "../form.js";
|
||||
|
||||
|
||||
export let value;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import Datalist from "./Datalist.svelte";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import { getErrorMessage } from "../form.js";
|
||||
|
||||
export let field;
|
||||
export let value;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import Tinymce from "../../libs/Tinymce.svelte";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import Tinymce from "../../../libs/Tinymce.svelte";
|
||||
import { getErrorMessage } from "../form.js";
|
||||
|
||||
export let value;
|
||||
export let field;
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import { getErrorMessage } from "../form.js";
|
||||
export let field;
|
||||
export let value;
|
||||
export let isCreateMode;
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import Datalist from "./Datalist.svelte";
|
||||
import Selectlist from "./Selectlist.svelte";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import { getErrorMessage } from "../form.js";
|
||||
|
||||
export let field;
|
||||
export let value;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import { getErrorMessage } from "../form.js";
|
||||
export let field;
|
||||
export let value;
|
||||
export let isCreateMode;
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getContext } from "svelte";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import Icon from "../../../common/Icon.svelte";
|
||||
import { getErrorMessage } from "../form.js";
|
||||
const channelurl = getContext("channelurl");
|
||||
export let validationErrors;
|
||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import Index from "../../content/Index.svelte";
|
||||
import Index from "../../../content/Index.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const channel = getContext("channel");
|
||||
@@ -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}/>
|
||||
+8
-8
@@ -1,14 +1,14 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {uniqBy, debounce} 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";
|
||||
import Datalist from "./Datalist.svelte";
|
||||
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");
|
||||
@@ -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);
|
||||
}
|
||||
Generated
+10
-1
@@ -5,7 +5,8 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@codemirror/lang-markdown": "^6.2.2"
|
||||
"@codemirror/lang-markdown": "^6.2.2",
|
||||
"fast-equals": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.1.2",
|
||||
@@ -853,6 +854,14 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
|
||||
"integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@
|
||||
"vite": "^3.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-markdown": "^6.2.2"
|
||||
"@codemirror/lang-markdown": "^6.2.2",
|
||||
"fast-equals": "^5.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.record-status-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
background: rgb(var(--lucent-bg-dark)); // old 206, 223, 210
|
||||
z-index: 1050;
|
||||
//position: fixed;
|
||||
//bottom: 0;
|
||||
//left: 0px;
|
||||
//width: 100%;
|
||||
//background: rgb(var(--lucent-bg-dark)); // old 206, 223, 210
|
||||
//z-index: 1050;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace Lucent\Account;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Lucent\Support\Collection;
|
||||
use PhpOption\Option;
|
||||
use Lucent\Support\Option\Option;
|
||||
|
||||
class UserRepo
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Lucent\Channel;
|
||||
use Lucent\Schema\Schema\Schema;
|
||||
use Lucent\Schema\SchemaService;
|
||||
use Lucent\Support\Collection;
|
||||
use PhpOption\Option;
|
||||
use Lucent\Support\Option\Option;
|
||||
|
||||
final class ChannelService
|
||||
{
|
||||
|
||||
@@ -28,17 +28,18 @@ class EdgeRepo
|
||||
|
||||
/**
|
||||
* @param string $from
|
||||
* @param Option<EdgeCollection> $edges
|
||||
* @param EdgeCollection $edges
|
||||
* @return void
|
||||
*/
|
||||
public function update(string $from, Option $edges): void
|
||||
public function update(string $from, EdgeCollection $edges): void
|
||||
{
|
||||
DB::table("edges")->where("source", $from)->delete();
|
||||
if($edges->isEmpty()){
|
||||
return;
|
||||
}
|
||||
|
||||
$edges->map(function (EdgeCollection $edgeCollection) {
|
||||
$edgesDB = collect($edgeCollection)->map(fn(Edge $e) => $e->toDB())->toArray();
|
||||
DB::table("edges")->insert($edgesDB);
|
||||
});
|
||||
$edgesDB = collect($edges)->map(fn(Edge $e) => $e->toDB())->toArray();
|
||||
DB::table("edges")->insert($edgesDB);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -38,10 +38,10 @@ class EdgeService
|
||||
|
||||
/**
|
||||
* @param string $from
|
||||
* @param Option<EdgeCollection> $edges
|
||||
* @param EdgeCollection $edges
|
||||
* @return void
|
||||
*/
|
||||
public function update(string $from, Option $edges): void
|
||||
public function update(string $from, EdgeCollection $edges): void
|
||||
{
|
||||
$this->edgeRepo->update($from, $edges);
|
||||
}
|
||||
|
||||
@@ -10,12 +10,17 @@ use Lucent\Channel\ChannelService;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Query\Operator;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Record\Contracts\EditorTree;
|
||||
use Lucent\Record\Contracts\NewDocumentData;
|
||||
use Lucent\Record\Contracts\RecordEdgeData;
|
||||
use Lucent\Record\Contracts\UpdateRecordData;
|
||||
use Lucent\Record\Manager;
|
||||
use Lucent\Record\QueryRecord;
|
||||
use Lucent\Record\RecordService;
|
||||
use Lucent\Record\Status;
|
||||
use Lucent\Schema\Schema\System;
|
||||
use Lucent\Schema\Validator\ValidatorException;
|
||||
use Lucent\Support\Collection;
|
||||
use Lucent\Support\Result\Result;
|
||||
use Lucent\Support\Result\Success;
|
||||
use Lucent\Svelte\Svelte;
|
||||
@@ -29,11 +34,11 @@ class RecordController extends Controller
|
||||
public function __construct(
|
||||
private readonly RecordService $recordService,
|
||||
private readonly AccountService $accountService,
|
||||
private readonly AuthService $authService,
|
||||
private readonly ChannelService $channelService,
|
||||
private readonly Svelte $svelte,
|
||||
private readonly Query $query,
|
||||
private readonly Manager $recordManager
|
||||
private readonly Manager $recordManager,
|
||||
private readonly EditorTree $editorTree
|
||||
)
|
||||
{
|
||||
}
|
||||
@@ -160,6 +165,7 @@ class RecordController extends Controller
|
||||
$schema = $this->channelService->channel->schemas->where("name", $request->input("schema"))->first();
|
||||
$recordHistory = $this->recordManager->fromSession($request->session())->getRecords();
|
||||
$record = $this->recordService->createEmpty($schema);
|
||||
|
||||
return $this->svelte->render(
|
||||
layout: "channel",
|
||||
view: "recordEdit",
|
||||
@@ -237,7 +243,7 @@ class RecordController extends Controller
|
||||
title: "Edit Record",
|
||||
data: [
|
||||
"schema" => $schema,
|
||||
"graph" => toArray($graph->tree()->first()),
|
||||
"graph" => $this->editorTree->toEditor($graph->tree()->first()),
|
||||
"record" => toArray($record),
|
||||
"users" => $this->accountService->all(),
|
||||
"recordHistory" => $recordHistory,
|
||||
@@ -308,22 +314,34 @@ class RecordController extends Controller
|
||||
|
||||
public function save(Request $request)
|
||||
{
|
||||
$recordId = $request->input("record.id");
|
||||
$recordId = $request->input("id");
|
||||
|
||||
|
||||
$recordEdgeData = (new Collection($request->input("edges")))->map(fn($item) => new RecordEdgeData(
|
||||
target: $item["target"],
|
||||
targetSchema:$item["targetSchema"],
|
||||
field: $item["field"],
|
||||
data: Option::fromValue(data_get($item,"data")),
|
||||
));
|
||||
|
||||
|
||||
$res = match ($request->input("isCreateMode")) {
|
||||
true => $this->recordService->createDocument(
|
||||
schemaName: $request->input("record.schema"),
|
||||
data: $request->input("record.data"),
|
||||
id: Option::fromValue($recordId),
|
||||
edges: Option::fromValue($request->input("edges")),
|
||||
status: Status::from($request->input("record.status")),
|
||||
true => $this->recordService->createDocument(new NewDocumentData(
|
||||
schemaName: $request->input("schemaName"),
|
||||
data: $request->input("data"),
|
||||
id: Option::fromValue($recordId),
|
||||
edges: $recordEdgeData,
|
||||
status: Status::from($request->input("status")),
|
||||
)
|
||||
),
|
||||
default => $this->recordService->update(
|
||||
id: $request->input("record.id"),
|
||||
data: $request->input("record.data"),
|
||||
status: Status::from($request->input("record.status")),
|
||||
edges: Option::fromValue($request->input("edges")),
|
||||
updateEdges: true,
|
||||
default => $this->recordService->update(new UpdateRecordData(
|
||||
id: $recordId,
|
||||
data: $request->input("data"),
|
||||
edges: $recordEdgeData,
|
||||
status: Status::from($request->input("status")),
|
||||
updateEdges: true,
|
||||
)
|
||||
|
||||
)
|
||||
};
|
||||
if ($res->error()->isDefined()) {
|
||||
@@ -332,13 +350,19 @@ class RecordController extends Controller
|
||||
|
||||
$newGraph = $this->query
|
||||
->filter(["id" => $recordId])
|
||||
->limit(10)
|
||||
->childrenDepth(1)
|
||||
->limit(1)
|
||||
->skip(0)
|
||||
->childrenDepth(2)
|
||||
->childrenLimit(200)
|
||||
->parentsDepth(1)
|
||||
->parentsLimit(200)
|
||||
->run();
|
||||
|
||||
|
||||
return result(Success::create(toArray($newGraph)));
|
||||
return result(Success::create([
|
||||
"record" => $newGraph->rootRecords->first(),
|
||||
"graph" => $this->editorTree->toEditor($newGraph->tree()->first())
|
||||
]));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ use Lucent\Edge\Edge;
|
||||
use Lucent\Record\QueryRecord;
|
||||
use Lucent\Record\Record;
|
||||
use Lucent\Support\Collection;
|
||||
use PhpOption\Option;
|
||||
use Lucent\Support\Option\Option;
|
||||
|
||||
final class Graph
|
||||
{
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record\Contracts;
|
||||
|
||||
use Lucent\Record\QueryRecord;
|
||||
use Lucent\Support\Collection;
|
||||
|
||||
class EditorTree
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function toEditor(QueryRecord $record): array
|
||||
{
|
||||
return $record->_children->reduce(function (array $carry, Collection $records) {
|
||||
return array_merge($carry, $records->toArray());
|
||||
}, []) ?? [];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record\Contracts;
|
||||
|
||||
use Lucent\Record\Status;
|
||||
use Lucent\Support\Collection;
|
||||
use PhpOption\Option;
|
||||
|
||||
class NewDocumentData
|
||||
{
|
||||
/**
|
||||
* @param string $schemaName
|
||||
* @param array $data
|
||||
* @param Option<string> $id
|
||||
* @param Collection<RecordEdgeData> $edges
|
||||
* @param Status $status
|
||||
*/
|
||||
public function __construct(
|
||||
public string $schemaName,
|
||||
public array $data,
|
||||
public Option $id,
|
||||
public Collection $edges,
|
||||
public Status $status = Status::DRAFT,
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record\Contracts;
|
||||
|
||||
use Lucent\Edge\Edge;
|
||||
use Lucent\Record\Record;
|
||||
use PhpOption\Option;
|
||||
|
||||
class RecordEdgeData
|
||||
{
|
||||
public function __construct(
|
||||
public string $target,
|
||||
public string $targetSchema,
|
||||
public string $field,
|
||||
public Option $data,
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function toEdge(Record $record, string $index): Edge
|
||||
{
|
||||
return new Edge(
|
||||
source: $record->id,
|
||||
target: $this->target,
|
||||
sourceSchema: $record->schema,
|
||||
targetSchema: $this->targetSchema,
|
||||
field: $this->field,
|
||||
data: $this->data,
|
||||
rank: $index,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Record\Contracts;
|
||||
|
||||
use Lucent\Record\Status;
|
||||
use Lucent\Support\Collection;
|
||||
|
||||
class UpdateRecordData
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param array $data
|
||||
* @param Status $status
|
||||
* @param Collection<RecordEdgeData> $edges
|
||||
* @param bool $updateEdges
|
||||
*/
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public array $data,
|
||||
public Collection $edges,
|
||||
public Status $status,
|
||||
public bool $updateEdges,
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ namespace Lucent\Record;
|
||||
|
||||
use Lucent\Edge\Edge;
|
||||
use Lucent\Support\Collection;
|
||||
use PhpOption\Option;
|
||||
use Lucent\Support\Option\Option;
|
||||
|
||||
class QueryRecord
|
||||
{
|
||||
|
||||
@@ -12,9 +12,11 @@ use Lucent\Edge\EdgeService;
|
||||
use Lucent\File\FileService;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Record\Contracts\NewDocumentData;
|
||||
use Lucent\Record\Contracts\RecordEdgeData;
|
||||
use Lucent\Record\Contracts\UpdateRecordData;
|
||||
use Lucent\Revision\RevisionService;
|
||||
use Lucent\Schema\Field\FieldDataInterface;
|
||||
use Lucent\Schema\Field\FieldInterface;
|
||||
use Lucent\Schema\Schema\Schema;
|
||||
use Lucent\Schema\Validator\Validator;
|
||||
use Lucent\Schema\Validator\ValidatorError;
|
||||
@@ -42,36 +44,27 @@ readonly class RecordService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $schemaName
|
||||
* @param array $data
|
||||
* @param Option<string> $id
|
||||
* @param Option<array> $edges
|
||||
* @param Status $status
|
||||
* @param NewDocumentData $data
|
||||
* @return Result<string|Collection<ValidatorError>>
|
||||
*/
|
||||
public function createDocument(
|
||||
string $schemaName,
|
||||
array $data,
|
||||
Option $id,
|
||||
Option $edges,
|
||||
Status $status = Status::DRAFT,
|
||||
): Result
|
||||
public function createDocument(NewDocumentData $data): Result
|
||||
{
|
||||
$schema = $this->channelService->getSchema($schemaName)->get();
|
||||
$formattedData = $this->inputFormatter->fill($schemaName, new RecordData($data));
|
||||
$newRecordId = $id->getOrElse(Id::new());
|
||||
$uniqueEdges = $this->getUniqueEdges($edges, $newRecordId, $schemaName);
|
||||
$schema = $this->channelService->getSchema($data->schemaName)->get();
|
||||
$formattedData = $this->inputFormatter->fill($data->schemaName, new RecordData($data->data));
|
||||
$newRecordId = $data->id->getOrElse(Id::new());
|
||||
|
||||
$record = new Document(
|
||||
id: $newRecordId,
|
||||
schema: $schema->name,
|
||||
status: $status,
|
||||
status: $data->status,
|
||||
_sys: System::newRecord($this->authService->currentUserId()),
|
||||
data: $formattedData,
|
||||
);
|
||||
|
||||
if ($status === Status::PUBLISHED) {
|
||||
$errors = $this->recordValidator->check($schemaName, $record->data);
|
||||
$uniqueEdges = $this->getUniqueEdges($data->edges, $record);
|
||||
|
||||
if ($data->status === Status::PUBLISHED) {
|
||||
$errors = $this->recordValidator->check($data->schemaName, $record->data);
|
||||
if ($errors->isNotEmpty()) {
|
||||
return Error::create($errors);
|
||||
}
|
||||
@@ -83,22 +76,15 @@ readonly class RecordService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Option<array> $edges
|
||||
* @param string $recordId
|
||||
* @param string $schemaName
|
||||
* @return Option<EdgeCollection>
|
||||
* @param Collection<RecordEdgeData> $edges
|
||||
* @param Record $record
|
||||
* @return EdgeCollection
|
||||
*/
|
||||
private function getUniqueEdges(Option $edges, string $recordId, string $schemaName): Option
|
||||
private function getUniqueEdges(Collection $edges, Record $record): EdgeCollection
|
||||
{
|
||||
return $edges
|
||||
->map(function ($edges) use ($recordId, $schemaName) {
|
||||
foreach ($edges as $index => $edge) {
|
||||
$edges[$index]['source'] = $recordId;
|
||||
$edges[$index]['sourceSchema'] = $schemaName;
|
||||
$edges[$index]["rank"] = $index;
|
||||
}
|
||||
return EdgeCollection::fromArray($edges);
|
||||
});
|
||||
$edges = $edges
|
||||
->map(fn(RecordEdgeData $edge, $index) => $edge->toEdge($record, $index));
|
||||
return new EdgeCollection(...$edges->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,35 +152,23 @@ readonly class RecordService
|
||||
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param array $data
|
||||
* @param Status $status
|
||||
* @param Option<array> $edges
|
||||
* @param bool $updateEdges
|
||||
* @param UpdateRecordData $data
|
||||
* @return Result<string|Collection<ValidatorError>>
|
||||
*/
|
||||
public
|
||||
function update(
|
||||
string $id,
|
||||
array $data,
|
||||
Status $status,
|
||||
Option $edges,
|
||||
bool $updateEdges = false
|
||||
): Result
|
||||
public function update(UpdateRecordData $data): Result
|
||||
{
|
||||
$record = $this->query->filter(["id" => $id])->run()->rootRecords->first();
|
||||
$record = $this->query->filter(["id" => $data->id])->run()->rootRecords->first();
|
||||
|
||||
if (empty($record)) {
|
||||
return Error::create("Record id is missing");
|
||||
}
|
||||
$formattedData = $this->inputFormatter->fill($record->schema, new RecordData($data));
|
||||
$formattedData = $this->inputFormatter->fill($record->schema, new RecordData($data->data));
|
||||
|
||||
$uniqueEdges = new EdgeCollection();
|
||||
if ($updateEdges) {
|
||||
$uniqueEdges = $this->getUniqueEdges($edges, $record->id, $record->schema);
|
||||
if ($data->updateEdges) {
|
||||
$uniqueEdges = $this->getUniqueEdges($data->edges, $record);
|
||||
}
|
||||
|
||||
if ($status === Status::PUBLISHED) {
|
||||
if ($data->status === Status::PUBLISHED) {
|
||||
$errors = $this->recordValidator->check($record->schema, $record->data);
|
||||
if ($errors->isNotEmpty()) {
|
||||
return Error::create($errors);
|
||||
@@ -204,13 +178,13 @@ readonly class RecordService
|
||||
$newRecord = new Document(
|
||||
id: $record->id,
|
||||
schema: $record->schema,
|
||||
status: $status,
|
||||
status: $data->status,
|
||||
_sys: $record->_sys->update($this->authService->currentUserId()),
|
||||
data: $record->data->merge($formattedData),
|
||||
);
|
||||
|
||||
RecordRepo::update($newRecord);
|
||||
if ($updateEdges) {
|
||||
if ($data->updateEdges) {
|
||||
$this->edgeService->update($newRecord->id, $uniqueEdges);
|
||||
}
|
||||
$this->revisionService->create($newRecord, $uniqueEdges);
|
||||
|
||||
@@ -19,7 +19,7 @@ readonly class Revision
|
||||
* @param string $schema
|
||||
* @param System $_sys
|
||||
* @param RecordData $data
|
||||
* @param Option<EdgeCollection> $_edges
|
||||
* @param EdgeCollection $_edges
|
||||
* @param Option<FileInfo> $_file
|
||||
*/
|
||||
function __construct(
|
||||
@@ -28,7 +28,7 @@ readonly class Revision
|
||||
public string $schema,
|
||||
public System $_sys,
|
||||
public RecordData $data,
|
||||
public Option $_edges,
|
||||
public EdgeCollection $_edges,
|
||||
public Option $_file,
|
||||
|
||||
)
|
||||
@@ -37,10 +37,10 @@ readonly class Revision
|
||||
|
||||
/**
|
||||
* @param Record $record
|
||||
* @param Option<EdgeCollection> $edges
|
||||
* @param EdgeCollection $edges
|
||||
* @return Revision
|
||||
*/
|
||||
public static function fromRecord(Record $record, Option $edges): Revision
|
||||
public static function fromRecord(Record $record, EdgeCollection $edges): Revision
|
||||
{
|
||||
return new Revision(
|
||||
id: (string)Str::uuid(),
|
||||
|
||||
@@ -39,10 +39,10 @@ readonly class RevisionService
|
||||
|
||||
/**
|
||||
* @param Record $record
|
||||
* @param Option<EdgeCollection> $edges
|
||||
* @param EdgeCollection $edges
|
||||
* @return void
|
||||
*/
|
||||
public function create(Record $record, Option $edges): void
|
||||
public function create(Record $record,EdgeCollection $edges): void
|
||||
{
|
||||
$schema = $this->channelService->getSchema($record->schema)->get();
|
||||
if($schema->revisions <= 0){
|
||||
|
||||
@@ -12,7 +12,7 @@ use Lucent\Schema\Sidebar\Item\LinkItem;
|
||||
use Lucent\Schema\Sidebar\Item\SchemaItem;
|
||||
use Lucent\Schema\Sidebar\View\ItemData;
|
||||
use Lucent\Schema\Sidebar\View\SectionData;
|
||||
use PhpOption\Option;
|
||||
use Lucent\Support\Option\Option;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class SidebarService
|
||||
|
||||
@@ -6,7 +6,7 @@ use Lucent\Channel\ChannelService;
|
||||
use Lucent\Record\RecordData;
|
||||
use Lucent\Schema\Field\FieldDataInterface;
|
||||
use Lucent\Support\Collection;
|
||||
use PhpOption\Option;
|
||||
use Lucent\Support\Option\Option;
|
||||
|
||||
|
||||
class Validator
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Support;
|
||||
|
||||
use Spatie\LaravelData\Data as LaravelData;
|
||||
use Spatie\LaravelData\DataPipeline;
|
||||
use Spatie\LaravelData\DataPipes\AuthorizedDataPipe;
|
||||
use Spatie\LaravelData\DataPipes\CastPropertiesDataPipe;
|
||||
use Spatie\LaravelData\DataPipes\DefaultValuesDataPipe;
|
||||
use Spatie\LaravelData\DataPipes\FillRouteParameterPropertiesDataPipe;
|
||||
use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe;
|
||||
use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe;
|
||||
|
||||
class Data extends LaravelData
|
||||
{
|
||||
public static function pipeline(): DataPipeline
|
||||
{
|
||||
return DataPipeline::create()
|
||||
->into(static::class)
|
||||
->through(AuthorizedDataPipe::class)
|
||||
->through(MapPropertiesDataPipe::class)
|
||||
->through(FillRouteParameterPropertiesDataPipe::class)
|
||||
->through(ValidatePropertiesDataPipe::class)
|
||||
->through(DefaultValuesDataPipe::class)
|
||||
->through(CastPropertiesDataPipe::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
namespace Lucent\Support\Option;
|
||||
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @extends Option<T>
|
||||
*/
|
||||
final class LazyOption extends Option
|
||||
{
|
||||
/** @var callable(mixed...):(Option<T>) */
|
||||
private $callback;
|
||||
|
||||
/** @var array<int, mixed> */
|
||||
private $arguments;
|
||||
|
||||
/** @var Option<T>|null */
|
||||
private $option;
|
||||
|
||||
/**
|
||||
* @template S
|
||||
* @param callable(mixed...):(Option<S>) $callback
|
||||
* @param array<int, mixed> $arguments
|
||||
*
|
||||
* @return LazyOption<S>
|
||||
*/
|
||||
public static function create($callback, array $arguments = []): self
|
||||
{
|
||||
return new self($callback, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(mixed...):(Option<T>) $callback
|
||||
* @param array<int, mixed> $arguments
|
||||
*/
|
||||
public function __construct($callback, array $arguments = [])
|
||||
{
|
||||
if (!is_callable($callback)) {
|
||||
throw new \InvalidArgumentException('Invalid callback given');
|
||||
}
|
||||
|
||||
$this->callback = $callback;
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
public function isDefined(): bool
|
||||
{
|
||||
return $this->option()->isDefined();
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->option()->isEmpty();
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
return $this->option()->get();
|
||||
}
|
||||
|
||||
public function getOrElse($default)
|
||||
{
|
||||
return $this->option()->getOrElse($default);
|
||||
}
|
||||
|
||||
public function getOrCall($callable)
|
||||
{
|
||||
return $this->option()->getOrCall($callable);
|
||||
}
|
||||
|
||||
public function getOrThrow(\Exception $ex)
|
||||
{
|
||||
return $this->option()->getOrThrow($ex);
|
||||
}
|
||||
|
||||
public function orElse(Option $else)
|
||||
{
|
||||
return $this->option()->orElse($else);
|
||||
}
|
||||
|
||||
public function ifDefined($callable)
|
||||
{
|
||||
$this->option()->forAll($callable);
|
||||
}
|
||||
|
||||
public function forAll($callable)
|
||||
{
|
||||
return $this->option()->forAll($callable);
|
||||
}
|
||||
|
||||
public function map($callable)
|
||||
{
|
||||
return $this->option()->map($callable);
|
||||
}
|
||||
|
||||
public function flatMap($callable)
|
||||
{
|
||||
return $this->option()->flatMap($callable);
|
||||
}
|
||||
|
||||
public function filter($callable)
|
||||
{
|
||||
return $this->option()->filter($callable);
|
||||
}
|
||||
|
||||
public function filterNot($callable)
|
||||
{
|
||||
return $this->option()->filterNot($callable);
|
||||
}
|
||||
|
||||
public function select($value)
|
||||
{
|
||||
return $this->option()->select($value);
|
||||
}
|
||||
|
||||
public function reject($value)
|
||||
{
|
||||
return $this->option()->reject($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Traversable<T>
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return $this->option()->getIterator();
|
||||
}
|
||||
|
||||
public function foldLeft($initialValue, $callable)
|
||||
{
|
||||
return $this->option()->foldLeft($initialValue, $callable);
|
||||
}
|
||||
|
||||
public function foldRight($initialValue, $callable)
|
||||
{
|
||||
return $this->option()->foldRight($initialValue, $callable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Option<T>
|
||||
*/
|
||||
private function option(): Option
|
||||
{
|
||||
if (null === $this->option) {
|
||||
/** @var mixed */
|
||||
$option = call_user_func_array($this->callback, $this->arguments);
|
||||
if ($option instanceof Option) {
|
||||
$this->option = $option;
|
||||
} else {
|
||||
throw new \RuntimeException(sprintf('Expected instance of %s', Option::class));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->option;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
namespace Lucent\Support\Option;
|
||||
|
||||
use EmptyIterator;
|
||||
|
||||
/**
|
||||
* @extends Option<mixed>
|
||||
*/
|
||||
final class None extends Option
|
||||
{
|
||||
/** @var None|null */
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* @return None
|
||||
*/
|
||||
public static function create(): self
|
||||
{
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
throw new \RuntimeException('None has no value.');
|
||||
}
|
||||
|
||||
public function getOrCall($callable)
|
||||
{
|
||||
return $callable();
|
||||
}
|
||||
|
||||
public function getOrElse($default)
|
||||
{
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function getOrThrow(\Exception $ex)
|
||||
{
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isDefined(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function orElse(Option $else)
|
||||
{
|
||||
return $else;
|
||||
}
|
||||
|
||||
public function ifDefined($callable)
|
||||
{
|
||||
// Just do nothing in that case.
|
||||
}
|
||||
|
||||
public function forAll($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function map($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function flatMap($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function filter($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function filterNot($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function select($value)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function reject($value)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIterator(): EmptyIterator
|
||||
{
|
||||
return new EmptyIterator();
|
||||
}
|
||||
|
||||
public function foldLeft($initialValue, $callable)
|
||||
{
|
||||
return $initialValue;
|
||||
}
|
||||
|
||||
public function foldRight($initialValue, $callable)
|
||||
{
|
||||
return $initialValue;
|
||||
}
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Support\Option;
|
||||
|
||||
use ArrayAccess;
|
||||
use IteratorAggregate;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @implements IteratorAggregate<T>
|
||||
*/
|
||||
abstract class Option implements IteratorAggregate, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Creates an option given a return value.
|
||||
*
|
||||
* This is intended for consuming existing APIs and allows you to easily
|
||||
* convert them to an option. By default, we treat ``null`` as the None
|
||||
* case, and everything else as Some.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param S $value The actual return value.
|
||||
* @param S $noneValue The value which should be considered "None"; null by
|
||||
* default.
|
||||
*
|
||||
* @return Option<S>
|
||||
*/
|
||||
public static function fromValue($value, $noneValue = null)
|
||||
{
|
||||
if ($value === $noneValue) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
return new Some($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an option from an array's value.
|
||||
*
|
||||
* If the key does not exist in the array, the array is not actually an
|
||||
* array, or the array's value at the given key is null, None is returned.
|
||||
* Otherwise, Some is returned wrapping the value at the given key.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param array<string|int,S>|ArrayAccess<string|int,S>|null $array A potential array or \ArrayAccess value.
|
||||
* @param string $key The key to check.
|
||||
*
|
||||
* @return Option<S>
|
||||
*/
|
||||
public static function fromArraysValue($array, $key)
|
||||
{
|
||||
if (!(is_array($array) || $array instanceof ArrayAccess) || !isset($array[$key])) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
return new Some($array[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a lazy-option with the given callback.
|
||||
*
|
||||
* This is also a helper constructor for lazy-consuming existing APIs where
|
||||
* the return value is not yet an option. By default, we treat ``null`` as
|
||||
* None case, and everything else as Some.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable $callback The callback to evaluate.
|
||||
* @param array $arguments The arguments for the callback.
|
||||
* @param S $noneValue The value which should be considered "None";
|
||||
* null by default.
|
||||
*
|
||||
* @return LazyOption<S>
|
||||
*/
|
||||
public static function fromReturn($callback, array $arguments = [], $noneValue = null)
|
||||
{
|
||||
return new LazyOption(static function () use ($callback, $arguments, $noneValue) {
|
||||
/** @var mixed */
|
||||
$return = call_user_func_array($callback, $arguments);
|
||||
|
||||
if ($return === $noneValue) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
return new Some($return);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Option factory, which creates new option based on passed value.
|
||||
*
|
||||
* If value is already an option, it simply returns. If value is callable,
|
||||
* LazyOption with passed callback created and returned. If Option
|
||||
* returned from callback, it returns directly. On other case value passed
|
||||
* to Option::fromValue() method.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param Option<S>|callable|S $value
|
||||
* @param S $noneValue Used when $value is mixed or
|
||||
* callable, for None-check.
|
||||
*
|
||||
* @return Option<S>|LazyOption<S>
|
||||
*/
|
||||
public static function ensure($value, $noneValue = null)
|
||||
{
|
||||
if ($value instanceof self) {
|
||||
return $value;
|
||||
} elseif (is_callable($value)) {
|
||||
return new LazyOption(static function () use ($value, $noneValue) {
|
||||
/** @var mixed */
|
||||
$return = $value();
|
||||
|
||||
if ($return instanceof self) {
|
||||
return $return;
|
||||
} else {
|
||||
return self::fromValue($return, $noneValue);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return self::fromValue($value, $noneValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lift a function so that it accepts Option as parameters.
|
||||
*
|
||||
* We return a new closure that wraps the original callback. If any of the
|
||||
* parameters passed to the lifted function is empty, the function will
|
||||
* return a value of None. Otherwise, we will pass all parameters to the
|
||||
* original callback and return the value inside a new Option, unless an
|
||||
* Option is returned from the function, in which case, we use that.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable $callback
|
||||
* @param mixed $noneValue
|
||||
*
|
||||
* @return callable
|
||||
*/
|
||||
public static function lift($callback, $noneValue = null)
|
||||
{
|
||||
return static function () use ($callback, $noneValue) {
|
||||
/** @var array<int, mixed> */
|
||||
$args = func_get_args();
|
||||
|
||||
$reduced_args = array_reduce(
|
||||
$args,
|
||||
/** @param bool $status */
|
||||
static function ($status, self $o) {
|
||||
return $o->isEmpty() ? true : $status;
|
||||
},
|
||||
false
|
||||
);
|
||||
// if at least one parameter is empty, return None
|
||||
if ($reduced_args) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
$args = array_map(
|
||||
/** @return T */
|
||||
static function (self $o) {
|
||||
// it is safe to do so because the fold above checked
|
||||
// that all arguments are of type Some
|
||||
/** @var T */
|
||||
return $o->get();
|
||||
},
|
||||
$args
|
||||
);
|
||||
|
||||
return self::ensure(call_user_func_array($callback, $args), $noneValue);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value if available, or throws an exception otherwise.
|
||||
*
|
||||
* @return T
|
||||
* @throws \RuntimeException If value is not available.
|
||||
*
|
||||
*/
|
||||
abstract public function get();
|
||||
|
||||
/**
|
||||
* Returns the value if available, or the default value if not.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param S $default
|
||||
*
|
||||
* @return T|S
|
||||
*/
|
||||
abstract public function getOrElse($default);
|
||||
|
||||
/**
|
||||
* Returns the value if available, or the results of the callable.
|
||||
*
|
||||
* This is preferable over ``getOrElse`` if the computation of the default
|
||||
* value is expensive.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable():S $callable
|
||||
*
|
||||
* @return T|S
|
||||
*/
|
||||
abstract public function getOrCall($callable);
|
||||
|
||||
/**
|
||||
* Returns the value if available, or throws the passed exception.
|
||||
*
|
||||
* @param \Exception $ex
|
||||
*
|
||||
* @return T
|
||||
*/
|
||||
abstract public function getOrThrow(\Exception $ex);
|
||||
|
||||
/**
|
||||
* Returns true if no value is available, false otherwise.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function isEmpty();
|
||||
|
||||
/**
|
||||
* Returns true if a value is available, false otherwise.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function isDefined();
|
||||
|
||||
/**
|
||||
* Returns this option if non-empty, or the passed option otherwise.
|
||||
*
|
||||
* This can be used to try multiple alternatives, and is especially useful
|
||||
* with lazy evaluating options:
|
||||
*
|
||||
* ```php
|
||||
* $repo->findSomething()
|
||||
* ->orElse(new LazyOption(array($repo, 'findSomethingElse')))
|
||||
* ->orElse(new LazyOption(array($repo, 'createSomething')));
|
||||
* ```
|
||||
*
|
||||
* @param Option<T> $else
|
||||
*
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function orElse(self $else);
|
||||
|
||||
/**
|
||||
* This is similar to map() below except that the return value has no meaning;
|
||||
* the passed callable is simply executed if the option is non-empty, and
|
||||
* ignored if the option is empty.
|
||||
*
|
||||
* In all cases, the return value of the callable is discarded.
|
||||
*
|
||||
* ```php
|
||||
* $comment->getMaybeFile()->ifDefined(function($file) {
|
||||
* // Do something with $file here.
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* If you're looking for something like ``ifEmpty``, you can use ``getOrCall``
|
||||
* and ``getOrElse`` in these cases.
|
||||
*
|
||||
* @param callable(T):mixed $callable
|
||||
*
|
||||
* @return void
|
||||
* @deprecated Use forAll() instead.
|
||||
*
|
||||
*/
|
||||
abstract public function ifDefined($callable);
|
||||
|
||||
/**
|
||||
* This is similar to map() except that the return value of the callable has no meaning.
|
||||
*
|
||||
* The passed callable is simply executed if the option is non-empty, and ignored if the
|
||||
* option is empty. This method is preferred for callables with side-effects, while map()
|
||||
* is intended for callables without side-effects.
|
||||
*
|
||||
* @param callable(T):mixed $callable
|
||||
*
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function forAll($callable);
|
||||
|
||||
/**
|
||||
* Applies the callable to the value of the option if it is non-empty,
|
||||
* and returns the return value of the callable wrapped in Some().
|
||||
*
|
||||
* If the option is empty, then the callable is not applied.
|
||||
*
|
||||
* ```php
|
||||
* (new Some("foo"))->map('strtoupper')->get(); // "FOO"
|
||||
* ```
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):S $callable
|
||||
*
|
||||
* @return Option<S>
|
||||
*/
|
||||
abstract public function map($callable);
|
||||
|
||||
/**
|
||||
* Applies the callable to the value of the option if it is non-empty, and
|
||||
* returns the return value of the callable directly.
|
||||
*
|
||||
* In contrast to ``map``, the return value of the callable is expected to
|
||||
* be an Option itself; it is not automatically wrapped in Some().
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):Option<S> $callable must return an Option
|
||||
*
|
||||
* @return Option<S>
|
||||
*/
|
||||
abstract public function flatMap($callable);
|
||||
|
||||
/**
|
||||
* If the option is empty, it is returned immediately without applying the callable.
|
||||
*
|
||||
* If the option is non-empty, the callable is applied, and if it returns true,
|
||||
* the option itself is returned; otherwise, None is returned.
|
||||
*
|
||||
* @param callable(T):bool $callable
|
||||
*
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function filter($callable);
|
||||
|
||||
/**
|
||||
* If the option is empty, it is returned immediately without applying the callable.
|
||||
*
|
||||
* If the option is non-empty, the callable is applied, and if it returns false,
|
||||
* the option itself is returned; otherwise, None is returned.
|
||||
*
|
||||
* @param callable(T):bool $callable
|
||||
*
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function filterNot($callable);
|
||||
|
||||
/**
|
||||
* If the option is empty, it is returned immediately.
|
||||
*
|
||||
* If the option is non-empty, and its value does not equal the passed value
|
||||
* (via a shallow comparison ===), then None is returned. Otherwise, the
|
||||
* Option is returned.
|
||||
*
|
||||
* In other words, this will filter all but the passed value.
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function select($value);
|
||||
|
||||
/**
|
||||
* If the option is empty, it is returned immediately.
|
||||
*
|
||||
* If the option is non-empty, and its value does equal the passed value (via
|
||||
* a shallow comparison ===), then None is returned; otherwise, the Option is
|
||||
* returned.
|
||||
*
|
||||
* In other words, this will let all values through except the passed value.
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function reject($value);
|
||||
|
||||
/**
|
||||
* Binary operator for the initial value and the option's value.
|
||||
*
|
||||
* If empty, the initial value is returned. If non-empty, the callable
|
||||
* receives the initial value and the option's value as arguments.
|
||||
*
|
||||
* ```php
|
||||
*
|
||||
* $some = new Some(5);
|
||||
* $none = None::create();
|
||||
* $result = $some->foldLeft(1, function($a, $b) { return $a + $b; }); // int(6)
|
||||
* $result = $none->foldLeft(1, function($a, $b) { return $a + $b; }); // int(1)
|
||||
*
|
||||
* // This can be used instead of something like the following:
|
||||
* $option = Option::fromValue($integerOrNull);
|
||||
* $result = 1;
|
||||
* if ( ! $option->isEmpty()) {
|
||||
* $result += $option->get();
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param S $initialValue
|
||||
* @param callable(S, T):S $callable
|
||||
*
|
||||
* @return S
|
||||
*/
|
||||
abstract public function foldLeft($initialValue, $callable);
|
||||
|
||||
/**
|
||||
* foldLeft() but with reversed arguments for the callable.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param S $initialValue
|
||||
* @param callable(T, S):S $callable
|
||||
*
|
||||
* @return S
|
||||
*/
|
||||
abstract public function foldRight($initialValue, $callable);
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return $this->getOrElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
namespace Lucent\Support\Option;
|
||||
|
||||
use ArrayIterator;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @extends Option<T>
|
||||
*/
|
||||
final class Some extends Option
|
||||
{
|
||||
/** @var T */
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @param T $value
|
||||
*/
|
||||
public function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template U
|
||||
*
|
||||
* @param U $value
|
||||
*
|
||||
* @return Some<U>
|
||||
*/
|
||||
public static function create($value): self
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
public function isDefined(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getOrElse($default)
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getOrCall($callable)
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getOrThrow(\Exception $ex)
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function orElse(Option $else)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function ifDefined($callable)
|
||||
{
|
||||
$this->forAll($callable);
|
||||
}
|
||||
|
||||
public function forAll($callable)
|
||||
{
|
||||
$callable($this->value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function map($callable)
|
||||
{
|
||||
return new self($callable($this->value));
|
||||
}
|
||||
|
||||
public function flatMap($callable)
|
||||
{
|
||||
/** @var mixed */
|
||||
$rs = $callable($this->value);
|
||||
if (!$rs instanceof Option) {
|
||||
throw new \RuntimeException('Callables passed to flatMap() must return an Option. Maybe you should use map() instead?');
|
||||
}
|
||||
|
||||
return $rs;
|
||||
}
|
||||
|
||||
public function filter($callable)
|
||||
{
|
||||
if (true === $callable($this->value)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return None::create();
|
||||
}
|
||||
|
||||
public function filterNot($callable)
|
||||
{
|
||||
if (false === $callable($this->value)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return None::create();
|
||||
}
|
||||
|
||||
public function select($value)
|
||||
{
|
||||
if ($this->value === $value) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return None::create();
|
||||
}
|
||||
|
||||
public function reject($value)
|
||||
{
|
||||
if ($this->value === $value) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayIterator<int, T>
|
||||
*/
|
||||
public function getIterator(): ArrayIterator
|
||||
{
|
||||
return new ArrayIterator([$this->value]);
|
||||
}
|
||||
|
||||
public function foldLeft($initialValue, $callable)
|
||||
{
|
||||
return $callable($initialValue, $this->value);
|
||||
}
|
||||
|
||||
public function foldRight($initialValue, $callable)
|
||||
{
|
||||
return $callable($this->value, $initialValue);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace Lucent\Support\Result;
|
||||
|
||||
use PhpOption\None;
|
||||
use PhpOption\Option;
|
||||
use PhpOption\Some;
|
||||
use Lucent\Support\Option\None;
|
||||
use Lucent\Support\Option\Option;
|
||||
use Lucent\Support\Option\Some;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Lucent\Support\Result;
|
||||
|
||||
use PhpOption\Option;
|
||||
use Lucent\Support\Option\Option;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace Lucent\Support\Result;
|
||||
|
||||
use PhpOption\None;
|
||||
use PhpOption\Option;
|
||||
use PhpOption\Some;
|
||||
use Lucent\Support\Option\None;
|
||||
use Lucent\Support\Option\Option;
|
||||
use Lucent\Support\Option\Some;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
use PhpOption\None;
|
||||
use PhpOption\Some;
|
||||
|
||||
use Lucent\Support\Option\None;
|
||||
use Lucent\Support\Option\Some;
|
||||
|
||||
if (!function_exists('some')) {
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user