This commit is contained in:
2026-01-13 17:51:19 +02:00
parent 25d3b525f6
commit 268c696d64
25 changed files with 889 additions and 80 deletions
@@ -2,6 +2,7 @@
import SchemaLayout from "../../layouts/SchemaLayout.svelte";
import TextFieldProps from "./TextFieldProps.svelte";
import RelationFieldProps from "./RelationFieldProps.svelte";
import FileFieldProps from "./FileFieldProps.svelte";
import DeleteButton from "../../common/DeleteButton.svelte";
import { post } from "../../modules/remote";
import { getApp } from "../../app";
@@ -128,6 +129,8 @@
{:else if data.field.type === "relation"}
<RelationFieldProps field={data.field} schemas={data.schemas}
></RelationFieldProps>
{:else if data.field.type === "file"}
<FileFieldProps field={data.field}></FileFieldProps>
{/if}
<button type="submit">Update</button>
@@ -0,0 +1,15 @@
<script>
let { field } = $props();
</script>
<fieldset>
<label>
Min items
<input type="number" bind:value={field.props.min} />
</label>
<label>
Max items
<input type="number" bind:value={field.props.max} />
</label>
</fieldset>
@@ -1,6 +1,7 @@
<script>
import TextField from "./fields/TextField.svelte";
import RelationField from "./fields/RelationField.svelte";
import FileField from "./fields/FileField.svelte";
let {
fields,
record,
@@ -45,6 +46,14 @@
{/each}
{/if}
{/if}
{#if field.type === "file"}
{@render fileField(field, "main")}
{#if field.translatable}
{#each selectedLocales as locale (locale)}
{@render fileField(field, locale)}
{/each}
{/if}
{/if}
</div>
{/each}
@@ -70,3 +79,15 @@
edgeRecordPreviews={findFieldEdges(field, locale)}
></RelationField>
{/snippet}
{#snippet fileField(field, locale)}
<FileField
{channel}
{record}
validationError={findFieldValidationError(field, locale)}
schemaField={field}
{locale}
dataField={findDataField(field, locale)}
edgeRecordPreviews={findFieldEdges(field, locale)}
></FileField>
{/snippet}
@@ -0,0 +1,10 @@
<script>
let { schemaField, validationError } = $props();
let hasError = $derived(!!validationError);
</script>
{#if hasError}
<small id={schemaField.id + "-help"}>{validationError.message}</small>
{:else if schemaField.help != ""}
<small id={schemaField.id + "-help"}>{schemaField.help}</small>
{/if}
@@ -0,0 +1,9 @@
<script>
import { getLocaleName } from "../locale.svelte.js";
let { channel, schemaField, locale } = $props();
</script>
{#if locale !== "main"}
{getLocaleName(channel, locale)} >
{/if}
{schemaField.name} <br />
@@ -0,0 +1,226 @@
<script>
import { get, post } from "../../../modules/remote";
import { uploadFile } from "../../../modules/upload";
import { getApp } from "../../../app";
import FieldLabel from "./FieldLabel.svelte";
import FieldError from "./FieldError.svelte";
import Sortable from "../../../common/Sortable.svelte";
let {
channel,
record,
schemaField,
dataField,
locale,
validationError,
edgeRecordPreviews,
} = $props();
let originalValue = dataField?.value ?? schemaField.props.default;
let newValue = $state(originalValue);
let valuesChanged = $derived(newValue !== originalValue);
let errorMessage = $state("");
let filesInProgress = $state([]);
let uploadInProgress = $derived(filesInProgress.length > 0);
// let validationErrorState = $state(validationError);
const app = getApp();
let suggestionsLoaded = $state(false);
let suggestions = $state([]);
let selectedRecordIds = $state([]);
let dialog = $state();
function handleModalOpen(e) {
// Add logic to handle adding a record
dialog.showModal();
if (suggestionsLoaded) {
return;
}
// get(app.url("records/files"), { recordId: record.id }, (data, err) => {
suggestionsLoaded = true;
// suggestions = data;
// });
}
function handleModalClose(e) {
dialog.close();
}
function handleInsertSelected() {
suggestionsLoaded = false;
post(
app.url("edges/many"),
{
toIds: selectedRecordIds,
from: record.id,
fieldId: schemaField.id,
locale: locale,
},
(data, err) => {
suggestionsLoaded = true;
dialog.close();
edgeRecordPreviews = data.edgeRecordPreviews;
validationError = data.validationError;
},
);
}
function handleSortUpdate(updatedEdges) {
// let updatedFieldIds = updatedFields.map((f) => f.id);
// fields = fields.filter((f) => !updatedFieldIds.includes(f.id));
// fields = [...fields, ...updatedFields];
post(
app.url("records/sort-edges"),
{
ids: updatedEdges.map((e) => e.edge.id),
},
(data, err) => {
edgeRecordPreviews = updatedEdges;
},
);
}
function handleRemoveEdge(edgeId) {
post(
app.url("edges/delete"),
{
id: edgeId,
from: record.id,
fieldId: schemaField.id,
locale: locale,
},
(data, err) => {
edgeRecordPreviews = edgeRecordPreviews.filter(
(e) => e.edge.id !== edgeId,
);
validationError = data.validationError;
},
);
}
function handleFilesUpload(e) {
let files = e.target.files ? [...e.target.files] : [];
let filesUploaded = files.map((file) => {
let fileInProgress = {
pct: 0,
hasFailed: false,
name: file.name,
};
filesInProgress.push(fileInProgress);
const progress = ({ pct, isComplete }) => {
filesInProgress.find((f) => f.name === file.name).pct = pct;
if (isComplete) {
filesInProgress = filesInProgress.filter(
(f) => f.name !== file.name,
);
}
};
const error = (errorMessage) => {
filesInProgress.find((f) => f.name === file.name).hasFailed =
true;
};
uploadFile(
file,
record.id,
schemaField.id,
locale,
progress,
error,
);
});
}
</script>
<div style="min-width: 400px;">
<label>
<FieldLabel {locale} {channel} {schemaField}></FieldLabel>
</label>
<FieldError {schemaField} {validationError}></FieldError>
<button onclick={handleModalOpen}>Choose files</button>
<dialog bind:this={dialog}>
<article>
<header>
<button onclick={handleModalClose} aria-label="Close" rel="prev"
></button>
<p>
<strong>Records</strong>
</p>
</header>
{#if suggestionsLoaded}
<form>
<button onclick={handleInsertSelected}>
Insert selected
</button>
<input
oninput={handleFilesUpload}
type="file"
multiple
disabled={uploadInProgress}
/>
{#each filesInProgress as fileInProgress}
<div>
<span>{fileInProgress.name}</span>
<progress value={fileInProgress.pct} max="100" />
{#if fileInProgress.hasFailed}
<span>Error</span>
{/if}
</div>
{/each}
<table>
<tbody>
{#each suggestions as suggestion}
<tr>
<td>
<input
type="checkbox"
value={suggestion.id}
bind:group={selectedRecordIds}
/>
</td>
<td>
<a href="#">{suggestion.title}</a>
</td>
<td>
<a href="#">
{suggestion.schemaName}
</a>
</td>
</tr>
{/each}
</tbody>
</table>
</form>
{:else}
<progress />
{/if}
</article>
</dialog>
<div>
{#if edgeRecordPreviews.length == 0}
No relations exist
{:else}
<Sortable
onUpdate={handleSortUpdate}
items={edgeRecordPreviews}
itemKey="edge.id"
>
{#snippet itemView(edgeRecordPreview)}
<div>
<a href="#">{edgeRecordPreview.recordPreview.title}</a>
{edgeRecordPreview.recordPreview.schemaName}
<button
onclick={(e) =>
handleRemoveEdge(edgeRecordPreview.edge.id)}
>remove</button
>
</div>
{/snippet}
</Sortable>
{/if}
</div>
</div>
@@ -1,7 +1,9 @@
<script>
import { get, post } from "../../../modules/remote";
import { getApp } from "../../../app";
import { getLocaleName } from "../locale.svelte.js";
import FieldLabel from "./FieldLabel.svelte";
import FieldError from "./FieldError.svelte";
import Sortable from "../../../common/Sortable.svelte";
let {
channel,
record,
@@ -16,7 +18,6 @@
let valuesChanged = $derived(newValue !== originalValue);
let errorMessage = $state("");
// let validationErrorState = $state(validationError);
let hasError = $derived(!!validationError);
const app = getApp();
let suggestionsLoaded = $state(false);
@@ -57,7 +58,41 @@
(data, err) => {
suggestionsLoaded = true;
dialog.close();
edgeRecordPreviews = data;
edgeRecordPreviews = data.edgeRecordPreviews;
validationError = data.validationError;
},
);
}
function handleSortUpdate(updatedEdges) {
// let updatedFieldIds = updatedFields.map((f) => f.id);
// fields = fields.filter((f) => !updatedFieldIds.includes(f.id));
// fields = [...fields, ...updatedFields];
post(
app.url("records/sort-edges"),
{
ids: updatedEdges.map((e) => e.edge.id),
},
(data, err) => {
edgeRecordPreviews = updatedEdges;
},
);
}
function handleRemoveEdge(edgeId) {
post(
app.url("edges/delete"),
{
id: edgeId,
from: record.id,
fieldId: schemaField.id,
locale: locale,
},
(data, err) => {
edgeRecordPreviews = edgeRecordPreviews.filter(
(e) => e.edge.id !== edgeId,
);
validationError = data.validationError;
},
);
}
@@ -65,17 +100,9 @@
<div style="min-width: 400px;">
<label>
{#if locale !== "main"}
{getLocaleName(channel, locale)} >
{/if}
{schemaField.name} <br />
<FieldLabel {locale} {channel} {schemaField}></FieldLabel>
</label>
{#if hasError}
<small id={schemaField.id + "-help"}>{validationError.message}</small>
{:else if schemaField.help != ""}
<small id={schemaField.id + "-help"}>{schemaField.help}</small>
{/if}
<FieldError {schemaField} {validationError}></FieldError>
<button onclick={handleModalOpen}>Choose record</button>
<dialog bind:this={dialog}>
@@ -125,12 +152,23 @@
{#if edgeRecordPreviews.length == 0}
No relations exist
{:else}
{#each edgeRecordPreviews as edgeRecordPreview}
<div>
<a href="#">{edgeRecordPreview.recordPreview.title}</a>
{edgeRecordPreview.recordPreview.schemaName}
</div>
{/each}
<Sortable
onUpdate={handleSortUpdate}
items={edgeRecordPreviews}
itemKey="edge.id"
>
{#snippet itemView(edgeRecordPreview)}
<div>
<a href="#">{edgeRecordPreview.recordPreview.title}</a>
{edgeRecordPreview.recordPreview.schemaName}
<button
onclick={(e) =>
handleRemoveEdge(edgeRecordPreview.edge.id)}
>remove</button
>
</div>
{/snippet}
</Sortable>
{/if}
</div>
</div>
@@ -1,15 +1,14 @@
<script>
import { post } from "../../../modules/remote";
import { getApp } from "../../../app";
import { getLocaleName } from "../locale.svelte.js";
import FieldLabel from "./FieldLabel.svelte";
import FieldError from "./FieldError.svelte";
let { channel, record, schemaField, dataField, locale, validationError } =
$props();
let originalValue = dataField?.value ?? schemaField.props.default;
let newValue = $state(originalValue);
let valuesChanged = $derived(newValue !== originalValue);
let errorMessage = $state("");
// let validationErrorState = $state(validationError);
let hasError = $derived(!!validationError);
const app = getApp();
function save() {
@@ -81,33 +80,9 @@
};
</script>
<!-- <div style="position: relative;">
{#if field.selectOptions}
<Autocomplete {field} bind:value></Autocomplete>
{:else}
<input
type="text"
{id}
class="form-control"
class:is-invalid={errorMessage}
bind:value
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
{/if}
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div> -->
<div style="min-width: 400px;">
<label>
{#if locale !== "main"}
{getLocaleName(channel, locale)} >
{/if}
{schemaField.name} <br />
<FieldLabel {locale} {channel} {schemaField}></FieldLabel>
<input
type="text"
@@ -120,14 +95,8 @@
onblur={handleBlur}
oncompositionstart={handleCompositionStart}
oncompositionend={handleCompositionEnd}
aria-invalid={hasError ? "true" : ""}
/>
{#if hasError}
<small id={schemaField.id + "-help"}
>{validationError.message}</small
>
{:else if schemaField.help != ""}
<small id={schemaField.id + "-help"}>{schemaField.help}</small>
{/if}
<!-- aria-invalid={hasError ? "true" : ""} -->
<FieldError {schemaField} {validationError}></FieldError>
</label>
</div>
+58
View File
@@ -0,0 +1,58 @@
import { getApp } from "../app";
export function uploadFile(file, recordId, fieldId, locale, progress, error) {
const app = getApp();
const csrf = document.querySelector('meta[name="csrf-token"]').content;
const chunkSize = 2 * 1024 * 1024; // 2MB
const totalChunks = Math.ceil(file.size / chunkSize);
// Start the recursive process
sendChunk(0, null);
function sendChunk(currentChunk, fileId) {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append("file", chunk);
if (fileId) {
formData.append("fileId", fileId);
}
formData.append("recordId", recordId);
formData.append("fieldId", fieldId);
formData.append("isLast", currentChunk === totalChunks - 1);
formData.append("filename", file.name);
formData.append("locale", locale);
formData.append("_token", csrf);
const xhr = new XMLHttpRequest();
xhr.open("POST", app.url("upload"), true);
// Success Callback
xhr.onload = function () {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
let fileId = response.fileId;
const nextChunk = currentChunk + 1;
if (nextChunk < totalChunks) {
progress({
pct: Math.round((nextChunk / totalChunks) * 100),
isComplete: false,
});
sendChunk(nextChunk, fileId);
} else {
progress({
pct: 100,
isComplete: true,
});
}
} else {
error("Upload failed at chunk " + currentChunk);
}
};
xhr.send(formData);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php namespace Lucent\Core\Data;
class File
{
public function __construct(
public string $id,
public string $name,
public int $size,
public int $width,
public int $height,
public string $mime,
public string $checksum,
public ?string $recordId,
public bool $isShared,
) {}
}
+14
View File
@@ -0,0 +1,14 @@
<?php namespace Lucent\Core\Data;
class RecordFile
{
public function __construct(
public string $id,
public string $recordId,
public string $fileId,
public string $fieldId,
public RecordMode $mode,
public string $locale,
public int $rank,
) {}
}
+38
View File
@@ -0,0 +1,38 @@
<?php namespace Lucent\Core\File;
use Carbon\Carbon;
use Lucent\Core\Data\File;
use stdClass;
class FileModule
{
public static function toDb(File $file): array
{
return [
"id" => $file->id,
"name" => $file->name,
"size" => $file->size,
"width" => $file->width,
"height" => $file->height,
"mime" => $file->mime,
"checksum" => $file->checksum,
"record_id" => $file->recordId,
"is_shared" => $file->isShared,
];
}
public static function fromDb(stdClass $data): File
{
return new File(
id: data_get($data, "id"),
name: data_get($data, "name"),
size: data_get($data, "size"),
width: data_get($data, "width"),
height: data_get($data, "height"),
mime: data_get($data, "mime"),
checksum: data_get($data, "checksum"),
recordId: data_get($data, "recordId"),
isShared: data_get($data, "isShared"),
);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php namespace Lucent\Core\File;
use Lucent\Core\Data\RecordFile;
use Lucent\Core\Data\RecordMode;
use stdClass;
class RecordFileModule
{
public static function toDb(RecordFile $file): array
{
return [
"id" => $file->id,
"record_id" => $file->recordId,
"file_id" => $file->fileId,
"field_id" => $file->fieldId,
"mode" => $file->mode,
"locale" => $file->locale,
"rank" => $file->rank,
];
}
public static function fromDb(stdClass $data): RecordFile
{
return new RecordFile(
id: data_get($data, "id"),
recordId: data_get($data, "recordId"),
fileId: data_get($data, "file_id"),
fieldId: data_get($data, "field_id"),
mode: RecordMode::from(data_get($data, "mode")),
locale: data_get($data, "locale"),
rank: data_get($data, "rank"),
);
}
}
+105 -2
View File
@@ -14,16 +14,23 @@ class RecordValidationModule
* @param string[] $locales
* @param Field[] $schemaFields
* @param RecordField[] $recordFields
* @param Edge[] $recordEdges
* @return RecordError[]
*/
public static function validate(
array $locales,
array $schemaFields,
array $recordFields,
array $recordEdges,
): array {
$errors = [];
foreach ($schemaFields as $schemaField) {
$res = static::validateField("main", $schemaField, $recordFields);
$res = static::validateField(
"main",
$schemaField,
$recordFields,
$recordEdges,
);
$errors[] = $res;
if ($schemaField->translatable) {
foreach ($locales as $locale) {
@@ -31,6 +38,7 @@ class RecordValidationModule
$locale["id"],
$schemaField,
$recordFields,
$recordEdges,
);
$errors[] = $res;
}
@@ -47,25 +55,39 @@ class RecordValidationModule
*
* @param Field $schemaField
* @param RecordField[] $recordFields
* @param Edge[] $recordEdges
* @return array
*/
public static function validateField(
string $locale,
Field $schemaField,
array $recordFields,
array $recordEdges,
): ?RecordError {
// General Validations
$error = static::validateRequired($locale, $schemaField, $recordFields);
$error = static::validateRequired(
$locale,
$schemaField,
$recordFields,
$recordEdges,
);
if (!empty($error)) {
return $error;
}
// Type specific
$error = match ($schemaField->type) {
"text" => static::validateText(
$locale,
$schemaField,
$recordFields,
),
"relation" => static::validateRelation(
$locale,
$schemaField,
$recordEdges,
),
default => static::validateText(
$locale,
$schemaField,
@@ -80,16 +102,37 @@ class RecordValidationModule
*
* @param Field $schemaField
* @param RecordField[] $recordFields
* @param Edge[] $recordEdges
* @return ?RecordError
*/
public static function validateRequired(
string $locale,
Field $schemaField,
array $recordFields,
array $recordEdges,
): ?RecordError {
if ($schemaField->required === false) {
return null;
}
return match ($schemaField->type) {
"relation" => static::validateRequiredRelation(
$locale,
$schemaField,
$recordEdges,
),
default => static::validateRequiredField(
$locale,
$schemaField,
$recordFields,
),
};
}
private static function validateRequiredField(
string $locale,
Field $schemaField,
array $recordFields,
): ?RecordError {
$recordField = static::findField($recordFields, $schemaField, $locale);
if (empty($recordField) || empty($recordField->value)) {
@@ -102,6 +145,27 @@ class RecordValidationModule
return null;
}
private static function validateRequiredRelation(
string $locale,
Field $schemaField,
array $recordEdges,
): ?RecordError {
if (
collect($recordEdges)
->where("fieldId", $schemaField->id)
->where("locale", $locale)
->where("mode", RecordMode::DRAFT)
->isEmpty()
) {
return new RecordError(
$schemaField->id,
$locale,
"This field is required",
);
}
return null;
}
/**
*
@@ -151,6 +215,45 @@ class RecordValidationModule
return null;
}
/**
*
* @param Field $schemaField
* @param Edge[] $recordEdges
* @return ?RecordError
*/
public static function validateRelation(
string $locale,
Field $schemaField,
array $recordEdges,
): ?RecordError {
$count = collect($recordEdges)
->where("fieldId", $schemaField->id)
->where("locale", $locale)
->where("mode", RecordMode::DRAFT)
->count();
if ($schemaField->props->min > 0) {
if ($count < $schemaField->props->min) {
return new RecordError(
$schemaField->id,
$locale,
"You have to have at least {$schemaField->props->min} related records",
);
}
}
if ($schemaField->props->max > 0) {
if ($count > $schemaField->props->max) {
return new RecordError(
$schemaField->id,
$locale,
"You have to have at most {$schemaField->props->max} related records",
);
}
}
return null;
}
private static function findField(
array $recordFields,
Field $schemaField,
+21
View File
@@ -29,6 +29,11 @@ class EdgeRepo
->delete();
}
public static function delete(string $id): void
{
DB::table(self::TABLE_NAME)->where("id", $id)->delete();
}
/**
* @return Edge[]
*/
@@ -41,6 +46,22 @@ class EdgeRepo
->toArray();
}
/**
* @return ?Edge
*/
public static function findOne(string $id): ?Edge
{
$edges = DB::table(self::TABLE_NAME)->where("id", $id)->get();
return $edges->map(EdgeModule::fromDb(...))->first();
}
public static function update(Edge $edge): void
{
DB::table(self::TABLE_NAME)
->where("id", $edge->id)
->update(EdgeModule::toDb($edge));
}
public static function findLastOfField(
string $from,
string $fieldId,
+27
View File
@@ -0,0 +1,27 @@
<?php namespace Lucent\Core\Repository;
use Illuminate\Support\Facades\DB;
use Lucent\Core\Data\File;
use Lucent\Core\File\FileModule;
class FileRepo
{
const TABLE_NAME = "files";
public static function insert(File $file): void
{
DB::table(self::TABLE_NAME)->insert(FileModule::toDb($file));
}
// public static function update(File $file): void
// {
// DB::table(self::TABLE_NAME)
// ->where("id", $file->id)
// ->update(FileModule::toDb($file));
// }
public static function delete(string $fileId): void
{
DB::table(self::TABLE_NAME)->where("id", $fileId)->delete();
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php namespace Lucent\Core\Repository;
use Illuminate\Support\Facades\DB;
use Lucent\Core\Data\RecordFile;
use Lucent\Core\File\RecordFileModule;
class RecordFileRepo
{
const TABLE_NAME = "records_files";
public static function insert(RecordFile $file): void
{
DB::table(self::TABLE_NAME)->insert(RecordFileModule::toDb($file));
}
// public static function update(File $file): void
// {
// DB::table(self::TABLE_NAME)
// ->where("id", $file->id)
// ->update(FileModule::toDb($file));
// }
public static function delete(string $fileId): void
{
DB::table(self::TABLE_NAME)->where("id", $fileId)->delete();
}
}
+2
View File
@@ -71,6 +71,7 @@ class RecordRepo
->where("records_data.mode", RecordMode::DRAFT->value)
->orderBy("created_at", "desc")
->limit(5)
->distinct()
->get()
->map(fn($row) => RecordModule::recordPreviewFromDb($schemas, $row))
->toArray();
@@ -147,6 +148,7 @@ class RecordRepo
);
})
->orderBy("edges.rank", "asc")
->distinct()
->get()
->map(
@@ -7,6 +7,7 @@ class FieldProp
return match ($type) {
"text" => new TextFieldProp(min: 0, max: 0, default: ""),
"relation" => new RelationFieldProp(schemas: [], min: 0, max: 0),
"file" => new FileFieldProp(min: 0, max: 0),
default => new InvalidFieldProp(),
};
}
@@ -30,6 +31,10 @@ class FieldProp
min: $props["min"] ?? 0,
max: $props["max"] ?? 0,
),
"file" => new FileFieldProp(
min: $props["min"] ?? 0,
max: $props["max"] ?? 0,
),
default => new InvalidFieldProp(),
};
}
@@ -0,0 +1,6 @@
<?php namespace Lucent\Core\Schema\Data\FieldProp;
class FileFieldProp implements IFieldProp
{
public function __construct(public int $min, public int $max) {}
}
+12 -16
View File
@@ -7,10 +7,7 @@ use stdClass;
class EdgeRepo
{
public function __construct()
{
}
public function __construct() {}
public function insert(Edge $edge): void
{
@@ -22,7 +19,6 @@ class EdgeRepo
}
throw $e;
}
}
/**
@@ -41,7 +37,6 @@ class EdgeRepo
}
throw $e;
}
}
/**
@@ -56,25 +51,26 @@ class EdgeRepo
Database::make()->table("edges")->insert($edgesDB);
}
/**
* @return list<Edge>
*/
public function findAll(): array
{
$edges = Database::make()->table("edges")->get();
return $edges->map([$this, 'mapEdge'])->toArray();
return $edges->map([$this, "mapEdge"])->toArray();
}
public function findForSource(string $recordId): array
{
$edges = Database::make()->table("edges")->where("source", $recordId)->get();
return $edges->map([$this, 'mapEdge'])->toArray();
$edges = Database::make()
->table("edges")
->where("source", $recordId)
->get();
return $edges->map([$this, "mapEdge"])->toArray();
}
public function mapEdge(stdClass $edge): Edge
{
return new Edge(
source: $edge->source,
target: $edge->target,
@@ -82,14 +78,14 @@ class EdgeRepo
targetSchema: $edge->targetSchema,
field: $edge->field,
rank: $edge->rank,
depth: $edge->depth ?? 0
depth: $edge->depth ?? 0,
);
}
public function remove(Edge $edge): void
{
Database::make()->table("edges")
Database::make()
->table("edges")
->where("source", $edge->source)
->where("target", $edge->target)
->where("sourceSchema", $edge->sourceSchema)
@@ -100,7 +96,8 @@ class EdgeRepo
public function findLastEdgeRank(string $source, string $field): string
{
$data = Database::make()->table("edges")
$data = Database::make()
->table("edges")
->where("source", $source)
->where("field", $field)
->orderBy("rank", "desc")
@@ -108,5 +105,4 @@ class EdgeRepo
return $data->rank ?? "";
}
}
+45 -1
View File
@@ -7,14 +7,17 @@ use Illuminate\Http\Request;
use Lucent\Core\Repository\EdgeRepo;
use Lucent\Core\Repository\SchemaRepo;
use Lucent\Core\Repository\RecordRepo;
use Lucent\Core\Repository\FieldRepo;
use Lucent\Core\Data\Edge;
use Lucent\Core\Data\RecordMode;
use Lucent\Id\Id;
use Lucent\Core\Record\RecordValidationModule;
use function Lucent\Response\fail;
use function Lucent\Response\ok;
class EdgeController extends Controller
{
// Inserts edges to record, maybe I should move that to RecordController
public function postCreateMany(Request $request)
{
$from = $request->input("from");
@@ -46,7 +49,48 @@ class EdgeController extends Controller
$locale,
$from,
);
$field = FieldRepo::findOne($fieldId);
return ok($edgeRecordPreviews);
$recordEdges = array_map(fn($e) => $e->edge, $edgeRecordPreviews);
$validationError = RecordValidationModule::validateField(
$locale,
$field,
[],
$recordEdges,
);
return ok([
"edgeRecordPreviews" => $edgeRecordPreviews,
"validationError" => $validationError,
]);
}
public function postDelete(Request $request)
{
$id = $request->input("id");
$fieldId = $request->input("fieldId");
$locale = $request->input("locale");
$from = $request->input("from");
EdgeRepo::delete($id);
$schemas = SchemaRepo::all();
$edgeRecordPreviews = RecordRepo::findEdgeRecordPreviewsForField(
$schemas,
$fieldId,
$locale,
$from,
);
$field = FieldRepo::findOne($fieldId);
$recordEdges = array_map(fn($e) => $e->edge, $edgeRecordPreviews);
$validationError = RecordValidationModule::validateField(
$locale,
$field,
[],
$recordEdges,
);
return ok([
"validationError" => $validationError,
]);
}
}
+22 -6
View File
@@ -11,6 +11,7 @@ use Lucent\Core\Data\RecordField;
use Lucent\Core\Query\QueryModule;
use Lucent\Core\Repository\FieldRepo;
use Lucent\Core\Repository\SchemaRepo;
use Lucent\Core\Repository\EdgeRepo;
use Lucent\Core\Repository\RecordFieldRepo;
use Lucent\Core\Record\RecordModule;
use Lucent\Core\Record\RecordFieldModule;
@@ -190,12 +191,6 @@ class RecordController
$fields = FieldRepo::findBySchemaId($record->schemaId);
$locales = ChannelModule::get()->locales;
$validationErrors = RecordValidationModule::validate(
$locales,
$fields,
$draftData,
);
$recordStatus = RecordModule::getStatus($record);
$edgeRecordPreviews = RecordRepo::findEdgeRecordPreviewsByRecordId(
$schemas,
@@ -210,6 +205,13 @@ class RecordController
->where("edge.mode", RecordMode::LIVE)
->values()
->toArray();
$recordEdges = array_map(fn($e) => $e->edge, $edgeRecordPreviewsDraft);
$validationErrors = RecordValidationModule::validate(
$locales,
$fields,
$draftData,
$recordEdges,
);
return Svelte::view(
view: "recordEdit",
@@ -306,6 +308,7 @@ class RecordController
$locale,
$field,
[$recordField],
[],
);
return ok([
@@ -314,6 +317,19 @@ class RecordController
]);
}
public function postSortEdges(Request $request)
{
$edgeIds = $request->input("ids");
foreach ($edgeIds as $index => $id) {
$edge = EdgeRepo::findOne($id);
if ($edge) {
$edge->rank = $index;
EdgeRepo::update($edge);
}
}
return response()->json(["ids" => $edgeIds], 200);
}
public function clone(Request $request)
{
try {
+101
View File
@@ -0,0 +1,101 @@
<?php
namespace Lucent\Http\Controller;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Lucent\Core\Data\File;
use Lucent\Core\Data\RecordFile;
use Illuminate\Support\Facades\Validator;
use Lucent\Channel\ChannelService;
use Lucent\Core\Data\RecordMode;
use Lucent\Core\Repository\FileRepo;
use Lucent\Core\Repository\RecordFileRepo;
use Lucent\File\FileService;
use Lucent\File\ImageService;
use Lucent\Id\Id;
use Lucent\Query\Query;
use Lucent\Record\InputData\RecordInputData;
use Lucent\Record\RecordService;
use Lucent\Record\Status;
use function Lucent\Response\fail;
use function Lucent\Response\ok;
class UploadController
{
public function upload(Request $request)
{
// 1. Basic validation
if (!$request->hasFile("file")) {
return response()->json(["error" => "No file provided"], 400);
}
$file = $request->file("file");
$fileId = $request->input("fileId") ?? Id::new();
$filename = $request->input("filename");
$isLast = $request->input("isLast") === "true";
$recordId = $request->input("recordId");
$fieldId = $request->input("fieldId");
$locale = $request->input("locale");
// 2. Define path (e.g., storage/app/chunks/myvideo.mp4)
$extension = $extension = pathinfo($filename, PATHINFO_EXTENSION);
$tempPath = "chunks/" . $fileId . "." . $extension;
$fullTempPath = Storage::disk("local")->path($tempPath);
// 3. Append the raw chunk data to the file
// This creates the file if it doesn't exist, or adds to it if it does.
Storage::disk("local")->append(
$tempPath,
file_get_contents($file->getRealPath()),
null,
);
// 4. Finalize
if ($isLast) {
$dimensions = getimagesize($fullTempPath);
$width = $dimensions[0] ?? 0;
$height = $dimensions[1] ?? 0;
$file = new File(
id: $fileId,
name: $filename,
size: Storage::disk("local")->size($tempPath),
width: $width,
height: $height,
mime: Storage::disk("local")->mimeType($tempPath),
checksum: hash_file("md5", $fullTempPath),
recordId: $recordId,
isShared: false,
);
$finalPath = "uploads/" . $fileId . "." . $extension;
Storage::disk("local")->move($tempPath, $finalPath);
FileRepo::insert($file);
$recordFile = new RecordFile(
id: Id::new(),
recordId: $recordId,
fileId: $fileId,
fieldId: $fieldId,
mode: RecordMode::DRAFT,
locale: $locale,
rank: 0,
);
RecordFileRepo::insert($recordFile);
return response()->json([
"status" => "finished",
"fileId" => $fileId,
"location" => Storage::url($finalPath),
]);
}
return response()->json([
"status" => "chunk_saved",
"fileId" => $fileId,
]);
}
}
+10
View File
@@ -8,6 +8,7 @@ use Lucent\Http\Controller\EdgeController;
use Lucent\Http\Controller\FieldController;
use Lucent\Http\Controller\FileController;
use Lucent\Http\Controller\HomeController;
use Lucent\Http\Controller\UploadController;
use Lucent\Http\Controller\MemberController;
use Lucent\Http\Controller\RecordController;
use Lucent\Http\Controller\RevisionController;
@@ -99,11 +100,20 @@ Route::group(
"untrash",
]);
Route::post("records/sort-edges", [
RecordController::class,
"postSortEdges",
]);
// EDGES
Route::post("edges/many", [
EdgeController::class,
"postCreateMany",
]);
Route::post("edges/delete", [EdgeController::class, "postDelete"]);
// UPLOAD
Route::post("upload", [UploadController::class, "upload"]);
});
//OLD