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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user