updates
This commit is contained in:
File diff suppressed because one or more lines are too long
Vendored
+171
File diff suppressed because one or more lines are too long
Vendored
-171
File diff suppressed because one or more lines are too long
Vendored
+3
-3
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"main.js": {
|
||||
"file": "assets/main.55e6cd6b.js",
|
||||
"file": "assets/main.4387b1b7.js",
|
||||
"src": "main.js",
|
||||
"isEntry": true,
|
||||
"css": [
|
||||
"assets/main.e0c13bde.css"
|
||||
"assets/main.0c108e59.css"
|
||||
]
|
||||
},
|
||||
"main.css": {
|
||||
"file": "assets/main.e0c13bde.css",
|
||||
"file": "assets/main.0c108e59.css",
|
||||
"src": "main.css"
|
||||
}
|
||||
}
|
||||
@@ -40,17 +40,17 @@
|
||||
<!-- </div>-->
|
||||
<div class="offcanvas-body">
|
||||
|
||||
<div class="accordion" id="accordionPanelsStayOpenExample">
|
||||
<div class="accordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="panelsStayOpen-headingOne">
|
||||
<h2 class="accordion-header" id="panelsStayOpen-headingMain">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#panelsStayOpen-collapseOne" aria-expanded="true"
|
||||
aria-controls="panelsStayOpen-collapseOne">
|
||||
data-bs-target="#panelsStayOpen-collapseMain" aria-expanded="true"
|
||||
aria-controls="panelsStayOpen-collapseMain">
|
||||
Main
|
||||
</button>
|
||||
</h2>
|
||||
<div id="panelsStayOpen-collapseOne" class="accordion-collapse collapse show"
|
||||
aria-labelledby="panelsStayOpen-headingOne">
|
||||
<div id="panelsStayOpen-collapseMain" class="accordion-collapse collapse show"
|
||||
aria-labelledby="panelsStayOpen-headingMain">
|
||||
<div class="accordion-body">
|
||||
<NavbarMenu
|
||||
schemas={ channel.schemas.filter((sc) => sc.isEntry)}
|
||||
@@ -60,18 +60,36 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="panelsStayOpen-headingTwo">
|
||||
<h2 class="accordion-header" id="panelsStayOpen-headingOther">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#panelsStayOpen-collapseTwo" aria-expanded="false"
|
||||
aria-controls="panelsStayOpen-collapseTwo">
|
||||
data-bs-target="#panelsStayOpen-collapseOther" aria-expanded="false"
|
||||
aria-controls="panelsStayOpen-collapseOther">
|
||||
Other
|
||||
</button>
|
||||
</h2>
|
||||
<div id="panelsStayOpen-collapseTwo" class="accordion-collapse collapse"
|
||||
aria-labelledby="panelsStayOpen-headingTwo">
|
||||
<div id="panelsStayOpen-collapseOther" class="accordion-collapse collapse"
|
||||
aria-labelledby="panelsStayOpen-headingOther">
|
||||
<div class="accordion-body">
|
||||
<NavbarMenu
|
||||
schemas={ channel.schemas.filter((sc) => !sc.isEntry)}
|
||||
schemas={ channel.schemas.filter((sc) => !sc.isEntry && sc.type !== "files")}
|
||||
schema={schema}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="panelsStayOpen-headingFS">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#panelsStayOpen-collapseFS" aria-expanded="false"
|
||||
aria-controls="panelsStayOpen-collapseFS">
|
||||
Filesystem
|
||||
</button>
|
||||
</h2>
|
||||
<div id="panelsStayOpen-collapseFS" class="accordion-collapse collapse"
|
||||
aria-labelledby="panelsStayOpen-headingFS">
|
||||
<div class="accordion-body">
|
||||
<NavbarMenu
|
||||
schemas={ channel.schemas.filter((sc) => sc.type === "files")}
|
||||
schema={schema}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
import SortFields from "./SortFields.svelte";
|
||||
import AppliedFilter from "./AppliedFilter.svelte";
|
||||
import {getContext} from "svelte";
|
||||
import {getContext,createEventDispatcher} from "svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let sort;
|
||||
export let schema;
|
||||
export let operators;
|
||||
@@ -21,6 +23,21 @@
|
||||
|
||||
let csvUrl = url.pathname + "/csv?" + url.searchParams.toString();
|
||||
|
||||
function search(e){
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
let filterKey = data.keys().next().value;
|
||||
let filterValue = data.values().next().value;
|
||||
const url = new URL(modalUrl ?? window.location.href);
|
||||
url.searchParams.set("skip", "0");
|
||||
url.searchParams.set(filterKey, filterValue);
|
||||
if (inModal) {
|
||||
dispatch("refresh", url);
|
||||
} else {
|
||||
window.location = url;
|
||||
}
|
||||
|
||||
}
|
||||
function uploadComplete(e) {
|
||||
records = e.detail;
|
||||
}
|
||||
@@ -49,9 +66,15 @@
|
||||
on:refresh
|
||||
/>
|
||||
|
||||
<form method="GET">
|
||||
<input type="search" name="filter[data.{schema.fields[0].name}_regex]" placeholder="Search"
|
||||
class="form-control" required>
|
||||
<form method="GET" on:submit={search}>
|
||||
{#if schema.fields[0]?.name}
|
||||
<input type="search" name="filter[data.{schema.fields[0].name }_regex]" placeholder="Search"
|
||||
class="form-control" required>
|
||||
{:else}
|
||||
<input type="search" name="filter[_file.originalName_regex]" placeholder="Search"
|
||||
class="form-control" required>
|
||||
{/if}
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
@@ -51,13 +51,13 @@
|
||||
<ReferenceInline
|
||||
bind:graph
|
||||
{record}
|
||||
{schema}
|
||||
{field}
|
||||
{validationErrors}
|
||||
/>
|
||||
{:else if field.info.name === "reference" && field.layout === "table"}
|
||||
<ReferenceTable
|
||||
bind:graph
|
||||
{id}
|
||||
{record}
|
||||
{field}
|
||||
{validationErrors}
|
||||
@@ -73,8 +73,8 @@
|
||||
{:else if field.info.name === "reference"}
|
||||
<Reference
|
||||
bind:graph
|
||||
{id}
|
||||
{record}
|
||||
{schema}
|
||||
{field}
|
||||
{validationErrors}
|
||||
/>
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
const channel = getContext("channel");
|
||||
export let graph;
|
||||
export let record;
|
||||
|
||||
let parentEdgesByField = graph.edges
|
||||
.filter((edge) => edge.source !== record.id && edge.depth === 0)
|
||||
let parentEdgesByField = graph.parentEdges
|
||||
.filter((edge) => edge.source !== record.id && edge.depth === 1)
|
||||
.reduce((carry, edge) => {
|
||||
|
||||
let schemaField = edge.sourceSchema + edge.field;
|
||||
@@ -32,7 +31,7 @@
|
||||
}
|
||||
return carry;
|
||||
}, {});
|
||||
|
||||
console.log(parentEdgesByField)
|
||||
let childrenEdgesByField = graph.edges
|
||||
.filter((edge) => edge.source === record.id && edge.depth === 0)
|
||||
.reduce((carry, edge) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import {afterUpdate, createEventDispatcher, onMount,} from "svelte";
|
||||
import {afterUpdate, createEventDispatcher, onMount,getContext} from "svelte";
|
||||
|
||||
import {isEqual} from "lodash";
|
||||
import FormField from "./FormField.svelte";
|
||||
@@ -8,9 +8,9 @@
|
||||
import StatusSelect from "./StatusSelect.svelte";
|
||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
const dispatch = createEventDispatcher();
|
||||
export let schema;
|
||||
export let schemas;
|
||||
export let record;
|
||||
export let graph = {
|
||||
records: [],
|
||||
@@ -18,7 +18,7 @@
|
||||
};
|
||||
export let isCreateMode;
|
||||
let originalContent;
|
||||
let activeContentTab = "_default";
|
||||
let activeContentTab = "";
|
||||
let hasUnsavedData = false;
|
||||
$: validationErrors = null;
|
||||
$: errorMessage = validationErrors
|
||||
@@ -28,7 +28,7 @@
|
||||
: null;
|
||||
|
||||
let activeFields = schema.fields.filter(
|
||||
(f) => f.trashed === false && f.name !== "id"
|
||||
(f) => f.name !== "id"
|
||||
);
|
||||
|
||||
let tabname = "_default";
|
||||
@@ -116,7 +116,7 @@
|
||||
|
||||
|
||||
axios
|
||||
.post("/records", {
|
||||
.post(channel.lucentUrl + "/records", {
|
||||
record: record,
|
||||
edges: graph.edges,
|
||||
isCreateMode: isCreateMode,
|
||||
@@ -159,24 +159,23 @@
|
||||
|
||||
<div class=" mt-1">
|
||||
<ContentTabs
|
||||
{schema}
|
||||
{isCreateMode}
|
||||
bind:active={activeContentTab}
|
||||
{record}
|
||||
{schema}
|
||||
{isCreateMode}
|
||||
bind:active={activeContentTab}
|
||||
{record}
|
||||
/>
|
||||
<FilePreview {record} {schema}/>
|
||||
<!-- <fieldset disabled="disabled"> -->
|
||||
{#each activeFields as field (field.name)}
|
||||
{#if fieldToTabs[activeContentTab].includes(field.name)}
|
||||
{#if activeContentTab === field.group}
|
||||
<FormField
|
||||
bind:data={record.data}
|
||||
bind:graph={graph}
|
||||
{field}
|
||||
{schema}
|
||||
{schemas}
|
||||
{record}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
bind:data={record.data}
|
||||
bind:graph={graph}
|
||||
{field}
|
||||
{schema}
|
||||
{record}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -189,26 +188,26 @@
|
||||
{/if}
|
||||
{#if isCreateMode}
|
||||
<button
|
||||
class="ms-2 btn btn-primary btn-spinner"
|
||||
on:click={save}
|
||||
class="ms-2 btn btn-primary btn-spinner"
|
||||
on:click={save}
|
||||
>
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Add
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
disabled={!hasUnsavedData}
|
||||
class="ms-2 btn btn-primary btn-spinner"
|
||||
on:click={save}
|
||||
disabled={!hasUnsavedData}
|
||||
class="ms-2 btn btn-primary btn-spinner"
|
||||
on:click={save}
|
||||
>
|
||||
<span
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
class="spinner-border spinner-border-sm"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Save
|
||||
</button>
|
||||
|
||||
@@ -63,10 +63,7 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.button-file {
|
||||
width: 64px;
|
||||
height: 65px;
|
||||
}
|
||||
|
||||
.card .trash-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -2,30 +2,32 @@
|
||||
import Icon from "../common/Icon.svelte";
|
||||
|
||||
|
||||
import {createEventDispatcher, onMount} from "svelte";
|
||||
import {createEventDispatcher, onMount, getContext} from "svelte";
|
||||
import Preview from "../files/Preview.svelte";
|
||||
import InlineEdit from "./InlineEdit.svelte";
|
||||
import Reference from "../content/elements/Reference.svelte";
|
||||
import File from "../content/elements/File.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
const dispatch = createEventDispatcher();
|
||||
export let isFirst;
|
||||
export let isLast;
|
||||
export let toDelete = false;
|
||||
export let schemas;
|
||||
export let record;
|
||||
let editRecord;
|
||||
let schema = schemas.find((aschema) => aschema.name === record.schema);
|
||||
let editGraph;
|
||||
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
|
||||
$: editMode = false;
|
||||
$: expanded = false;
|
||||
|
||||
function editInline(e) {
|
||||
e.preventDefault();
|
||||
axios
|
||||
.get("/records/editInline/" + record.id)
|
||||
.get(channel.lucentUrl + "/records/editInline/" + record.id)
|
||||
.then((response) => {
|
||||
record = response.data;
|
||||
editRecord = response.data;
|
||||
editRecord = response.data.record;
|
||||
editGraph = response.data.graph;
|
||||
editMode = true;
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -76,7 +78,7 @@
|
||||
function deleteFromChannel(e) {
|
||||
e.preventDefault();
|
||||
axios
|
||||
.post("/records/status/trashed", [record])
|
||||
.post(channel.lucentUrl +"/records/status/trashed", [record])
|
||||
.then((response) => {
|
||||
dispatch("remove", record.id);
|
||||
})
|
||||
@@ -92,13 +94,13 @@
|
||||
<p>Item was removed from the current record.</p>
|
||||
<p>
|
||||
<button
|
||||
class="btn btn-sm btn-outline border border-1 border-dark"
|
||||
on:click={undo}>Undo
|
||||
class="btn btn-sm btn-outline border border-1 border-dark"
|
||||
on:click={undo}>Undo
|
||||
</button
|
||||
>
|
||||
<button
|
||||
class="btn btn-sm btn-danger "
|
||||
on:click={deleteFromChannel}
|
||||
class="btn btn-sm btn-danger "
|
||||
on:click={deleteFromChannel}
|
||||
>Delete completely from channel
|
||||
</button
|
||||
>
|
||||
@@ -110,26 +112,26 @@
|
||||
</div>
|
||||
{:else if editMode === true}
|
||||
<InlineEdit
|
||||
{schema}
|
||||
{schemas}
|
||||
record={editRecord}
|
||||
isCreateMode={false}
|
||||
on:cancel={cancel}
|
||||
on:inlinesaved={handleInlinesaved}
|
||||
{schema}
|
||||
record={editRecord}
|
||||
graph={editGraph}
|
||||
isCreateMode={false}
|
||||
on:cancel={cancel}
|
||||
on:inlinesaved={handleInlinesaved}
|
||||
/>
|
||||
{:else}
|
||||
<div class="lx-card mt-4 bg-primary bg-opacity-10">
|
||||
<div class="actions">
|
||||
<small class="text-muted">{schema.label}</small>
|
||||
<button
|
||||
class="btn btn-sm btn-link"
|
||||
on:click|preventDefault={editInline}
|
||||
class="btn btn-sm btn-link"
|
||||
on:click|preventDefault={editInline}
|
||||
>
|
||||
<Icon icon="pencil" width={12} height={12}/>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-link"
|
||||
on:click={(e) => (expanded = !expanded)}
|
||||
class="btn btn-sm btn-link"
|
||||
on:click={(e) => (expanded = !expanded)}
|
||||
>
|
||||
{#if expanded}
|
||||
<Icon icon="compress" width={12} height={12}/>
|
||||
@@ -139,19 +141,19 @@
|
||||
</button>
|
||||
<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>
|
||||
|
||||
<div class="dropdown-menu">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="/records/{record.id}"
|
||||
target="_blank"
|
||||
class="dropdown-item"
|
||||
href="/records/{record.id}"
|
||||
target="_blank"
|
||||
>Edit in new tab
|
||||
</a>
|
||||
<button class="dropdown-item" on:click={trash}>
|
||||
@@ -162,16 +164,16 @@
|
||||
|
||||
{#if !isFirst}
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary border-0"
|
||||
on:click|preventDefault={moveup}
|
||||
class="btn btn-sm btn-outline-primary border-0"
|
||||
on:click|preventDefault={moveup}
|
||||
>
|
||||
<Icon icon="circle-chevron-up"/>
|
||||
</button>
|
||||
{/if}
|
||||
{#if !isLast}
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary border-0"
|
||||
on:click|preventDefault={movedn}
|
||||
class="btn btn-sm btn-outline-primary border-0"
|
||||
on:click|preventDefault={movedn}
|
||||
>
|
||||
<Icon icon="circle-chevron-down"/>
|
||||
</button>
|
||||
@@ -189,7 +191,7 @@
|
||||
>{field.label}</span
|
||||
>
|
||||
{#if field.ui === "reference"}
|
||||
<Reference {record} {schemas} {field}/>
|
||||
<Reference {record} {field}/>
|
||||
{:else if field.ui === "file"}
|
||||
<File {record} {field}/>
|
||||
{:else}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
<script>
|
||||
import Mustache from "mustache";
|
||||
import { imgurl } from "../files/imageserver";
|
||||
import { uniqBy } from "lodash";
|
||||
import { onMount, getContext } from "svelte";
|
||||
import { Network } from "vis-network/esnext";
|
||||
import "vis-network/styles/vis-network.css";
|
||||
|
||||
const channelurl = getContext("channelurl");
|
||||
export let schemas;
|
||||
export let recordGraph;
|
||||
let network;
|
||||
let allEdges = recordGraph._graph.edges.map((e) => {
|
||||
e.fieldLabel = schemas
|
||||
.find((s) => s.uid === e.fromSchema)
|
||||
.fields.find((f) => f.name === e.field).label;
|
||||
return e;
|
||||
});
|
||||
|
||||
let nodes = recordGraph._graph.nodes.map((r) => {
|
||||
let nodeOptions = {
|
||||
id: r.data.id,
|
||||
label: renderTitle(
|
||||
schemas.find((s) => s.uid === r._sys.schema),
|
||||
r
|
||||
),
|
||||
borderWidth: 0,
|
||||
color: {
|
||||
background:
|
||||
r.data.id === recordGraph.data.id ? "#0b5d1e" : "#eeeeee",
|
||||
},
|
||||
font: {
|
||||
multi: true,
|
||||
color: r.data.id === recordGraph.data.id ? "#fff" : "#333",
|
||||
},
|
||||
};
|
||||
|
||||
nodeOptions.shape = "box";
|
||||
if (r._file?.path) {
|
||||
nodeOptions.shape = "image";
|
||||
nodeOptions.image = imgurl(r, 64, 64, "crop");
|
||||
}
|
||||
|
||||
return nodeOptions;
|
||||
});
|
||||
|
||||
nodes = uniqBy(nodes, (n) => n.id);
|
||||
|
||||
// create an array with edges
|
||||
let networkEdges = allEdges.map((e) => {
|
||||
return {
|
||||
from: e.from,
|
||||
to: e.to,
|
||||
label: e.fieldLabel,
|
||||
arrows: {
|
||||
to: {
|
||||
enabled: true,
|
||||
type: "arrow",
|
||||
scaleFactor: 0.5,
|
||||
},
|
||||
},
|
||||
font: { align: "middle" },
|
||||
};
|
||||
});
|
||||
|
||||
let data = {
|
||||
nodes: nodes,
|
||||
edges: networkEdges,
|
||||
};
|
||||
let options = {
|
||||
physics: false,
|
||||
layout: {
|
||||
hierarchical: {
|
||||
enabled: true,
|
||||
nodeSpacing: 150,
|
||||
treeSpacing: 200,
|
||||
direction: "UD",
|
||||
sortMethod: "directed",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
let networkInstance = new Network(network, data, options);
|
||||
|
||||
networkInstance.on("doubleClick", function (params) {
|
||||
if (params.nodes[0]) {
|
||||
window.location = channelurl + "/records/" + params.nodes[0];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function renderTitle(schema, record) {
|
||||
const template = `${schema.titleTemplate}\n <i>${schema.label}</i>`;
|
||||
Mustache.parse(template);
|
||||
|
||||
let recordData = { ...record, ...record.data };
|
||||
|
||||
return Mustache.render(template, recordData);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="lx-card">
|
||||
<div class="network-container" bind:this={network} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.network-container {
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
max-height: 800px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {uniqBy} from "lodash";
|
||||
import {insertEdges} from "./reference";
|
||||
import PreviewCard from "../PreviewCard.svelte";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import {sortByField} from "../../edges/sortEdges";
|
||||
@@ -37,26 +37,7 @@
|
||||
|
||||
function insert(e) {
|
||||
e.preventDefault();
|
||||
const recordsToInsert = e.detail.records;
|
||||
const action = e.detail.action;
|
||||
let newEdges = recordsToInsert.map((r) => {
|
||||
return {
|
||||
target: r.id,
|
||||
source: record.id,
|
||||
sourceSchema: record.schema,
|
||||
targetSchema: r.schema,
|
||||
field: field.name,
|
||||
rank: ""
|
||||
};
|
||||
});
|
||||
|
||||
let replacedEdges = graph.edges;
|
||||
if (action === "replace") {
|
||||
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
|
||||
}
|
||||
|
||||
graph.records = uniqBy([...graph.records, ...recordsToInsert], (r) => r.id);
|
||||
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.target + edge.field);
|
||||
graph = insertEdges(graph,record,e.detail.records,field.name,e.detail.action);
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -68,7 +49,6 @@
|
||||
{/if}
|
||||
<div class="inline-card-wrapper">
|
||||
<ReferenceInlineButtons
|
||||
{field}
|
||||
buttonClass="mt-2"
|
||||
recordId={null}
|
||||
schemas={collections}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {uniqBy} from "lodash";
|
||||
import {previewTitle} from "../Preview";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import {sortByField} from "../../edges/sortEdges";
|
||||
@@ -8,10 +7,10 @@
|
||||
import Sortable from "../../libs/Sortable.svelte";
|
||||
import RenderField from "../../content/RenderField.svelte";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
import {insertEdges} from "./reference.js";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let field;
|
||||
|
||||
export let record;
|
||||
export let graph;
|
||||
export let validationErrors;
|
||||
@@ -46,26 +45,8 @@
|
||||
|
||||
function insert(e) {
|
||||
e.preventDefault();
|
||||
const recordsToInsert = e.detail.records;
|
||||
const action = e.detail.action;
|
||||
let newEdges = recordsToInsert.map((r) => {
|
||||
return {
|
||||
target: r.id,
|
||||
source: record.id,
|
||||
sourceSchema: record.schema,
|
||||
targetSchema: r.schema,
|
||||
field: field.name,
|
||||
rank: ""
|
||||
};
|
||||
});
|
||||
|
||||
let replacedEdges = graph.edges;
|
||||
if (action === "replace") {
|
||||
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
|
||||
}
|
||||
|
||||
graph.records = uniqBy([...graph.records, ...recordsToInsert], (r) => r.id);
|
||||
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.target + edge.field);
|
||||
graph = insertEdges(graph,record,e.detail.records,field.name,e.detail.action);
|
||||
console.log(graph)
|
||||
}
|
||||
|
||||
$:visibleColumns = [];
|
||||
@@ -118,7 +99,7 @@
|
||||
<div class="d-flex align-items-center">
|
||||
<a
|
||||
class="me-2 text-decoration-none text-dark fs-6"
|
||||
href="/records/{record.id}"
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
target="_blank"
|
||||
>
|
||||
{previewTitle(channel.schemas, record)}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import RenderField from "../../content/RenderField.svelte";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
import Datalist from "./Datalist.svelte";
|
||||
import {insertEdges} from "./reference.js";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let field;
|
||||
@@ -62,26 +63,7 @@
|
||||
|
||||
function insert(e, insertRecord) {
|
||||
e.preventDefault();
|
||||
const recordsToInsert = [insertRecord];
|
||||
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((edge) => edge.field !== field.name);
|
||||
}
|
||||
|
||||
graph.records = uniqBy([...graph.records, ...recordsToInsert], (r) => r.id);
|
||||
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.target + edge.field);
|
||||
graph = insertEdges(graph,record,[insertRecord],field.name,e.detail.action);
|
||||
}
|
||||
|
||||
const updateResults = debounce((e) => {
|
||||
|
||||
@@ -2,22 +2,38 @@
|
||||
export let field;
|
||||
export let value;
|
||||
export let search;
|
||||
function select(e, option) {
|
||||
|
||||
function select(e, option) {
|
||||
e.preventDefault();
|
||||
value = option;
|
||||
search = "";
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{#if field.selectOptions}
|
||||
{#each field.selectOptions as suggestion (suggestion)}
|
||||
<div
|
||||
on:click={(e) => select(e, suggestion)}
|
||||
on:keypress={(e) => select(e, suggestion)}
|
||||
>
|
||||
{#if Array.isArray(field.selectOptions)}
|
||||
{#each field.selectOptions as suggestion (suggestion)}
|
||||
<div
|
||||
on:click={(e) => select(e, suggestion)}
|
||||
on:keypress={(e) => select(e, suggestion)}
|
||||
>
|
||||
<span class="dropdown-item">
|
||||
{suggestion}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each Object.entries(field.selectOptions) as [k, v] (k)}
|
||||
<div
|
||||
on:click={(e) => select(e, k)}
|
||||
on:keypress={(e) => select(e, k)}
|
||||
>
|
||||
<span class="dropdown-item">
|
||||
{v}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
autocomplete="off"
|
||||
readonly={field.readonly && !isCreateMode}
|
||||
/>
|
||||
|
||||
<div class="dropdown-menu w-100">
|
||||
<Selectlist
|
||||
{field}
|
||||
@@ -87,7 +86,12 @@
|
||||
{#if value}
|
||||
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
|
||||
<div class="d-flex align-items-center ">
|
||||
{value}
|
||||
{#if Array.isArray(field.selectOptions)}
|
||||
{value}
|
||||
{:else}
|
||||
{field.selectOptions[value]}
|
||||
{/if}
|
||||
|
||||
<button
|
||||
on:click|preventDefault={(e) => (value = "")}
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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,
|
||||
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);
|
||||
return graph;
|
||||
}
|
||||
@@ -158,7 +158,7 @@ class RecordController extends Controller
|
||||
|
||||
public function newInline(Request $request)
|
||||
{
|
||||
$schema = $this->channelService->getSchema($request->input("schema"));
|
||||
$schema = $this->channelService->getSchema($request->input("schema"))->get();
|
||||
$record = $this->recordService->createEmpty($schema);
|
||||
$queryRecord = QueryRecord::fromRecord($record);
|
||||
|
||||
@@ -221,12 +221,14 @@ class RecordController extends Controller
|
||||
->limit(1)
|
||||
->childrenDepth(2)
|
||||
->parentsDepth(1)
|
||||
->tree();
|
||||
->run();
|
||||
|
||||
$record = $graph->records->first();
|
||||
|
||||
return ok(
|
||||
[
|
||||
"graph" => $graph->toArray(),
|
||||
"record" => $graph->first()->toArray(),
|
||||
"graph" => toArray($graph),
|
||||
"record" => toArray($record)
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -287,7 +289,7 @@ class RecordController extends Controller
|
||||
$newGraph = $this->query
|
||||
->filter(["id" => $recordId])
|
||||
->limit(10)
|
||||
->childrenDepth(2)
|
||||
->childrenDepth(1)
|
||||
->parentsDepth(1)
|
||||
->run();
|
||||
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class Filter
|
||||
{
|
||||
private array $operatorsList;
|
||||
|
||||
public function __construct(
|
||||
public array $arguments = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function add(array $arguments): Filter
|
||||
{
|
||||
$this->arguments = $arguments;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
private function formatArguments(array $arguments): array
|
||||
{
|
||||
return (array)collect($arguments)->reduce(fn($c, $v, $k) => $this->formatArgument($c, $v, $k), []);
|
||||
}
|
||||
|
||||
private function formatArgument(array $arguments, mixed $value, string $filter): array
|
||||
{
|
||||
|
||||
$operator = $this->detectOperator($filter);
|
||||
$field = $this->detectField($filter, $operator);
|
||||
$formattedValue = match ($operator) {
|
||||
"eq" => $this->formatText($value),
|
||||
"ne" => $this->formatText($value),
|
||||
"eqnum" => $this->formatNumber($value),
|
||||
"nenum" => $this->formatNumber($value),
|
||||
"object" => $this->formatText($value),
|
||||
"in" => $this->formatListString($value),
|
||||
"nin" => $this->formatListString($value),
|
||||
"innum" => $this->formatListNum($value),
|
||||
"ninnum" => $this->formatListNum($value),
|
||||
"eqtrue" => true,
|
||||
"eqfalse" => false,
|
||||
"netrue" => true,
|
||||
"nefalse" => false,
|
||||
"regex" => "%{$value}%",
|
||||
"gt" => \is_numeric($value) ? floatval($value) : $value,
|
||||
"gte" => \is_numeric($value) ? floatval($value) : $value,
|
||||
"lt" => \is_numeric($value) ? floatval($value) : $value,
|
||||
"lte" => \is_numeric($value) ? floatval($value) : $value,
|
||||
"null" => null,
|
||||
"nnull" => null,
|
||||
"exists" => true,
|
||||
"nexists" => false,
|
||||
default => $value,
|
||||
};
|
||||
|
||||
|
||||
$matchedOperator = $this->operatorsList[$operator];
|
||||
$arguments[] = [
|
||||
"field" => str_replace(".", "->", $field),
|
||||
"operator" => $matchedOperator->db,
|
||||
"value" => $formattedValue
|
||||
];
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
private function formatText(string $value): string
|
||||
{
|
||||
return trim($value);
|
||||
}
|
||||
|
||||
private function formatNumber(string $value): float
|
||||
{
|
||||
return \floatval($value);
|
||||
}
|
||||
|
||||
|
||||
private function formatListString(mixed $value): array
|
||||
{
|
||||
if (\is_string($value)) {
|
||||
$value = explode(",", $value);
|
||||
}
|
||||
|
||||
return \array_map(fn($v) => $this->formatText($v), $value);
|
||||
}
|
||||
|
||||
private function formatListNum(mixed $value): array
|
||||
{
|
||||
if (\is_string($value)) {
|
||||
$value = explode(",", $value);
|
||||
}
|
||||
|
||||
return \array_map(fn($v) => $this->formatNumber($v), $value);
|
||||
}
|
||||
|
||||
|
||||
private function detectOperator(string $filter): string
|
||||
{
|
||||
$exploded = \explode("_", $filter);
|
||||
$candidate = end($exploded);
|
||||
$operatorsListNames = collect($this->operatorsList)->map(fn($o) => $o->name)->toArray();
|
||||
|
||||
if (\in_array($candidate, $operatorsListNames)) {
|
||||
return $candidate;
|
||||
}
|
||||
return 'eq';
|
||||
}
|
||||
|
||||
private function detectField(string $filter, string $operator): string
|
||||
{
|
||||
$exploded = \explode("_", $filter);
|
||||
$candidate = array_pop($exploded);
|
||||
|
||||
if ($candidate == $operator) {
|
||||
return \implode("_", $exploded);
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
|
||||
private function formatReferences(Query $query): array
|
||||
{
|
||||
[$arguments, $referenceArguments] = $this->separateMainFromReferenceArguments();
|
||||
if (empty($referenceArguments)) {
|
||||
return [$arguments, []];
|
||||
};
|
||||
|
||||
$subqueries = collect($referenceArguments)->reduce(function ($c, $v, $k) {
|
||||
$keyWithoutRef = str_replace("children.", "", $k);
|
||||
[$field] = explode(".", $keyWithoutRef);
|
||||
$referenceField = str_replace($field . ".", "", $keyWithoutRef);
|
||||
$c[$field][$referenceField] = $v;
|
||||
return $c;
|
||||
}, []);
|
||||
|
||||
|
||||
$sourceIds = collect($subqueries)->reduce(function ($c, $subquery, $k) use ($query) {
|
||||
|
||||
$graph = $query->filter($subquery)->run();
|
||||
|
||||
if (!$graph->hasResults()) {
|
||||
return $c;
|
||||
}
|
||||
|
||||
$targetIds = collect($graph->records)->pluck("id");
|
||||
$sourceIds = DB::table("edges")->whereIn("target", $targetIds)->where("field", $k)->get()->pluck("source");
|
||||
return array_merge($c, $sourceIds->toArray());
|
||||
}, []);
|
||||
|
||||
return [$arguments, [
|
||||
"field" => "id",
|
||||
"operator" => "in",
|
||||
"value" => $sourceIds
|
||||
]];
|
||||
}
|
||||
|
||||
private function separateMainFromReferenceArguments(): array
|
||||
{
|
||||
return collect($this->arguments)->partition(function ($v, $k) {
|
||||
if (!str_starts_with($k, "children.")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
|
||||
public function run(Query $query): array
|
||||
{
|
||||
[$argumentsWithoutReferences, $referencesFilter] = $this->formatReferences($query);
|
||||
|
||||
$this->operatorsList = Operator::list();
|
||||
$formattedArguments = $this->formatArguments($argumentsWithoutReferences);
|
||||
if (!empty($referencesFilter)) {
|
||||
$formattedArguments[] = $referencesFilter;
|
||||
}
|
||||
return $formattedArguments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query\Filter;
|
||||
|
||||
|
||||
final class AndFilter implements Filter
|
||||
{
|
||||
|
||||
|
||||
public function __construct(
|
||||
public array $params = []
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function toArray():array{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query\Filter;
|
||||
|
||||
class Argument
|
||||
{
|
||||
public function __construct(
|
||||
public string $field,
|
||||
public string $operator,
|
||||
public mixed $value,
|
||||
){}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php namespace Lucent\Query\Filter;
|
||||
|
||||
|
||||
interface Filter
|
||||
{
|
||||
public function toArray():array;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query\Filter;
|
||||
|
||||
|
||||
final class OrFilter implements Filter
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
public array $params = []
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function toArray():array{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query;
|
||||
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Lucent\Query\Filter\AndFilter;
|
||||
use Lucent\Query\Filter\Argument;
|
||||
use Lucent\Query\Filter\Filter;
|
||||
use Lucent\Query\Filter\OrFilter;
|
||||
|
||||
final class FilterParser
|
||||
{
|
||||
|
||||
public function __construct(public Application $app)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array $arguments
|
||||
* @return array<Argument>
|
||||
*/
|
||||
private function formatArguments(array $arguments): array
|
||||
{
|
||||
return collect($arguments)->reduce(function ($c, $v, $k) {
|
||||
$c[] = $this->formatArgument($v, $k);
|
||||
return $c;
|
||||
}, []);
|
||||
}
|
||||
|
||||
private function formatArgument(mixed $value, string $filter): Argument
|
||||
{
|
||||
|
||||
$operator = $this->detectOperator($filter);
|
||||
$field = $this->detectField($filter, $operator);
|
||||
$formattedValue = match ($operator) {
|
||||
"eq" => $this->formatText($value),
|
||||
"ne" => $this->formatText($value),
|
||||
"eqnum" => $this->formatNumber($value),
|
||||
"nenum" => $this->formatNumber($value),
|
||||
"object" => $this->formatText($value),
|
||||
"in" => $this->formatListString($value),
|
||||
"nin" => $this->formatListString($value),
|
||||
"innum" => $this->formatListNum($value),
|
||||
"ninnum" => $this->formatListNum($value),
|
||||
"eqtrue" => true,
|
||||
"eqfalse" => false,
|
||||
"netrue" => true,
|
||||
"nefalse" => false,
|
||||
"regex" => "%{$value}%",
|
||||
"gt" => is_numeric($value) ? floatval($value) : $value,
|
||||
"gte" => is_numeric($value) ? floatval($value) : $value,
|
||||
"lt" => is_numeric($value) ? floatval($value) : $value,
|
||||
"lte" => is_numeric($value) ? floatval($value) : $value,
|
||||
"null" => null,
|
||||
"nnull" => null,
|
||||
"exists" => true,
|
||||
"nexists" => false,
|
||||
default => $value,
|
||||
};
|
||||
|
||||
|
||||
$matchedOperator = Operator::list()[$operator];
|
||||
return new Argument(
|
||||
field: str_replace(".", "->", $field),
|
||||
operator: $matchedOperator->db,
|
||||
value: $formattedValue
|
||||
);
|
||||
}
|
||||
|
||||
private function formatText(string $value): string
|
||||
{
|
||||
return trim($value);
|
||||
}
|
||||
|
||||
private function formatNumber(string $value): float
|
||||
{
|
||||
return floatval($value);
|
||||
}
|
||||
|
||||
|
||||
private function formatListString(mixed $value): array
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$value = explode(",", $value);
|
||||
}
|
||||
|
||||
return array_map(fn($v) => $this->formatText($v), $value);
|
||||
}
|
||||
|
||||
private function formatListNum(mixed $value): array
|
||||
{
|
||||
if (\is_string($value)) {
|
||||
$value = explode(",", $value);
|
||||
}
|
||||
|
||||
return \array_map(fn($v) => $this->formatNumber($v), $value);
|
||||
}
|
||||
|
||||
|
||||
private function detectOperator(string $filter): string
|
||||
{
|
||||
$exploded = \explode("_", $filter);
|
||||
$candidate = end($exploded);
|
||||
$operatorsListNames = collect(Operator::list())->map(fn($o) => $o->name)->toArray();
|
||||
|
||||
if (\in_array($candidate, $operatorsListNames)) {
|
||||
return $candidate;
|
||||
}
|
||||
return 'eq';
|
||||
}
|
||||
|
||||
private function detectField(string $filter, string $operator): string
|
||||
{
|
||||
|
||||
$exploded = explode("_", $filter);
|
||||
$candidate = array_pop($exploded);
|
||||
if ($candidate === $operator) {
|
||||
return implode("_", $exploded);
|
||||
}
|
||||
return $filter;
|
||||
}
|
||||
|
||||
|
||||
private function formatReferences(array $referenceArguments): Argument
|
||||
{
|
||||
$subqueries = collect($referenceArguments)->reduce(function ($c, $v, $k) {
|
||||
$keyWithoutRef = str_replace("children.", "", $k);
|
||||
[$field] = explode(".", $keyWithoutRef);
|
||||
$referenceField = str_replace($field . ".", "", $keyWithoutRef);
|
||||
$c[$field][$referenceField] = $v;
|
||||
return $c;
|
||||
}, []);
|
||||
|
||||
$sourceIds = collect($subqueries)->reduce(function ($c, $subquery, $k) {
|
||||
|
||||
$query = $this->app->make(Query::class);
|
||||
$graph = $query->filter($subquery)->run();
|
||||
|
||||
if (!$graph->hasResults()) {
|
||||
return $c;
|
||||
}
|
||||
|
||||
$targetIds = collect($graph->records)->pluck("id");
|
||||
$sourceIds = DB::table("edges")->whereIn("target", $targetIds)->where("field", $k)->get()->pluck("source");
|
||||
return array_merge($c, $sourceIds->toArray());
|
||||
}, []);
|
||||
|
||||
return new Argument(
|
||||
field: "id",
|
||||
operator: "in",
|
||||
value: $sourceIds
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private function separateMainFromReferenceArguments(Filter $arguments): array
|
||||
{
|
||||
return collect($arguments->toArray())->partition(function ($v, $k) {
|
||||
if (!str_starts_with($k, "children.")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array<Argument>
|
||||
*/
|
||||
private function parseArguments(Filter $arguments): array
|
||||
{
|
||||
[$normalArguments, $referenceArguments] = $this->separateMainFromReferenceArguments($arguments);
|
||||
|
||||
$formattedArguments = $this->formatArguments($normalArguments);
|
||||
if (!empty($referenceArguments)) {
|
||||
$formattedArguments[] = $this->formatReferences($referenceArguments);
|
||||
}
|
||||
|
||||
return $formattedArguments;
|
||||
}
|
||||
|
||||
|
||||
public function parse(Builder $builder, Filter $filter): Builder
|
||||
{
|
||||
$arguments = $this->parseArguments($filter);
|
||||
return match (get_class($filter)) {
|
||||
AndFilter::class => $this->parseAnd($builder, $arguments),
|
||||
OrFilter::class => $this->parseOr($builder, $arguments),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Argument> $arguments
|
||||
*/
|
||||
private function parseAnd(Builder $builder, array $arguments): Builder
|
||||
{
|
||||
|
||||
$ignoredFilters = [];
|
||||
foreach ($arguments as $argument) {
|
||||
if (in_array($argument->field, $ignoredFilters)) {
|
||||
continue;
|
||||
} else if ($argument->operator == "in") {
|
||||
$builder->whereIn($argument->field, $argument->value);
|
||||
} else if ($argument->operator == "nin") {
|
||||
$builder->whereNotIn($argument->field, $argument->value);
|
||||
} elseif ($argument->operator == "eqobject") {
|
||||
|
||||
$object = $argument->value;
|
||||
// unset related filters used here
|
||||
$addToIgnored = collect($arguments)
|
||||
->filter(fn(Argument $f) => str_starts_with($f->field, $object))
|
||||
->values()
|
||||
->map(fn($f) => $f->field)
|
||||
->toArray();
|
||||
|
||||
$ignoredFilters = array_merge($ignoredFilters, $addToIgnored);
|
||||
|
||||
$objectFilters = collect($arguments)
|
||||
->filter(fn($f) => str_starts_with($f->field, $object))
|
||||
->values()
|
||||
->reduce(function ($c, $f) use ($object) {
|
||||
$field = str_replace($object . "->", "", $f->field);
|
||||
$c[$field] = $f->value;
|
||||
return $c;
|
||||
});
|
||||
|
||||
// target result
|
||||
// filter[data.previousNames_object]=previousNames&filter[previousNames.name_eq]=alpha&filter[previousNames.id_eqnum]=24
|
||||
// $query->whereJsonContains("data->previousNames", [["name" => "alpha", "id" => 24]]);
|
||||
// $query->whereJsonContains($filter["field"], [$objectFilters]);
|
||||
} else {
|
||||
$builder->where($argument->field, $argument->operator, $argument->value);
|
||||
}
|
||||
}
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Argument> $arguments
|
||||
*/
|
||||
private function parseOr(Builder $builder, array $arguments): Builder
|
||||
{
|
||||
$builder->where(function (Builder $orBuilder) use ($arguments) {
|
||||
$ignoredFilters = [];
|
||||
foreach ($arguments as $argument) {
|
||||
if (in_array($argument->field, $ignoredFilters)) {
|
||||
continue;
|
||||
} else if ($argument->operator == "in") {
|
||||
$orBuilder->orWhereIn($argument->field, $argument->value);
|
||||
} else if ($argument->operator == "nin") {
|
||||
$orBuilder->orWhereNotIn($argument->field, $argument->value);
|
||||
} elseif ($argument->operator == "eqobject") {
|
||||
|
||||
$object = $argument->value;
|
||||
// unset related filters used here
|
||||
$addToIgnored = collect($arguments)
|
||||
->filter(fn(Argument $f) => str_starts_with($f->field, $object))
|
||||
->values()
|
||||
->map(fn($f) => $f->field)
|
||||
->toArray();
|
||||
|
||||
$ignoredFilters = array_merge($ignoredFilters, $addToIgnored);
|
||||
|
||||
$objectFilters = collect($arguments)
|
||||
->filter(fn($f) => str_starts_with($f->field, $object))
|
||||
->values()
|
||||
->reduce(function ($c, $f) use ($object) {
|
||||
$field = str_replace($object . "->", "", $f->field);
|
||||
$c[$field] = $f->value;
|
||||
return $c;
|
||||
});
|
||||
|
||||
// target result
|
||||
// filter[data.previousNames_object]=previousNames&filter[previousNames.name_eq]=alpha&filter[previousNames.id_eqnum]=24
|
||||
// $query->whereJsonContains("data->previousNames", [["name" => "alpha", "id" => 24]]);
|
||||
// $query->whereJsonContains($filter["field"], [$objectFilters]);
|
||||
} else {
|
||||
$orBuilder->orWhere($argument->field, $argument->operator, $argument->value);
|
||||
}
|
||||
}
|
||||
});
|
||||
return $builder;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+36
-1
@@ -16,6 +16,7 @@ final class Graph
|
||||
public function __construct(
|
||||
public Collection $records,
|
||||
public Collection $edges,
|
||||
public Collection $parentEdges,
|
||||
public ?int $total = null,
|
||||
)
|
||||
{
|
||||
@@ -66,6 +67,15 @@ final class Graph
|
||||
|
||||
public function tree(): Collection
|
||||
{
|
||||
return $this->getRootRecords()
|
||||
->map([$this, 'findParents'])
|
||||
->map([$this, 'findChildren'])
|
||||
;
|
||||
|
||||
return $rootRecords;
|
||||
|
||||
|
||||
|
||||
return $this->records->filter(function (QueryRecord $record) {
|
||||
return $this->edges->filter(fn(Edge $ed) => $ed->target == $record->id)->isEmpty();
|
||||
})->values()
|
||||
@@ -81,7 +91,6 @@ final class Graph
|
||||
foreach ($recordEdges as $element) {
|
||||
$groupRecordEdges[$element->field][] = $element;
|
||||
}
|
||||
|
||||
$children = [];
|
||||
foreach ($groupRecordEdges as $field => $edges) {
|
||||
|
||||
@@ -99,4 +108,30 @@ final class Graph
|
||||
return $record;
|
||||
}
|
||||
|
||||
|
||||
public function findParents(QueryRecord $record): QueryRecord
|
||||
{
|
||||
$recordEdges = $this->parentEdges->filter(fn(Edge $ed) => $ed->target === $record->id)->values()->sort(fn($a, $b) => $a->rank <=> $b->rank)->values();
|
||||
|
||||
$groupRecordEdges = [];
|
||||
foreach ($recordEdges as $element) {
|
||||
$groupRecordEdges[$element->field][] = $element;
|
||||
}
|
||||
$parents = [];
|
||||
foreach ($groupRecordEdges as $field => $edges) {
|
||||
|
||||
$parents[$field] = [];
|
||||
foreach ($edges as $anEdge) {
|
||||
$aRecord = $this->records->filter(fn(QueryRecord $rec) => $rec->id == $anEdge->source)->values();
|
||||
if (empty($aRecord[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parents[$field][] = $this->findParents($aRecord[0]);
|
||||
}
|
||||
}
|
||||
$record->_parents = $parents;
|
||||
return $record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+61
-62
@@ -4,9 +4,10 @@ namespace Lucent\Query;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Edge\Edge;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Query\Filter\AndFilter;
|
||||
use Lucent\Query\Filter\OrFilter;
|
||||
use Lucent\Record\InputFormatter;
|
||||
use Lucent\Record\QueryRecord;
|
||||
use Lucent\Record\Record;
|
||||
@@ -14,11 +15,14 @@ use Lucent\Record\Record;
|
||||
final class Query
|
||||
{
|
||||
|
||||
public Filter $filter;
|
||||
/**
|
||||
* @var array<AndFilter> $filters
|
||||
*/
|
||||
public array $filters;
|
||||
public QueryOptions $options;
|
||||
|
||||
public function __construct(
|
||||
public readonly ChannelService $channelService,
|
||||
public readonly FilterParser $filterParser,
|
||||
public readonly InputFormatter $inputFormatter,
|
||||
)
|
||||
{
|
||||
@@ -27,7 +31,13 @@ final class Query
|
||||
|
||||
public function filter(array $filterArguments): Query
|
||||
{
|
||||
$this->filter = new Filter($filterArguments);
|
||||
$this->filters[] = new AndFilter($filterArguments);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function orFilter(array $filterArguments): Query
|
||||
{
|
||||
$this->filters[] = new OrFilter($filterArguments);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -62,16 +72,22 @@ final class Query
|
||||
->get()->toArray();
|
||||
}
|
||||
$resultsRecordsUnique = collect(array_merge($resultsRecords, $edgeRecords))->unique("id")->values()->toArray();
|
||||
$resultEdges = collect(array_merge($resultChildrenEdges, $resultParentEdges))
|
||||
->unique(fn($edge) => $edge->source . $edge->target . $edge->field)
|
||||
->toArray();
|
||||
// $resultEdges = collect(array_merge($resultChildrenEdges, $resultParentEdges))
|
||||
// ->unique(fn($edge) => $edge->source . $edge->target . $edge->field)
|
||||
// ->toArray();
|
||||
|
||||
|
||||
return $this->formatRecords($resultsRecordsUnique, $resultEdges);
|
||||
$this->reset();
|
||||
return $this->formatRecords($resultsRecordsUnique, $resultChildrenEdges, $resultParentEdges);
|
||||
|
||||
}
|
||||
|
||||
private function formatRecords(array $records, array $edges): Graph
|
||||
private function reset()
|
||||
{
|
||||
$this->options = new QueryOptions();
|
||||
$this->filters = [];
|
||||
}
|
||||
|
||||
private function formatRecords(array $records, array $edges, array $parentEdges): Graph
|
||||
{
|
||||
$queryRecords = collect($records)->map(function ($recordData) {
|
||||
|
||||
@@ -89,10 +105,17 @@ final class Query
|
||||
|
||||
})->sortBy("rank")->values()->toArray();
|
||||
|
||||
$queryParentEdges = collect($parentEdges)->map(function ($edgeData) {
|
||||
|
||||
return Edge::fromArray((array)$edgeData);
|
||||
|
||||
})->sortBy("rank")->values()->toArray();
|
||||
|
||||
|
||||
return new Graph(
|
||||
new Collection($queryRecords),
|
||||
new Collection($queryEdges),
|
||||
new Collection($queryParentEdges),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,61 +126,21 @@ final class Query
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function parseFilters(Builder $query): Builder
|
||||
{
|
||||
$filters = $this->filter->run(new Query($this->channelService, $this->inputFormatter));
|
||||
$ignoredFilters = [];
|
||||
foreach ($filters as $filter) {
|
||||
if (in_array($filter["field"], $ignoredFilters)) {
|
||||
continue;
|
||||
} else if ($filter["operator"] == "in") {
|
||||
$query->whereIn($filter["field"], $filter["value"]);
|
||||
} else if ($filter["operator"] == "nin") {
|
||||
$query->whereNotIn($filter["field"], $filter["value"]);
|
||||
} elseif ($filter["operator"] == "eqobject") {
|
||||
|
||||
$object = $filter["value"];
|
||||
// unset related filters used here
|
||||
$addToIgnored = collect($filters)
|
||||
->filter(fn($f) => str_starts_with($f["field"], $object))
|
||||
->values()
|
||||
->map(fn($f) => $f["field"])
|
||||
->toArray();
|
||||
|
||||
$ignoredFilters = array_merge($ignoredFilters, $addToIgnored);
|
||||
|
||||
$objectFilters = collect($filters)
|
||||
->filter(fn($f) => str_starts_with($f["field"], $object))
|
||||
->values()
|
||||
->reduce(function ($c, $f) use ($object) {
|
||||
$field = str_replace($object . "->", "", $f["field"]);
|
||||
$c[$field] = $f["value"];
|
||||
return $c;
|
||||
});
|
||||
|
||||
// target result
|
||||
// filter[data.previousNames_object]=previousNames&filter[previousNames.name_eq]=alpha&filter[previousNames.id_eqnum]=24
|
||||
// $query->whereJsonContains("data->previousNames", [["name" => "alpha", "id" => 24]]);
|
||||
// $query->whereJsonContains($filter["field"], [$objectFilters]);
|
||||
} else {
|
||||
|
||||
$query->where($filter["field"], $filter["operator"], $filter["value"]);
|
||||
}
|
||||
foreach ($this->filters as $filter) {
|
||||
$query = $this->filterParser->parse($query, $filter);
|
||||
}
|
||||
|
||||
$query->whereIn("status", $this->options->status);
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SubqueryNoResultException
|
||||
*/
|
||||
|
||||
private function mainQuery(): array
|
||||
{
|
||||
$query = DB::table("records");
|
||||
$query = $this->parseFilters($query);
|
||||
if($this->options->limit > 0){
|
||||
if ($this->options->limit > 0) {
|
||||
$query->limit($this->options->limit);
|
||||
$query->offset($this->options->skip);
|
||||
}
|
||||
@@ -174,14 +157,19 @@ final class Query
|
||||
function getChildren(array $ids): array
|
||||
{
|
||||
$subquery = DB::table('edges AS g')
|
||||
->select(DB::raw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field, 0 as depth '))
|
||||
->whereIn('source', $ids)
|
||||
->limit($this->options->childrenLimit)
|
||||
->select(DB::raw('g.source,g.target,g.rank,g.sourceSchema,g.targetSchema,g.field, 1 as depth '))
|
||||
->whereIn('source', $ids);
|
||||
|
||||
if (!empty($this->options->childrenFields)) {
|
||||
$subquery->whereIn('field', $this->options->childrenFields);
|
||||
}
|
||||
|
||||
$subquery->limit($this->options->childrenLimit)
|
||||
->unionAll(
|
||||
DB::table(DB::raw("edges AS g, search_graph AS sg "))
|
||||
->selectRaw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field,sg.depth + 1 as depth')
|
||||
->whereRaw("g.source = sg.target")
|
||||
->where("sg.depth", "<=", $this->options->childrenDepth)
|
||||
->where("depth", "<", $this->options->childrenDepth)
|
||||
->orderBy("rank")
|
||||
);
|
||||
|
||||
@@ -191,18 +179,17 @@ final class Query
|
||||
->get()->toArray();
|
||||
}
|
||||
|
||||
private
|
||||
function getParents(array $ids): array
|
||||
private function getParents(array $ids): array
|
||||
{
|
||||
$subquery = DB::table('edges AS g')
|
||||
->select(DB::raw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field, 0 as depth '))
|
||||
->select(DB::raw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field, 1 as depth '))
|
||||
->limit($this->options->parentsLimit)
|
||||
->whereIn('g.target', $ids)
|
||||
->unionAll(
|
||||
DB::table(DB::raw("edges AS g, search_graph AS sg "))
|
||||
->selectRaw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field,sg.depth + 1 as depth')
|
||||
->whereRaw("g.target = sg.source")
|
||||
->where("sg.depth", "<=", $this->options->parentsDepth)
|
||||
->where("depth", "<", $this->options->parentsDepth)
|
||||
->orderBy("rank")
|
||||
);
|
||||
|
||||
@@ -251,6 +238,18 @@ final class Query
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function childrenFields(array $fields): Query
|
||||
{
|
||||
$this->options->childrenFields = $fields;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function parentFields(array $fields): Query
|
||||
{
|
||||
$this->options->parentFields = $fields;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function parentsDepth(int $depth): Query
|
||||
{
|
||||
$this->options->parentsDepth = $depth;
|
||||
|
||||
@@ -13,6 +13,8 @@ final class QueryOptions
|
||||
public int $parentsDepth = -1,
|
||||
public int $childrenLimit = -1,
|
||||
public int $parentsLimit = -1,
|
||||
public array $childrenFields = [],
|
||||
public array $parentFields = [],
|
||||
public array $sort = [],
|
||||
public array $status = ["published", "draft"]
|
||||
)
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query;
|
||||
|
||||
use Lucent\Edge\Edge;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Record\QueryRecord;
|
||||
|
||||
final class QueryResult
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* @param Collection<QueryRecord> $records
|
||||
* @param Collection<Edge> $edges
|
||||
* @param int|null $total
|
||||
*/
|
||||
public function __construct(
|
||||
public Collection $records,
|
||||
public Collection $edges,
|
||||
public ?int $total = null,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function getTotal(): ?int
|
||||
{
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
public function hasResults(): bool
|
||||
{
|
||||
return !empty($this->records);
|
||||
}
|
||||
|
||||
|
||||
public function graph(): Graph
|
||||
{
|
||||
return new Graph($this->records, $this->edges);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query;
|
||||
|
||||
use Exception;
|
||||
|
||||
|
||||
final class SubqueryNoResultException extends Exception
|
||||
{
|
||||
// Redefine the exception so message isn't optional
|
||||
public function __construct(string $message, int $code = 0, Exception $previous = null)
|
||||
{
|
||||
// make sure everything is assigned properly
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@ class QueryRecord
|
||||
public bool $isRoot,
|
||||
public ?File $_file = null,
|
||||
public array $_children = [],
|
||||
public array $_parents = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -83,9 +83,11 @@ readonly class RecordService
|
||||
_file: $uploadResult->recordFile,
|
||||
);
|
||||
|
||||
$errors = $this->recordValidator->check($schemaName, $record->data, $uniqueEdgesCollection);
|
||||
if ($errors->isNotEmpty()) {
|
||||
$this->recordValidator->throwException($errors);
|
||||
if (Status::from($status) === Status::PUBLISHED) {
|
||||
$errors = $this->recordValidator->check($schemaName, $record->data, $uniqueEdgesCollection);
|
||||
if ($errors->isNotEmpty()) {
|
||||
$this->recordValidator->throwException($errors);
|
||||
}
|
||||
}
|
||||
|
||||
RecordRepo::create($record);
|
||||
@@ -117,6 +119,7 @@ readonly class RecordService
|
||||
}
|
||||
$formattedData = $this->inputFormatter->fill($record->schema, new RecordData($data));
|
||||
|
||||
$uniqueEdgesCollection = null;
|
||||
if ($updateEdges) {
|
||||
$uniqueEdges = collect($edges)
|
||||
->map(function ($edge, $index) {
|
||||
@@ -127,9 +130,17 @@ readonly class RecordService
|
||||
->unique(fn($e) => $e['field'] . $e['source'] . $e['target'] . $e['sourceSchema'])
|
||||
->values()->toArray();
|
||||
$uniqueEdgesCollection = EdgeCollection::fromArray($uniqueEdges);
|
||||
$errors = $this->recordValidator->check($record->schema, $formattedData, $uniqueEdgesCollection);
|
||||
} else {
|
||||
$errors = $this->recordValidator->check($record->schema, $formattedData, null);
|
||||
}
|
||||
|
||||
if (Status::from($status) === Status::PUBLISHED) {
|
||||
$errors = $this->recordValidator->check($record->schema, $record->data, $uniqueEdgesCollection);
|
||||
if ($errors->isNotEmpty()) {
|
||||
$this->recordValidator->throwException($errors);
|
||||
}
|
||||
|
||||
if ($errors->isNotEmpty()) {
|
||||
$this->recordValidator->throwException($errors);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,9 +154,7 @@ readonly class RecordService
|
||||
);
|
||||
|
||||
|
||||
if ($errors->isNotEmpty()) {
|
||||
$this->recordValidator->throwException($errors);
|
||||
}
|
||||
|
||||
|
||||
RecordRepo::update($newRecord);
|
||||
if ($updateEdges) {
|
||||
@@ -237,7 +246,6 @@ readonly class RecordService
|
||||
|
||||
public function createEmpty(
|
||||
Schema $schema,
|
||||
string $userId,
|
||||
): Record
|
||||
{
|
||||
|
||||
@@ -252,7 +260,7 @@ readonly class RecordService
|
||||
id: Id::new(),
|
||||
schema: $schema->name,
|
||||
status: Status::DRAFT,
|
||||
_sys: System::newRecord($userId),
|
||||
_sys: System::newRecord($this->authService->currentUserId()),
|
||||
data: $formattedData,
|
||||
_file: null,
|
||||
);
|
||||
|
||||
@@ -42,6 +42,9 @@ class StaticGenerator
|
||||
}
|
||||
|
||||
if(!file_exists(public_path($directory."/index.html"))){
|
||||
if(file_exists(public_path($directory))){
|
||||
rmdir(public_path($directory));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
unlink(public_path($directory."/index.html"));
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>@yield('title') - Lucent Data Platform</title>
|
||||
<!-- if development -->
|
||||
@php
|
||||
echo '<script type="module" crossorigin src="http://127.0.0.1:5173/@vite/client"></script>';
|
||||
@endphp
|
||||
<script type="module" crossorigin src="http://127.0.0.1:5173/main.js"></script>
|
||||
{{-- @php--}}
|
||||
{{-- echo '<script type="module" crossorigin src="http://127.0.0.1:5173/@vite/client"></script>';--}}
|
||||
{{-- @endphp--}}
|
||||
{{-- <script type="module" crossorigin src="http://127.0.0.1:5173/main.js"></script>--}}
|
||||
<!-- if production -->
|
||||
{{-- <link rel="stylesheet" href="/vendor/lucent/dist/{{ $manifest['main.css']["file"] }}" />--}}
|
||||
{{-- <script type="module" src="/vendor/lucent/dist/{{ $manifest['main.js']["file"] }}"></script>--}}
|
||||
<link rel="stylesheet" href="/vendor/lucent/dist/{{ $manifest['main.css']["file"] }}" />
|
||||
<script type="module" src="/vendor/lucent/dist/{{ $manifest['main.js']["file"] }}"></script>
|
||||
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
|
||||
Reference in New Issue
Block a user