files
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 ?? "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user