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": {
|
"main.js": {
|
||||||
"file": "assets/main.55e6cd6b.js",
|
"file": "assets/main.4387b1b7.js",
|
||||||
"src": "main.js",
|
"src": "main.js",
|
||||||
"isEntry": true,
|
"isEntry": true,
|
||||||
"css": [
|
"css": [
|
||||||
"assets/main.e0c13bde.css"
|
"assets/main.0c108e59.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"main.css": {
|
"main.css": {
|
||||||
"file": "assets/main.e0c13bde.css",
|
"file": "assets/main.0c108e59.css",
|
||||||
"src": "main.css"
|
"src": "main.css"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,17 +40,17 @@
|
|||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
<div class="offcanvas-body">
|
<div class="offcanvas-body">
|
||||||
|
|
||||||
<div class="accordion" id="accordionPanelsStayOpenExample">
|
<div class="accordion">
|
||||||
<div class="accordion-item">
|
<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"
|
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||||
data-bs-target="#panelsStayOpen-collapseOne" aria-expanded="true"
|
data-bs-target="#panelsStayOpen-collapseMain" aria-expanded="true"
|
||||||
aria-controls="panelsStayOpen-collapseOne">
|
aria-controls="panelsStayOpen-collapseMain">
|
||||||
Main
|
Main
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="panelsStayOpen-collapseOne" class="accordion-collapse collapse show"
|
<div id="panelsStayOpen-collapseMain" class="accordion-collapse collapse show"
|
||||||
aria-labelledby="panelsStayOpen-headingOne">
|
aria-labelledby="panelsStayOpen-headingMain">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<NavbarMenu
|
<NavbarMenu
|
||||||
schemas={ channel.schemas.filter((sc) => sc.isEntry)}
|
schemas={ channel.schemas.filter((sc) => sc.isEntry)}
|
||||||
@@ -60,18 +60,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="accordion-item">
|
<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"
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
data-bs-target="#panelsStayOpen-collapseTwo" aria-expanded="false"
|
data-bs-target="#panelsStayOpen-collapseOther" aria-expanded="false"
|
||||||
aria-controls="panelsStayOpen-collapseTwo">
|
aria-controls="panelsStayOpen-collapseOther">
|
||||||
Other
|
Other
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="panelsStayOpen-collapseTwo" class="accordion-collapse collapse"
|
<div id="panelsStayOpen-collapseOther" class="accordion-collapse collapse"
|
||||||
aria-labelledby="panelsStayOpen-headingTwo">
|
aria-labelledby="panelsStayOpen-headingOther">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<NavbarMenu
|
<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}
|
schema={schema}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
import SortFields from "./SortFields.svelte";
|
import SortFields from "./SortFields.svelte";
|
||||||
import AppliedFilter from "./AppliedFilter.svelte";
|
import AppliedFilter from "./AppliedFilter.svelte";
|
||||||
import {getContext} from "svelte";
|
import {getContext,createEventDispatcher} from "svelte";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
export let sort;
|
export let sort;
|
||||||
export let schema;
|
export let schema;
|
||||||
export let operators;
|
export let operators;
|
||||||
@@ -21,6 +23,21 @@
|
|||||||
|
|
||||||
let csvUrl = url.pathname + "/csv?" + url.searchParams.toString();
|
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) {
|
function uploadComplete(e) {
|
||||||
records = e.detail;
|
records = e.detail;
|
||||||
}
|
}
|
||||||
@@ -49,9 +66,15 @@
|
|||||||
on:refresh
|
on:refresh
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<form method="GET">
|
<form method="GET" on:submit={search}>
|
||||||
<input type="search" name="filter[data.{schema.fields[0].name}_regex]" placeholder="Search"
|
{#if schema.fields[0]?.name}
|
||||||
class="form-control" required>
|
<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>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -51,13 +51,13 @@
|
|||||||
<ReferenceInline
|
<ReferenceInline
|
||||||
bind:graph
|
bind:graph
|
||||||
{record}
|
{record}
|
||||||
{schema}
|
|
||||||
{field}
|
{field}
|
||||||
{validationErrors}
|
{validationErrors}
|
||||||
/>
|
/>
|
||||||
{:else if field.info.name === "reference" && field.layout === "table"}
|
{:else if field.info.name === "reference" && field.layout === "table"}
|
||||||
<ReferenceTable
|
<ReferenceTable
|
||||||
bind:graph
|
bind:graph
|
||||||
|
{id}
|
||||||
{record}
|
{record}
|
||||||
{field}
|
{field}
|
||||||
{validationErrors}
|
{validationErrors}
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
{:else if field.info.name === "reference"}
|
{:else if field.info.name === "reference"}
|
||||||
<Reference
|
<Reference
|
||||||
bind:graph
|
bind:graph
|
||||||
|
{id}
|
||||||
{record}
|
{record}
|
||||||
{schema}
|
|
||||||
{field}
|
{field}
|
||||||
{validationErrors}
|
{validationErrors}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,9 +8,8 @@
|
|||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let graph;
|
export let graph;
|
||||||
export let record;
|
export let record;
|
||||||
|
let parentEdgesByField = graph.parentEdges
|
||||||
let parentEdgesByField = graph.edges
|
.filter((edge) => edge.source !== record.id && edge.depth === 1)
|
||||||
.filter((edge) => edge.source !== record.id && edge.depth === 0)
|
|
||||||
.reduce((carry, edge) => {
|
.reduce((carry, edge) => {
|
||||||
|
|
||||||
let schemaField = edge.sourceSchema + edge.field;
|
let schemaField = edge.sourceSchema + edge.field;
|
||||||
@@ -32,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
return carry;
|
return carry;
|
||||||
}, {});
|
}, {});
|
||||||
|
console.log(parentEdgesByField)
|
||||||
let childrenEdgesByField = graph.edges
|
let childrenEdgesByField = graph.edges
|
||||||
.filter((edge) => edge.source === record.id && edge.depth === 0)
|
.filter((edge) => edge.source === record.id && edge.depth === 0)
|
||||||
.reduce((carry, edge) => {
|
.reduce((carry, edge) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import {afterUpdate, createEventDispatcher, onMount,} from "svelte";
|
import {afterUpdate, createEventDispatcher, onMount,getContext} from "svelte";
|
||||||
|
|
||||||
import {isEqual} from "lodash";
|
import {isEqual} from "lodash";
|
||||||
import FormField from "./FormField.svelte";
|
import FormField from "./FormField.svelte";
|
||||||
@@ -8,9 +8,9 @@
|
|||||||
import StatusSelect from "./StatusSelect.svelte";
|
import StatusSelect from "./StatusSelect.svelte";
|
||||||
import ErrorAlert from "../common/ErrorAlert.svelte";
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
export let schema;
|
export let schema;
|
||||||
export let schemas;
|
|
||||||
export let record;
|
export let record;
|
||||||
export let graph = {
|
export let graph = {
|
||||||
records: [],
|
records: [],
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
};
|
};
|
||||||
export let isCreateMode;
|
export let isCreateMode;
|
||||||
let originalContent;
|
let originalContent;
|
||||||
let activeContentTab = "_default";
|
let activeContentTab = "";
|
||||||
let hasUnsavedData = false;
|
let hasUnsavedData = false;
|
||||||
$: validationErrors = null;
|
$: validationErrors = null;
|
||||||
$: errorMessage = validationErrors
|
$: errorMessage = validationErrors
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
let activeFields = schema.fields.filter(
|
let activeFields = schema.fields.filter(
|
||||||
(f) => f.trashed === false && f.name !== "id"
|
(f) => f.name !== "id"
|
||||||
);
|
);
|
||||||
|
|
||||||
let tabname = "_default";
|
let tabname = "_default";
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
|
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post("/records", {
|
.post(channel.lucentUrl + "/records", {
|
||||||
record: record,
|
record: record,
|
||||||
edges: graph.edges,
|
edges: graph.edges,
|
||||||
isCreateMode: isCreateMode,
|
isCreateMode: isCreateMode,
|
||||||
@@ -159,24 +159,23 @@
|
|||||||
|
|
||||||
<div class=" mt-1">
|
<div class=" mt-1">
|
||||||
<ContentTabs
|
<ContentTabs
|
||||||
{schema}
|
{schema}
|
||||||
{isCreateMode}
|
{isCreateMode}
|
||||||
bind:active={activeContentTab}
|
bind:active={activeContentTab}
|
||||||
{record}
|
{record}
|
||||||
/>
|
/>
|
||||||
<FilePreview {record} {schema}/>
|
<FilePreview {record} {schema}/>
|
||||||
<!-- <fieldset disabled="disabled"> -->
|
<!-- <fieldset disabled="disabled"> -->
|
||||||
{#each activeFields as field (field.name)}
|
{#each activeFields as field (field.name)}
|
||||||
{#if fieldToTabs[activeContentTab].includes(field.name)}
|
{#if activeContentTab === field.group}
|
||||||
<FormField
|
<FormField
|
||||||
bind:data={record.data}
|
bind:data={record.data}
|
||||||
bind:graph={graph}
|
bind:graph={graph}
|
||||||
{field}
|
{field}
|
||||||
{schema}
|
{schema}
|
||||||
{schemas}
|
{record}
|
||||||
{record}
|
{validationErrors}
|
||||||
{validationErrors}
|
{isCreateMode}
|
||||||
{isCreateMode}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
@@ -189,26 +188,26 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if isCreateMode}
|
{#if isCreateMode}
|
||||||
<button
|
<button
|
||||||
class="ms-2 btn btn-primary btn-spinner"
|
class="ms-2 btn btn-primary btn-spinner"
|
||||||
on:click={save}
|
on:click={save}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="spinner-border spinner-border-sm"
|
class="spinner-border spinner-border-sm"
|
||||||
role="status"
|
role="status"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
disabled={!hasUnsavedData}
|
disabled={!hasUnsavedData}
|
||||||
class="ms-2 btn btn-primary btn-spinner"
|
class="ms-2 btn btn-primary btn-spinner"
|
||||||
on:click={save}
|
on:click={save}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="spinner-border spinner-border-sm"
|
class="spinner-border spinner-border-sm"
|
||||||
role="status"
|
role="status"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -63,10 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.button-file {
|
|
||||||
width: 64px;
|
|
||||||
height: 65px;
|
|
||||||
}
|
|
||||||
.card .trash-button {
|
.card .trash-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,32 @@
|
|||||||
import Icon from "../common/Icon.svelte";
|
import Icon from "../common/Icon.svelte";
|
||||||
|
|
||||||
|
|
||||||
import {createEventDispatcher, onMount} from "svelte";
|
import {createEventDispatcher, onMount, getContext} from "svelte";
|
||||||
import Preview from "../files/Preview.svelte";
|
import Preview from "../files/Preview.svelte";
|
||||||
import InlineEdit from "./InlineEdit.svelte";
|
import InlineEdit from "./InlineEdit.svelte";
|
||||||
import Reference from "../content/elements/Reference.svelte";
|
import Reference from "../content/elements/Reference.svelte";
|
||||||
import File from "../content/elements/File.svelte";
|
import File from "../content/elements/File.svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
export let isFirst;
|
export let isFirst;
|
||||||
export let isLast;
|
export let isLast;
|
||||||
export let toDelete = false;
|
export let toDelete = false;
|
||||||
export let schemas;
|
|
||||||
export let record;
|
export let record;
|
||||||
let editRecord;
|
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;
|
$: editMode = false;
|
||||||
$: expanded = false;
|
$: expanded = false;
|
||||||
|
|
||||||
function editInline(e) {
|
function editInline(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
axios
|
axios
|
||||||
.get("/records/editInline/" + record.id)
|
.get(channel.lucentUrl + "/records/editInline/" + record.id)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
record = response.data;
|
record = response.data;
|
||||||
editRecord = response.data;
|
editRecord = response.data.record;
|
||||||
|
editGraph = response.data.graph;
|
||||||
editMode = true;
|
editMode = true;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -76,7 +78,7 @@
|
|||||||
function deleteFromChannel(e) {
|
function deleteFromChannel(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
axios
|
axios
|
||||||
.post("/records/status/trashed", [record])
|
.post(channel.lucentUrl +"/records/status/trashed", [record])
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch("remove", record.id);
|
dispatch("remove", record.id);
|
||||||
})
|
})
|
||||||
@@ -92,13 +94,13 @@
|
|||||||
<p>Item was removed from the current record.</p>
|
<p>Item was removed from the current record.</p>
|
||||||
<p>
|
<p>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-outline border border-1 border-dark"
|
class="btn btn-sm btn-outline border border-1 border-dark"
|
||||||
on:click={undo}>Undo
|
on:click={undo}>Undo
|
||||||
</button
|
</button
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-danger "
|
class="btn btn-sm btn-danger "
|
||||||
on:click={deleteFromChannel}
|
on:click={deleteFromChannel}
|
||||||
>Delete completely from channel
|
>Delete completely from channel
|
||||||
</button
|
</button
|
||||||
>
|
>
|
||||||
@@ -110,26 +112,26 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if editMode === true}
|
{:else if editMode === true}
|
||||||
<InlineEdit
|
<InlineEdit
|
||||||
{schema}
|
{schema}
|
||||||
{schemas}
|
record={editRecord}
|
||||||
record={editRecord}
|
graph={editGraph}
|
||||||
isCreateMode={false}
|
isCreateMode={false}
|
||||||
on:cancel={cancel}
|
on:cancel={cancel}
|
||||||
on:inlinesaved={handleInlinesaved}
|
on:inlinesaved={handleInlinesaved}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="lx-card mt-4 bg-primary bg-opacity-10">
|
<div class="lx-card mt-4 bg-primary bg-opacity-10">
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<small class="text-muted">{schema.label}</small>
|
<small class="text-muted">{schema.label}</small>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-link"
|
class="btn btn-sm btn-link"
|
||||||
on:click|preventDefault={editInline}
|
on:click|preventDefault={editInline}
|
||||||
>
|
>
|
||||||
<Icon icon="pencil" width={12} height={12}/>
|
<Icon icon="pencil" width={12} height={12}/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-link"
|
class="btn btn-sm btn-link"
|
||||||
on:click={(e) => (expanded = !expanded)}
|
on:click={(e) => (expanded = !expanded)}
|
||||||
>
|
>
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
<Icon icon="compress" width={12} height={12}/>
|
<Icon icon="compress" width={12} height={12}/>
|
||||||
@@ -139,19 +141,19 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="dropdown d-inline-block">
|
<div class="dropdown d-inline-block">
|
||||||
<button
|
<button
|
||||||
class="btn btn-link btn-sm"
|
class="btn btn-link btn-sm"
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
<Icon icon="ellipsis"/>
|
<Icon icon="ellipsis"/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<a
|
<a
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
href="/records/{record.id}"
|
href="/records/{record.id}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>Edit in new tab
|
>Edit in new tab
|
||||||
</a>
|
</a>
|
||||||
<button class="dropdown-item" on:click={trash}>
|
<button class="dropdown-item" on:click={trash}>
|
||||||
@@ -162,16 +164,16 @@
|
|||||||
|
|
||||||
{#if !isFirst}
|
{#if !isFirst}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-outline-primary border-0"
|
class="btn btn-sm btn-outline-primary border-0"
|
||||||
on:click|preventDefault={moveup}
|
on:click|preventDefault={moveup}
|
||||||
>
|
>
|
||||||
<Icon icon="circle-chevron-up"/>
|
<Icon icon="circle-chevron-up"/>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !isLast}
|
{#if !isLast}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-outline-primary border-0"
|
class="btn btn-sm btn-outline-primary border-0"
|
||||||
on:click|preventDefault={movedn}
|
on:click|preventDefault={movedn}
|
||||||
>
|
>
|
||||||
<Icon icon="circle-chevron-down"/>
|
<Icon icon="circle-chevron-down"/>
|
||||||
</button>
|
</button>
|
||||||
@@ -189,7 +191,7 @@
|
|||||||
>{field.label}</span
|
>{field.label}</span
|
||||||
>
|
>
|
||||||
{#if field.ui === "reference"}
|
{#if field.ui === "reference"}
|
||||||
<Reference {record} {schemas} {field}/>
|
<Reference {record} {field}/>
|
||||||
{:else if field.ui === "file"}
|
{:else if field.ui === "file"}
|
||||||
<File {record} {field}/>
|
<File {record} {field}/>
|
||||||
{:else}
|
{: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>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import {getContext} from "svelte";
|
||||||
import {uniqBy} from "lodash";
|
import {insertEdges} from "./reference";
|
||||||
import PreviewCard from "../PreviewCard.svelte";
|
import PreviewCard from "../PreviewCard.svelte";
|
||||||
import {getErrorMessage} from "./errorMessage";
|
import {getErrorMessage} from "./errorMessage";
|
||||||
import {sortByField} from "../../edges/sortEdges";
|
import {sortByField} from "../../edges/sortEdges";
|
||||||
@@ -37,26 +37,7 @@
|
|||||||
|
|
||||||
function insert(e) {
|
function insert(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const recordsToInsert = e.detail.records;
|
graph = insertEdges(graph,record,e.detail.records,field.name,e.detail.action);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -68,7 +49,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="inline-card-wrapper">
|
<div class="inline-card-wrapper">
|
||||||
<ReferenceInlineButtons
|
<ReferenceInlineButtons
|
||||||
{field}
|
|
||||||
buttonClass="mt-2"
|
buttonClass="mt-2"
|
||||||
recordId={null}
|
recordId={null}
|
||||||
schemas={collections}
|
schemas={collections}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import {getContext} from "svelte";
|
import {getContext} from "svelte";
|
||||||
import {uniqBy} from "lodash";
|
|
||||||
import {previewTitle} from "../Preview";
|
import {previewTitle} from "../Preview";
|
||||||
import {getErrorMessage} from "./errorMessage";
|
import {getErrorMessage} from "./errorMessage";
|
||||||
import {sortByField} from "../../edges/sortEdges";
|
import {sortByField} from "../../edges/sortEdges";
|
||||||
@@ -8,10 +7,10 @@
|
|||||||
import Sortable from "../../libs/Sortable.svelte";
|
import Sortable from "../../libs/Sortable.svelte";
|
||||||
import RenderField from "../../content/RenderField.svelte";
|
import RenderField from "../../content/RenderField.svelte";
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
import {insertEdges} from "./reference.js";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let field;
|
export let field;
|
||||||
|
|
||||||
export let record;
|
export let record;
|
||||||
export let graph;
|
export let graph;
|
||||||
export let validationErrors;
|
export let validationErrors;
|
||||||
@@ -46,26 +45,8 @@
|
|||||||
|
|
||||||
function insert(e) {
|
function insert(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const recordsToInsert = e.detail.records;
|
graph = insertEdges(graph,record,e.detail.records,field.name,e.detail.action);
|
||||||
const action = e.detail.action;
|
console.log(graph)
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$:visibleColumns = [];
|
$:visibleColumns = [];
|
||||||
@@ -118,7 +99,7 @@
|
|||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<a
|
<a
|
||||||
class="me-2 text-decoration-none text-dark fs-6"
|
class="me-2 text-decoration-none text-dark fs-6"
|
||||||
href="/records/{record.id}"
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{previewTitle(channel.schemas, record)}
|
{previewTitle(channel.schemas, record)}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import RenderField from "../../content/RenderField.svelte";
|
import RenderField from "../../content/RenderField.svelte";
|
||||||
import Icon from "../../common/Icon.svelte";
|
import Icon from "../../common/Icon.svelte";
|
||||||
import Datalist from "./Datalist.svelte";
|
import Datalist from "./Datalist.svelte";
|
||||||
|
import {insertEdges} from "./reference.js";
|
||||||
|
|
||||||
const channel = getContext("channel");
|
const channel = getContext("channel");
|
||||||
export let field;
|
export let field;
|
||||||
@@ -62,26 +63,7 @@
|
|||||||
|
|
||||||
function insert(e, insertRecord) {
|
function insert(e, insertRecord) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const recordsToInsert = [insertRecord];
|
graph = insertEdges(graph,record,[insertRecord],field.name,e.detail.action);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateResults = debounce((e) => {
|
const updateResults = debounce((e) => {
|
||||||
|
|||||||
@@ -2,22 +2,38 @@
|
|||||||
export let field;
|
export let field;
|
||||||
export let value;
|
export let value;
|
||||||
export let search;
|
export let search;
|
||||||
function select(e, option) {
|
|
||||||
|
function select(e, option) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
value = option;
|
value = option;
|
||||||
search = "";
|
search = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if field.selectOptions}
|
{#if field.selectOptions}
|
||||||
{#each field.selectOptions as suggestion (suggestion)}
|
{#if Array.isArray(field.selectOptions)}
|
||||||
<div
|
{#each field.selectOptions as suggestion (suggestion)}
|
||||||
on:click={(e) => select(e, suggestion)}
|
<div
|
||||||
on:keypress={(e) => select(e, suggestion)}
|
on:click={(e) => select(e, suggestion)}
|
||||||
>
|
on:keypress={(e) => select(e, suggestion)}
|
||||||
|
>
|
||||||
<span class="dropdown-item">
|
<span class="dropdown-item">
|
||||||
{suggestion}
|
{suggestion}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/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}
|
{/if}
|
||||||
|
|||||||
@@ -75,7 +75,6 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
readonly={field.readonly && !isCreateMode}
|
readonly={field.readonly && !isCreateMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="dropdown-menu w-100">
|
<div class="dropdown-menu w-100">
|
||||||
<Selectlist
|
<Selectlist
|
||||||
{field}
|
{field}
|
||||||
@@ -87,7 +86,12 @@
|
|||||||
{#if value}
|
{#if value}
|
||||||
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
|
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
|
||||||
<div class="d-flex align-items-center ">
|
<div class="d-flex align-items-center ">
|
||||||
{value}
|
{#if Array.isArray(field.selectOptions)}
|
||||||
|
{value}
|
||||||
|
{:else}
|
||||||
|
{field.selectOptions[value]}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click|preventDefault={(e) => (value = "")}
|
on:click|preventDefault={(e) => (value = "")}
|
||||||
type="button"
|
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)
|
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);
|
$record = $this->recordService->createEmpty($schema);
|
||||||
$queryRecord = QueryRecord::fromRecord($record);
|
$queryRecord = QueryRecord::fromRecord($record);
|
||||||
|
|
||||||
@@ -221,12 +221,14 @@ class RecordController extends Controller
|
|||||||
->limit(1)
|
->limit(1)
|
||||||
->childrenDepth(2)
|
->childrenDepth(2)
|
||||||
->parentsDepth(1)
|
->parentsDepth(1)
|
||||||
->tree();
|
->run();
|
||||||
|
|
||||||
|
$record = $graph->records->first();
|
||||||
|
|
||||||
return ok(
|
return ok(
|
||||||
[
|
[
|
||||||
"graph" => $graph->toArray(),
|
"graph" => toArray($graph),
|
||||||
"record" => $graph->first()->toArray(),
|
"record" => toArray($record)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -287,7 +289,7 @@ class RecordController extends Controller
|
|||||||
$newGraph = $this->query
|
$newGraph = $this->query
|
||||||
->filter(["id" => $recordId])
|
->filter(["id" => $recordId])
|
||||||
->limit(10)
|
->limit(10)
|
||||||
->childrenDepth(2)
|
->childrenDepth(1)
|
||||||
->parentsDepth(1)
|
->parentsDepth(1)
|
||||||
->run();
|
->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 function __construct(
|
||||||
public Collection $records,
|
public Collection $records,
|
||||||
public Collection $edges,
|
public Collection $edges,
|
||||||
|
public Collection $parentEdges,
|
||||||
public ?int $total = null,
|
public ?int $total = null,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -66,6 +67,15 @@ final class Graph
|
|||||||
|
|
||||||
public function tree(): Collection
|
public function tree(): Collection
|
||||||
{
|
{
|
||||||
|
return $this->getRootRecords()
|
||||||
|
->map([$this, 'findParents'])
|
||||||
|
->map([$this, 'findChildren'])
|
||||||
|
;
|
||||||
|
|
||||||
|
return $rootRecords;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return $this->records->filter(function (QueryRecord $record) {
|
return $this->records->filter(function (QueryRecord $record) {
|
||||||
return $this->edges->filter(fn(Edge $ed) => $ed->target == $record->id)->isEmpty();
|
return $this->edges->filter(fn(Edge $ed) => $ed->target == $record->id)->isEmpty();
|
||||||
})->values()
|
})->values()
|
||||||
@@ -81,7 +91,6 @@ final class Graph
|
|||||||
foreach ($recordEdges as $element) {
|
foreach ($recordEdges as $element) {
|
||||||
$groupRecordEdges[$element->field][] = $element;
|
$groupRecordEdges[$element->field][] = $element;
|
||||||
}
|
}
|
||||||
|
|
||||||
$children = [];
|
$children = [];
|
||||||
foreach ($groupRecordEdges as $field => $edges) {
|
foreach ($groupRecordEdges as $field => $edges) {
|
||||||
|
|
||||||
@@ -99,4 +108,30 @@ final class Graph
|
|||||||
return $record;
|
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\Database\Query\Builder;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Lucent\Channel\ChannelService;
|
|
||||||
use Lucent\Edge\Edge;
|
use Lucent\Edge\Edge;
|
||||||
use Lucent\Primitive\Collection;
|
use Lucent\Primitive\Collection;
|
||||||
|
use Lucent\Query\Filter\AndFilter;
|
||||||
|
use Lucent\Query\Filter\OrFilter;
|
||||||
use Lucent\Record\InputFormatter;
|
use Lucent\Record\InputFormatter;
|
||||||
use Lucent\Record\QueryRecord;
|
use Lucent\Record\QueryRecord;
|
||||||
use Lucent\Record\Record;
|
use Lucent\Record\Record;
|
||||||
@@ -14,11 +15,14 @@ use Lucent\Record\Record;
|
|||||||
final class Query
|
final class Query
|
||||||
{
|
{
|
||||||
|
|
||||||
public Filter $filter;
|
/**
|
||||||
|
* @var array<AndFilter> $filters
|
||||||
|
*/
|
||||||
|
public array $filters;
|
||||||
public QueryOptions $options;
|
public QueryOptions $options;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly ChannelService $channelService,
|
public readonly FilterParser $filterParser,
|
||||||
public readonly InputFormatter $inputFormatter,
|
public readonly InputFormatter $inputFormatter,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -27,7 +31,13 @@ final class Query
|
|||||||
|
|
||||||
public function filter(array $filterArguments): 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;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,16 +72,22 @@ final class Query
|
|||||||
->get()->toArray();
|
->get()->toArray();
|
||||||
}
|
}
|
||||||
$resultsRecordsUnique = collect(array_merge($resultsRecords, $edgeRecords))->unique("id")->values()->toArray();
|
$resultsRecordsUnique = collect(array_merge($resultsRecords, $edgeRecords))->unique("id")->values()->toArray();
|
||||||
$resultEdges = collect(array_merge($resultChildrenEdges, $resultParentEdges))
|
// $resultEdges = collect(array_merge($resultChildrenEdges, $resultParentEdges))
|
||||||
->unique(fn($edge) => $edge->source . $edge->target . $edge->field)
|
// ->unique(fn($edge) => $edge->source . $edge->target . $edge->field)
|
||||||
->toArray();
|
// ->toArray();
|
||||||
|
|
||||||
|
$this->reset();
|
||||||
return $this->formatRecords($resultsRecordsUnique, $resultEdges);
|
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) {
|
$queryRecords = collect($records)->map(function ($recordData) {
|
||||||
|
|
||||||
@@ -89,10 +105,17 @@ final class Query
|
|||||||
|
|
||||||
})->sortBy("rank")->values()->toArray();
|
})->sortBy("rank")->values()->toArray();
|
||||||
|
|
||||||
|
$queryParentEdges = collect($parentEdges)->map(function ($edgeData) {
|
||||||
|
|
||||||
|
return Edge::fromArray((array)$edgeData);
|
||||||
|
|
||||||
|
})->sortBy("rank")->values()->toArray();
|
||||||
|
|
||||||
|
|
||||||
return new Graph(
|
return new Graph(
|
||||||
new Collection($queryRecords),
|
new Collection($queryRecords),
|
||||||
new Collection($queryEdges),
|
new Collection($queryEdges),
|
||||||
|
new Collection($queryParentEdges),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,61 +126,21 @@ final class Query
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private function parseFilters(Builder $query): Builder
|
private function parseFilters(Builder $query): Builder
|
||||||
{
|
{
|
||||||
$filters = $this->filter->run(new Query($this->channelService, $this->inputFormatter));
|
foreach ($this->filters as $filter) {
|
||||||
$ignoredFilters = [];
|
$query = $this->filterParser->parse($query, $filter);
|
||||||
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"]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$query->whereIn("status", $this->options->status);
|
$query->whereIn("status", $this->options->status);
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws SubqueryNoResultException
|
|
||||||
*/
|
|
||||||
private function mainQuery(): array
|
private function mainQuery(): array
|
||||||
{
|
{
|
||||||
$query = DB::table("records");
|
$query = DB::table("records");
|
||||||
$query = $this->parseFilters($query);
|
$query = $this->parseFilters($query);
|
||||||
if($this->options->limit > 0){
|
if ($this->options->limit > 0) {
|
||||||
$query->limit($this->options->limit);
|
$query->limit($this->options->limit);
|
||||||
$query->offset($this->options->skip);
|
$query->offset($this->options->skip);
|
||||||
}
|
}
|
||||||
@@ -174,14 +157,19 @@ final class Query
|
|||||||
function getChildren(array $ids): array
|
function getChildren(array $ids): array
|
||||||
{
|
{
|
||||||
$subquery = DB::table('edges AS g')
|
$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 '))
|
||||||
->whereIn('source', $ids)
|
->whereIn('source', $ids);
|
||||||
->limit($this->options->childrenLimit)
|
|
||||||
|
if (!empty($this->options->childrenFields)) {
|
||||||
|
$subquery->whereIn('field', $this->options->childrenFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
$subquery->limit($this->options->childrenLimit)
|
||||||
->unionAll(
|
->unionAll(
|
||||||
DB::table(DB::raw("edges AS g, search_graph AS sg "))
|
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')
|
->selectRaw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field,sg.depth + 1 as depth')
|
||||||
->whereRaw("g.source = sg.target")
|
->whereRaw("g.source = sg.target")
|
||||||
->where("sg.depth", "<=", $this->options->childrenDepth)
|
->where("depth", "<", $this->options->childrenDepth)
|
||||||
->orderBy("rank")
|
->orderBy("rank")
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -191,18 +179,17 @@ final class Query
|
|||||||
->get()->toArray();
|
->get()->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private
|
private function getParents(array $ids): array
|
||||||
function getParents(array $ids): array
|
|
||||||
{
|
{
|
||||||
$subquery = DB::table('edges AS g')
|
$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)
|
->limit($this->options->parentsLimit)
|
||||||
->whereIn('g.target', $ids)
|
->whereIn('g.target', $ids)
|
||||||
->unionAll(
|
->unionAll(
|
||||||
DB::table(DB::raw("edges AS g, search_graph AS sg "))
|
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')
|
->selectRaw('g.source,g.target,g.rank,"g"."sourceSchema","g"."targetSchema",g.field,sg.depth + 1 as depth')
|
||||||
->whereRaw("g.target = sg.source")
|
->whereRaw("g.target = sg.source")
|
||||||
->where("sg.depth", "<=", $this->options->parentsDepth)
|
->where("depth", "<", $this->options->parentsDepth)
|
||||||
->orderBy("rank")
|
->orderBy("rank")
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -251,6 +238,18 @@ final class Query
|
|||||||
return $this;
|
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
|
public function parentsDepth(int $depth): Query
|
||||||
{
|
{
|
||||||
$this->options->parentsDepth = $depth;
|
$this->options->parentsDepth = $depth;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ final class QueryOptions
|
|||||||
public int $parentsDepth = -1,
|
public int $parentsDepth = -1,
|
||||||
public int $childrenLimit = -1,
|
public int $childrenLimit = -1,
|
||||||
public int $parentsLimit = -1,
|
public int $parentsLimit = -1,
|
||||||
|
public array $childrenFields = [],
|
||||||
|
public array $parentFields = [],
|
||||||
public array $sort = [],
|
public array $sort = [],
|
||||||
public array $status = ["published", "draft"]
|
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 bool $isRoot,
|
||||||
public ?File $_file = null,
|
public ?File $_file = null,
|
||||||
public array $_children = [],
|
public array $_children = [],
|
||||||
|
public array $_parents = [],
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,9 +83,11 @@ readonly class RecordService
|
|||||||
_file: $uploadResult->recordFile,
|
_file: $uploadResult->recordFile,
|
||||||
);
|
);
|
||||||
|
|
||||||
$errors = $this->recordValidator->check($schemaName, $record->data, $uniqueEdgesCollection);
|
if (Status::from($status) === Status::PUBLISHED) {
|
||||||
if ($errors->isNotEmpty()) {
|
$errors = $this->recordValidator->check($schemaName, $record->data, $uniqueEdgesCollection);
|
||||||
$this->recordValidator->throwException($errors);
|
if ($errors->isNotEmpty()) {
|
||||||
|
$this->recordValidator->throwException($errors);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RecordRepo::create($record);
|
RecordRepo::create($record);
|
||||||
@@ -117,6 +119,7 @@ readonly class RecordService
|
|||||||
}
|
}
|
||||||
$formattedData = $this->inputFormatter->fill($record->schema, new RecordData($data));
|
$formattedData = $this->inputFormatter->fill($record->schema, new RecordData($data));
|
||||||
|
|
||||||
|
$uniqueEdgesCollection = null;
|
||||||
if ($updateEdges) {
|
if ($updateEdges) {
|
||||||
$uniqueEdges = collect($edges)
|
$uniqueEdges = collect($edges)
|
||||||
->map(function ($edge, $index) {
|
->map(function ($edge, $index) {
|
||||||
@@ -127,9 +130,17 @@ readonly class RecordService
|
|||||||
->unique(fn($e) => $e['field'] . $e['source'] . $e['target'] . $e['sourceSchema'])
|
->unique(fn($e) => $e['field'] . $e['source'] . $e['target'] . $e['sourceSchema'])
|
||||||
->values()->toArray();
|
->values()->toArray();
|
||||||
$uniqueEdgesCollection = EdgeCollection::fromArray($uniqueEdges);
|
$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);
|
RecordRepo::update($newRecord);
|
||||||
if ($updateEdges) {
|
if ($updateEdges) {
|
||||||
@@ -237,7 +246,6 @@ readonly class RecordService
|
|||||||
|
|
||||||
public function createEmpty(
|
public function createEmpty(
|
||||||
Schema $schema,
|
Schema $schema,
|
||||||
string $userId,
|
|
||||||
): Record
|
): Record
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -252,7 +260,7 @@ readonly class RecordService
|
|||||||
id: Id::new(),
|
id: Id::new(),
|
||||||
schema: $schema->name,
|
schema: $schema->name,
|
||||||
status: Status::DRAFT,
|
status: Status::DRAFT,
|
||||||
_sys: System::newRecord($userId),
|
_sys: System::newRecord($this->authService->currentUserId()),
|
||||||
data: $formattedData,
|
data: $formattedData,
|
||||||
_file: null,
|
_file: null,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ class StaticGenerator
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!file_exists(public_path($directory."/index.html"))){
|
if(!file_exists(public_path($directory."/index.html"))){
|
||||||
|
if(file_exists(public_path($directory))){
|
||||||
|
rmdir(public_path($directory));
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
unlink(public_path($directory."/index.html"));
|
unlink(public_path($directory."/index.html"));
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
<title>@yield('title') - Lucent Data Platform</title>
|
<title>@yield('title') - Lucent Data Platform</title>
|
||||||
<!-- if development -->
|
<!-- if development -->
|
||||||
@php
|
{{-- @php--}}
|
||||||
echo '<script type="module" crossorigin src="http://127.0.0.1:5173/@vite/client"></script>';
|
{{-- echo '<script type="module" crossorigin src="http://127.0.0.1:5173/@vite/client"></script>';--}}
|
||||||
@endphp
|
{{-- @endphp--}}
|
||||||
<script type="module" crossorigin src="http://127.0.0.1:5173/main.js"></script>
|
{{-- <script type="module" crossorigin src="http://127.0.0.1:5173/main.js"></script>--}}
|
||||||
<!-- if production -->
|
<!-- if production -->
|
||||||
{{-- <link rel="stylesheet" href="/vendor/lucent/dist/{{ $manifest['main.css']["file"] }}" />--}}
|
<link rel="stylesheet" href="/vendor/lucent/dist/{{ $manifest['main.css']["file"] }}" />
|
||||||
{{-- <script type="module" src="/vendor/lucent/dist/{{ $manifest['main.js']["file"] }}"></script>--}}
|
<script type="module" src="/vendor/lucent/dist/{{ $manifest['main.js']["file"] }}"></script>
|
||||||
|
|
||||||
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
|
|||||||
Reference in New Issue
Block a user