wip upload files and select

This commit is contained in:
2026-05-05 19:21:59 +03:00
parent bd01e5c32c
commit 16e50e2d49
13 changed files with 439 additions and 208 deletions
@@ -42,10 +42,6 @@
window.location = url; window.location = url;
} }
} }
function uploadComplete(e) {
records = e.detail;
}
</script> </script>
<div class="toolbar"> <div class="toolbar">
+11 -15
View File
@@ -1,15 +1,14 @@
<script> <script>
import { createEventDispatcher, getContext } from "svelte"; import { createEventDispatcher, getContext } from "svelte";
import Icon from "../common/Icon.svelte"; import Icon from "../common/Icon.svelte";
import Index from "../content/Index.svelte"; import FileIndex from "./FileIndex.svelte";
import axios from "axios";
let dialogEl; let dialogEl;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const channel = getContext("channel"); const channel = getContext("channel");
$: data = {}; $: files = [];
let selectedRecords = []; $: selectedRecords = [];
// onMount(() => { // onMount(() => {
// load(); // load();
// }); // });
@@ -23,11 +22,11 @@
selectedRecords = []; selectedRecords = [];
} }
function load(schema) { function load(recordId) {
axios fetch(channel.lucentUrl + "/records/files/?recordId=" + recordId)
.get(channel.lucentUrl + "/content/" + schema) .then((response) => response.json())
.then((response) => { .then((json) => {
data = response.data; files = json;
}) })
.catch((error) => console.log(error)); .catch((error) => console.log(error));
} }
@@ -37,7 +36,6 @@
dispatch("insert", { dispatch("insert", {
records: selectedRecords, records: selectedRecords,
action: "insert", action: "insert",
schema: data.schema.name,
}); });
} }
@@ -49,14 +47,13 @@
}); });
} }
export function open(schema) { export function open(recordId) {
dialogEl.showModal(); dialogEl.showModal();
load(schema); load(recordId);
} }
</script> </script>
<dialog bind:this={dialogEl}> <dialog bind:this={dialogEl}>
{#if data.schema}
<div class="dialog-header"> <div class="dialog-header">
<button <button
type="button" type="button"
@@ -91,7 +88,6 @@
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<Index {...data} bind:selected={selectedRecords}></Index> <FileIndex {files} bind:selected={selectedRecords}></FileIndex>
</div> </div>
{/if}
</dialog> </dialog>
+119
View File
@@ -0,0 +1,119 @@
<script>
import { getContext } from "svelte";
import Icon from "../common/Icon.svelte";
import Checkbox from "../common/Checkbox.svelte";
import Preview from "../files/Preview.svelte";
import { fileurl } from "../files/imageserver";
const channel = getContext("channel");
export let files = [];
export let selected = [];
export let isWritable = true;
function eventToggleAll(e) {
selected = toggleAll(e, files, selected);
}
function select(file) {
selected = selectFile(file, selected);
}
export const toggleAll = (e, files, selected) => {
if (selected.length === files.length) {
return [];
}
e.currentTarget.checked = selected.length > 0;
return files;
};
export const selectFile = (file, selected) => {
let fileExists = selected.find((r) => r.id === file.id);
if (fileExists) {
return selected.filter((r) => r.id !== file.id);
}
return [...selected, file];
};
</script>
<div class="table mt-5">
<table>
<thead>
<tr>
{#if isWritable}
<th>
<Checkbox
value=""
on:change={eventToggleAll}
indeterminate={selected.length > 0 &&
selected.length < files.length}
checked={selected.length === files.length}
></Checkbox>
</th>
{/if}
<th></th>
</tr>
</thead>
<tbody>
{#each files as file (file.id)}
<tr>
<td class="title-td">
<div class="title-td-contents">
{#if isWritable}
<Checkbox
on:change={() => select(file)}
checked={selected.find(
(s) => s.id === file.id,
)}
value={file}
></Checkbox>
{/if}
<div class="file-table-row">
<Preview
{file}
size={file.width > 0 ? "medium" : "small"}
/>
<div>
{file.filename}
<span
>{(file.size / 1024).toFixed(1)}kB</span
>
{#if file.width > 0}
<span
>{file.width +
"x" +
file.height}</span
>
{/if}
<a
href={fileurl(channel, file)}
target="_blank"
>
Download
</a>
</div>
</div>
</div>
</td>
<!-- <RecordRow
{record}
{graph}
{schema}
{visibleColumns}
{sortParam}
{sortField}
{users}
/> -->
<!-- <td>
<Avatar
name={usernameById(users, record._sys.updatedBy)}
side={24}
/>
</td> -->
</tr>
{/each}
</tbody>
</table>
</div>
+15 -16
View File
@@ -3,7 +3,7 @@
import { imgurl } from "./imageserver.js"; import { imgurl } from "./imageserver.js";
import { getContext } from "svelte"; import { getContext } from "svelte";
export let record; export let file;
const channel = getContext("channel"); const channel = getContext("channel");
export let size = "small"; export let size = "small";
export let showFilename = false; export let showFilename = false;
@@ -28,43 +28,43 @@
fontSize = "13"; fontSize = "13";
} }
</script> </script>
<div style="display: flex;align-items: center;gap: 5px;">
{#if record}
{#if record._file.mime.startsWith("image")} <div style="display: flex;align-items: center;gap: 5px;">
{#if file}
{#if file.mime.startsWith("image")}
<!-- href={imgurl(record)} --> <!-- href={imgurl(record)} -->
<a <a
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/files/{file.id}"
title={record._file.originalName} title={file.filename}
style="width:{imageSide}px;height:{imageSide}px" style="width:{imageSide}px;height:{imageSide}px"
> >
<img <img
class="rounded w-100" class="rounded w-100"
src={imgurl(channel,record)} src={imgurl(channel, file)}
alt={record._file.path} alt={file.path}
/> />
</a> </a>
{:else} {:else}
<a <a
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/files/{file.id}"
title={record._file.path} title={file.path}
class="file-preview-small" class="file-preview-small"
style="width:{imageSide}px;height:{imageSide}px" style="width:{imageSide}px;height:{imageSide}px"
> >
<Icon icon="file" width={fileSide} height={fileSide} /> <Icon icon="file" width={fileSide} height={fileSide} />
<span class="ms-2" <span class="ms-2"
>.{record._file.path.split(".").pop().toLowerCase()}</span >.{file.path.split(".").pop().toLowerCase()}</span
> >
</a> </a>
{/if} {/if}
{/if} {/if}
{#if showFilename} {#if showFilename}
<a <a
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/files/{file.id}"
title={record._file.path} title={file.path}
class="preview-file-filename lx-small-text text-decoration-none" class="preview-file-filename lx-small-text text-decoration-none"
>{record._file.path} </a >{file.path}
> </a>
{/if} {/if}
</div> </div>
@@ -73,5 +73,4 @@
border-radius: 12px; border-radius: 12px;
padding: 4px; padding: 4px;
} }
</style> </style>
+14 -15
View File
@@ -1,29 +1,28 @@
export function imgurl(channel, record) { export function imgurl(channel, file) {
if (record._file.mime === "image/svg+xml") { if (file.mime === "image/svg+xml") {
return fileurl(channel, record); return fileurl(channel, file);
} }
return channel.disks[record._file.disk] + `/thumbs/${record._file.path}`; return channel.filesUrl + `/thumbs/${file.path}`;
} }
export function fileurl(channel, record) { export function fileurl(channel, file) {
return channel.disks[record._file.disk] + `/${record._file.path}`; return channel.filesUrl + `/${file.path}`;
} }
export function htmlurl(channel, record, preset) { export function htmlurl(channel, file, preset) {
let html = ""; let html = "";
let url = fileurl(channel, record) let url = fileurl(channel, file);
if (record._file.width > 0) { if (file.width > 0) {
let presetUrl = url; let presetUrl = url;
if (preset) { if (preset) {
presetUrl = channel.disks[record._file.disk] + `/templates/${preset}/${record._file.path}`; presetUrl = channel.filesUrl + `/templates/${preset}/${file.path}`;
} }
html = `<img src="${presetUrl}" alt="${record._file.path}" />` html = `<img src="${presetUrl}" alt="${file.path}" />`;
} else if (record._file.mime === "image/svg+xml") { } else if (file.mime === "image/svg+xml") {
html = `<img src="${url}" alt="${record._file.path}"/>` html = `<img src="${url}" alt="${file.path}"/>`;
} else { } else {
html = `<a href="${url}">${record._file.originalName}</a>` html = `<a href="${url}">${file.originalName}</a>`;
} }
return html; return html;
+7 -12
View File
@@ -22,11 +22,6 @@
); );
} }
function openBrowseModal(e, schema) {
e.preventDefault();
browseModal.open(schema);
}
async function reorder(e) { async function reorder(e) {
graph.edges = await sortByField( graph.edges = await sortByField(
e.detail.source, e.detail.source,
@@ -52,15 +47,15 @@
function uploadComplete(e) { function uploadComplete(e) {
records = e.detail; records = e.detail;
} }
function openBrowseModal(e) {
e.preventDefault();
browseModal.open(record.id);
}
</script> </script>
<div class="mb-0"> <div class="mb-0">
<!-- <button <button class="button" on:click={openBrowseModal}> Browse </button>
class="button"
on:click={(e) => openFileModal(e, collections[0].name)}
>
Browse
</button> -->
<div> <div>
<Uploader recordId={record.id} on:uploadComplete={uploadComplete} /> <Uploader recordId={record.id} on:uploadComplete={uploadComplete} />
@@ -80,4 +75,4 @@
{/each} {/each}
</Sortable> </Sortable>
{/if} --> {/if} -->
<!-- <FileDialog bind:this={browseModal} on:insert={insert}></FileDialog> --> <FileDialog bind:this={browseModal} on:insert={insert}></FileDialog>
+6 -4
View File
@@ -28,18 +28,20 @@ final class Channel
) { ) {
$this->lucentUrl = $url . "/lucent"; $this->lucentUrl = $url . "/lucent";
$this->filesUrl = $this->makeFilesUrl(); $this->filesUrl = $this->makeFilesUrl();
$this->previewTargetUrl = $url . "/" . $previewTarget; $this->previewTargetUrl = $url . "/" . $previewTarget;
} }
private function makeFilesUrl(): string private function makeFilesUrl(): string
{ {
return match (config("filesystems.disks.lucent.driver")) { $lucentDisk = config("lucent.disk");
"s3" => config("filesystems.disks.lucent.endpoint") . return match (config("filesystems.disks.$lucentDisk.driver")) {
"s3" => config("filesystems.disks.$lucentDisk.endpoint") .
"/" . "/" .
config("filesystems.disks.lucent.bucket"), config("filesystems.disks.$lucentDisk.bucket"),
"local" => $this->url . "local" => $this->url .
"/storage" . "/storage" .
config("filesystems.disks.lucent.endpoint"), config("filesystems.disks.$lucentDisk.endpoint"),
default => "", default => "",
}; };
} }
+4 -4
View File
@@ -110,9 +110,9 @@ class SetupDatabase extends Command
Blueprint $table, Blueprint $table,
) { ) {
$table->uuid("id")->primary(); $table->uuid("id")->primary();
$table->uuid("record"); $table->uuid("recordId");
$table->string("name"); $table->string("filename");
$table->string("ogName"); $table->jsonb("captions");
$table->string("mime"); $table->string("mime");
$table->string("path"); $table->string("path");
$table->integer("size"); $table->integer("size");
@@ -122,7 +122,7 @@ class SetupDatabase extends Command
}); });
DB::statement( DB::statement(
"CREATE INDEX ON " . $this->prefix . "files (record)", "CREATE INDEX ON " . $this->prefix . "files (recordId)",
); );
} }
} }
+35 -2
View File
@@ -2,12 +2,14 @@
namespace Lucent\Data; namespace Lucent\Data;
use stdClass;
class File class File
{ {
function __construct( function __construct(
public readonly string $id, public readonly string $id,
public readonly string $recordId, public readonly string $recordId,
public readonly string $originalName, public readonly string $filename,
public readonly string $mime, public readonly string $mime,
public readonly string $path, public readonly string $path,
public readonly int $size, public readonly int $size,
@@ -21,7 +23,22 @@ class File
return new File( return new File(
id: data_get($data, "id"), id: data_get($data, "id"),
recordId: data_get($data, "recordId"), recordId: data_get($data, "recordId"),
originalName: data_get($data, "originalName"), filename: data_get($data, "filename"),
mime: data_get($data, "mime"),
path: data_get($data, "path"),
size: data_get($data, "size"),
width: data_get($data, "width"),
height: data_get($data, "height"),
checksum: data_get($data, "checksum"),
);
}
public static function fromDB(stdClass $data): File
{
return new File(
id: data_get($data, "id"),
recordId: data_get($data, "recordId"),
filename: data_get($data, "filename"),
mime: data_get($data, "mime"), mime: data_get($data, "mime"),
path: data_get($data, "path"), path: data_get($data, "path"),
size: data_get($data, "size"), size: data_get($data, "size"),
@@ -35,4 +52,20 @@ class File
{ {
return \json_decode(\json_encode($this), true); return \json_decode(\json_encode($this), true);
} }
public function toDB(): array
{
return [
"id" => $this->id,
"recordId" => $this->recordId,
"filename" => $this->filename,
"mime" => $this->mime,
"path" => $this->path,
"size" => $this->size,
"width" => $this->width,
"height" => $this->height,
"checksum" => $this->checksum,
"captions" => "[]", // for the future
];
}
} }
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace Lucent\File;
use Lucent\Data\File as DataFile;
use Lucent\Database\Database;
use Lucent\Data\File;
class FileRepo
{
public function __construct() {}
public static function create(DataFile $file): void
{
Database::make()->table("lucent_files")->insert($file->toDB());
}
/**
* @return File[]
*/
public static function byRecordId(string $recordId): array
{
return Database::make()
->table("lucent_files")
->where("recordId", $recordId)
->get()
->map(File::fromDB(...))
->toArray();
}
}
+16 -4
View File
@@ -85,10 +85,10 @@ class FileService
[$width, $height] = $this->isImage($mimetype) [$width, $height] = $this->isImage($mimetype)
? getimagesize($file) ? getimagesize($file)
: [0, 0]; : [0, 0];
return new DataFile( $dataFile = new DataFile(
id: Id::new(), id: Id::new(),
recordId: $recordId, recordId: $recordId,
originalName: $originalFilename, filename: $originalFilename,
mime: $mimetype, mime: $mimetype,
path: $path, path: $path,
size: $file->getSize(), size: $file->getSize(),
@@ -96,6 +96,9 @@ class FileService
height: $height, height: $height,
checksum: $checksum, checksum: $checksum,
); );
FileRepo::create($dataFile);
return $dataFile;
} }
private function createFileName( private function createFileName(
@@ -112,10 +115,11 @@ class FileService
private function isImage(string $mimetype): bool private function isImage(string $mimetype): bool
{ {
$imageMimes = [ $imageMimes = [
"image/webp",
"image/gif",
"image/jpeg", "image/jpeg",
"image/png", "image/png",
"image/avif",
"image/webp",
"image/gif",
"image/tiff", "image/tiff",
]; ];
return in_array($mimetype, $imageMimes); return in_array($mimetype, $imageMimes);
@@ -156,4 +160,12 @@ class FileService
$image = $originalImage->fit(300, 300); $image = $originalImage->fit(300, 300);
$disk->put($thumbDir, $image->encode("webp", 75)); $disk->put($thumbDir, $image->encode("webp", 75));
} }
/**
* @return DataFile[]
*/
public function filesForRecord(string $recordId): array
{
return FileRepo::byRecordId($recordId);
}
} }
+9
View File
@@ -15,6 +15,7 @@ use Lucent\Record\InputData\RecordInputData;
use Lucent\Record\Manager; use Lucent\Record\Manager;
use Lucent\Record\QueryRecord; use Lucent\Record\QueryRecord;
use Lucent\Record\RecordService; use Lucent\Record\RecordService;
use Lucent\File\FileService;
use Lucent\Record\Status; use Lucent\Record\Status;
use Lucent\Schema\System; use Lucent\Schema\System;
use Lucent\Schema\Ui\Reference; use Lucent\Schema\Ui\Reference;
@@ -30,6 +31,7 @@ class RecordController extends Controller
private readonly RecordService $recordService, private readonly RecordService $recordService,
private readonly AccountService $accountService, private readonly AccountService $accountService,
private readonly ChannelService $channelService, private readonly ChannelService $channelService,
private readonly FileService $fileService,
private readonly Svelte $svelte, private readonly Svelte $svelte,
private readonly Query $query, private readonly Query $query,
private readonly Manager $recordManager, private readonly Manager $recordManager,
@@ -493,4 +495,11 @@ class RecordController extends Controller
} }
return ok(); return ok();
} }
public function files(Request $request)
{
$recordId = $request->input("recordId");
return $this->fileService->filesForRecord($recordId);
}
} }
+103 -62
View File
@@ -12,89 +12,130 @@ use Lucent\Http\Controller\RecordController;
use Lucent\Http\Controller\RevisionController; use Lucent\Http\Controller\RevisionController;
use Lucent\Http\Controller\SetupController; use Lucent\Http\Controller\SetupController;
Route::get("/lucent/setup", [SetupController::class, "setup"]);
Route::get("/lfs-{disk}/{any}", [FileController::class, "fromDisk"])->where(
"any",
".*",
);
Route::get('/lucent/setup', [SetupController::class, 'setup']); Route::group(
Route::get('/lfs-{disk}/{any}', [FileController::class, 'fromDisk'])->where('any', '.*'); [
"middleware" => ["web"],
"prefix" => "lucent",
Route::group([ ],
'middleware' => ['web'], function () {
'prefix' => "lucent" Route::middleware(["lucent.guest"])->group(function () {
], function () { Route::get("/", [AuthController::class, "login"]);
Route::middleware(['lucent.guest'])->group(function () {
Route::get('/', [AuthController::class, 'login']);
Route::get('/register', [AuthController::class, 'register']);
Route::post('/register', [AuthController::class, 'postRegister']);
Route::get('/login', [AuthController::class, 'login']);
Route::post('/login', [AuthController::class, 'postLogin']);
Route::get('/verify', [AuthController::class, 'verify']);
Route::post('/verify', [AuthController::class, 'postVerify']);
Route::get("/register", [AuthController::class, "register"]);
Route::post("/register", [AuthController::class, "postRegister"]);
Route::get("/login", [AuthController::class, "login"]);
Route::post("/login", [AuthController::class, "postLogin"]);
Route::get("/verify", [AuthController::class, "verify"]);
Route::post("/verify", [AuthController::class, "postVerify"]);
}); });
Route::middleware('lucent.auth')->group(function () { Route::middleware("lucent.auth")->group(function () {
Route::get('/logout', [AuthController::class, 'logout']); Route::get("/logout", [AuthController::class, "logout"]);
Route::get('/profile', [AccountController::class, 'profile']); Route::get("/profile", [AccountController::class, "profile"]);
Route::post('/account/update-name', [AccountController::class, 'updateName']); Route::post("/account/update-name", [
Route::post('/account/update-email', [AccountController::class, 'updateEmail']); AccountController::class,
Route::get('/command-report/{signature}', [BuildController::class, 'report']); "updateName",
Route::get('/command-report-source/{signature}', [BuildController::class, 'reportSource']); ]);
Route::post('/command/{signature}', [BuildController::class, 'build']); Route::post("/account/update-email", [
AccountController::class,
"updateEmail",
]);
Route::get("/command-report/{signature}", [
BuildController::class,
"report",
]);
Route::get("/command-report-source/{signature}", [
BuildController::class,
"reportSource",
]);
Route::post("/command/{signature}", [
BuildController::class,
"build",
]);
}); });
Route::middleware(["lucent.auth"])->group(function () { Route::middleware(["lucent.auth"])->group(function () {
Route::get('/members/', [MemberController::class, 'index']); Route::get("/members/", [MemberController::class, "index"]);
Route::post('/members/invite', [MemberController::class, 'invite']); Route::post("/members/invite", [MemberController::class, "invite"]);
Route::post('/members/update', [MemberController::class, 'update']); Route::post("/members/update", [MemberController::class, "update"]);
}); });
Route::middleware(["lucent.auth"])
Route::middleware(["lucent.auth"])->prefix("/records")->group(function () { ->prefix("/records")
->group(function () {
Route::get('/new', [RecordController::class, 'new']); Route::get("/files", [RecordController::class, "files"]);
Route::get('/newInline', [RecordController::class, 'newInline']); Route::get("/new", [RecordController::class, "new"]);
Route::get('/suggestions', [RecordController::class, 'suggestions']); Route::get("/newInline", [
Route::get('/{rid}', [RecordController::class, 'edit']); RecordController::class,
Route::post('/clone/{rid}', [RecordController::class, 'clone']); "newInline",
]);
Route::get("/suggestions", [
RecordController::class,
"suggestions",
]);
Route::get("/{rid}", [RecordController::class, "edit"]);
Route::post("/clone/{rid}", [RecordController::class, "clone"]);
// Route::get('/editInline/{rid}', [RecordController::class, 'editInline']); // Route::get('/editInline/{rid}', [RecordController::class, 'editInline']);
Route::get('/{rid}/parents', [RecordController::class, 'parents']); Route::get("/{rid}/parents", [
Route::post('/', [RecordController::class, 'save']); RecordController::class,
Route::post('/status/{status}', [RecordController::class, 'status']); "parents",
Route::post('/delete', [RecordController::class, 'delete']); ]);
Route::post('/{rid}/rollback/{version}', [RecordController::class, 'rollback']); Route::post("/", [RecordController::class, "save"]);
Route::post("/status/{status}", [
RecordController::class,
"status",
]);
Route::post("/delete", [RecordController::class, "delete"]);
Route::post("/{rid}/rollback/{version}", [
RecordController::class,
"rollback",
]);
}); });
Route::middleware(["lucent.auth"])->prefix("/edges")->group(function () { Route::middleware(["lucent.auth"])
Route::post('/insert-many', [EdgeController::class, 'insertMany']); ->prefix("/edges")
->group(function () {
Route::post("/insert-many", [
EdgeController::class,
"insertMany",
]);
}); });
Route::middleware(["lucent.auth"])->group(function () { Route::middleware(["lucent.auth"])->group(function () {
Route::get('/records/{rid}/revisions', [RevisionController::class, 'index']); Route::get("/records/{rid}/revisions", [
RevisionController::class,
"index",
]);
}); });
Route::middleware(["lucent.auth"])->group(function () { Route::middleware(["lucent.auth"])->group(function () {
Route::get('/', [HomeController::class, 'home']); Route::get("/", [HomeController::class, "home"]);
Route::get('/home/records', [HomeController::class, 'records']); Route::get("/home/records", [HomeController::class, "records"]);
}); });
Route::middleware(["lucent.auth"])->prefix("/content")->group(function () { Route::middleware(["lucent.auth"])
Route::get('/{schemaName}', [RecordController::class, 'index']); ->prefix("/content")
Route::get('/{schemaName}/csv', [RecordController::class, 'exportCSV']); ->group(function () {
Route::get('/{schemaName}/emptyTrash', [RecordController::class, 'emptyTrash']); Route::get("/{schemaName}", [RecordController::class, "index"]);
Route::get("/{schemaName}/csv", [
RecordController::class,
"exportCSV",
]);
Route::get("/{schemaName}/emptyTrash", [
RecordController::class,
"emptyTrash",
]);
}); });
Route::middleware(["lucent.auth"])->group(function () { Route::middleware(["lucent.auth"])->group(function () {
Route::post('/files/upload', [FileController::class, 'upload']); Route::post("/files/upload", [FileController::class, "upload"]);
Route::get('/files/download', [FileController::class, 'download']); Route::get("/files/download", [FileController::class, "download"]);
}); });
},
);
});