This commit is contained in:
2026-05-06 21:43:13 +03:00
parent 8b3a3964a5
commit 93a16ee916
23 changed files with 148 additions and 387 deletions
-1
View File
@@ -1 +0,0 @@
LUCENT_AUTH=lucent // or lunar
-3
View File
@@ -1,13 +1,10 @@
<script>
import RecordRow from "./RecordRow.svelte";
import { previewTitle } from "../records/Preview";
import { usernameById } from "../account/users";
import { getContext } from "svelte";
import Avatar from "../account/Avatar.svelte";
import { selectRecord, toggleAll } from "./functions/recordSelect.js";
import Checkbox from "../common/Checkbox.svelte";
import Preview from "../files/Preview.svelte";
import { fileurl } from "../files/imageserver.js";
const channel = getContext("channel");
@@ -1,9 +1,7 @@
<script>
import {createEventDispatcher, getContext} from "svelte";
import {previewTitle} from "../../records/Preview";
import { createEventDispatcher, getContext } from "svelte";
import Icon from "../../common/Icon.svelte";
const channel = getContext("channel");
const dispatch = createEventDispatcher();
export let schema;
export let operators;
@@ -19,13 +17,10 @@
isReference: key.startsWith("children"),
};
filter = [
extractOperator(key),
extractLabel(schema, key),
].reduce((mem, fn) => fn(mem), filter);
filter = [extractOperator(key), extractLabel(schema, key)].reduce(
(mem, fn) => fn(mem),
filter,
);
function extractOperator(key) {
return (filter) => {
@@ -50,18 +45,16 @@
const filterField = schema.fields.find((f) => f.name === fieldName);
filter.label = filterField?.label ?? fieldName;
return filter;
}
};
}
const filterRecord = extractFilterRecord(graph, value);
function extractFilterRecord(graph, value) {
if (!filter.isReference) {
return null;
}
return graph.records.find(r => r.id === value);
return graph.records.find((r) => r.id === value);
}
function removeFilter(k) {
@@ -78,19 +71,21 @@
</script>
<span class="applied-filter">
{#if filter.isReference && filterRecord}
{filter.label} is {filterRecord.data.name}
{:else}
{filter.label}
{operators.find((o) => o.name === filter.operator)?.symbol ?? ""}
{operators.find((o) => o.name === filter.operator)?.hasValue
? value
: ""}
{/if}
{#if filter.isReference && filterRecord}
{filter.label} is {previewTitle(channel.schemas, filterRecord)}
{:else}
{filter.label} {operators.find((o) => o.name === filter.operator)?.symbol ?? ""} {operators.find((o) => o.name === filter.operator)?.hasValue ? value : ""}
{/if}
<button
on:click|preventDefault={() => removeFilter(key)}
type="button"
class="button-text"
aria-label="Close"
><Icon width={12} height={12} icon="close"></Icon></button>
<button
on:click|preventDefault={() => removeFilter(key)}
type="button"
class="button-text"
aria-label="Close"
><Icon width={12} height={12} icon="close"></Icon></button
>
</span>
@@ -1,8 +1,6 @@
<script>
import {createEventDispatcher, getContext} from "svelte";
import {debounce} from "lodash";
import {previewTitle} from "../../records/Preview";
import { createEventDispatcher, getContext } from "svelte";
import { debounce } from "lodash";
const channel = getContext("channel");
const dispatch = createEventDispatcher();
@@ -10,9 +8,8 @@
export let value = "";
export let field;
let search = ""
$: searchOptions = []
let search = "";
$: searchOptions = [];
const updateResults = debounce((e) => {
axios
@@ -35,46 +32,36 @@
function apply(e, newOption) {
e.preventDefault();
value = newOption.id
value = newOption.id;
dispatch("addFilter");
value = ""
value = "";
}
</script>
<div class="reference-tags">
<input
type="search"
on:keyup={updateResults}
bind:value={search}
placeholder={"Search for "+field.label}
autocomplete="off"
type="search"
on:keyup={updateResults}
bind:value={search}
placeholder={"Search for " + field.label}
autocomplete="off"
/>
<div class="reference-tags-results">
{#if searchOptions}
{#each searchOptions as option (option.id)}
<div
class="reference-tags-option"
role="button"
tabindex="0"
on:click={(e) => apply(e, option)}
on:keypress={(e) => apply(e, option)}
class="reference-tags-option"
role="button"
tabindex="0"
on:click={(e) => apply(e, option)}
on:keypress={(e) => apply(e, option)}
>
{previewTitle(channel.schemas, option)}
{option.data.name}
</div>
{:else}
<div
class="start-typing">
Start typing...
</div>
<div class="start-typing">Start typing...</div>
{/each}
{/if}
</div>
</div>
+1 -2
View File
@@ -1,7 +1,6 @@
<script>
import { formatDistanceToNow, parseJSON } from "date-fns";
import Avatar from "../account/Avatar.svelte";
import { previewTitle } from "../records/Preview";
import Preview from "../files/Preview.svelte";
import { usernameById } from "../account/users";
import { getContext } from "svelte";
@@ -25,7 +24,7 @@
<Preview {record} size="tiny" showFilename={true} />
{:else}
<a href="{channel.lucentUrl}/records/{record.id}">
{previewTitle(channel.schemas, record, graph)}
{record.data.name}
</a>
{/if}
</div>
+8 -8
View File
@@ -1,5 +1,4 @@
<script>
import NavbarMenu from "./NavbarMenu.svelte";
import { getContext } from "svelte";
export let schema;
@@ -9,13 +8,14 @@
<div class="sidebar-top">
<a class="logo" href={channel.lucentUrl}>{channel.name}</a>
<a class="nav-item" href="{channel.lucentUrl}/profile"> </a>
</div>
<div class="sidebar">
<NavbarMenu
title="Content"
schemas={readableSchemas.filter((sc) => sc.isEntry)}
{schema}
expanded={true}
/>
{#each readableSchemas as aschema}
<a
class="sidebar-item"
class:active={aschema.name === schema?.name}
aria-current="page"
href="{channel.lucentUrl}/content/{aschema.name}">{aschema.label}</a
>
{/each}
</div>
-34
View File
@@ -1,34 +0,0 @@
<script>
import {getContext} from "svelte";
import Icon from "../common/Icon.svelte";
const channel = getContext("channel");
export let schemas;
export let title;
export let schema;
export let expanded = false;
if(schemas.find(s => s.name === schema?.name)){
expanded = true;
}
function toggleExpand(){
expanded = !expanded;
}
</script>
<button class="sidebar-header" tabindex="0" on:click={toggleExpand}>
{title}
{#if expanded}
<Icon icon="circle-chevron-up"></Icon>
{:else}
<Icon icon="circle-chevron-down"></Icon>
{/if}
</button>
{#if expanded}
{#each schemas as aschema}
<a class="sidebar-item" class:active={aschema.name === schema?.name}
aria-current="page"
href="{channel.lucentUrl}/content/{aschema.name}">{aschema.label}</a>
{/each}
{/if}
-33
View File
@@ -1,33 +0,0 @@
import Mustache from "mustache";
import { stripHtml } from "../../helpers";
export function previewTitle(schemas, record, graph) {
let schema = schemas.find((aSchema) => aSchema.name === record?.schema);
if (!schema?.cardTitle) {
return noTemplate(schema, record);
}
let recordData = record.data;
let render = Mustache.render(schema.cardTitle, recordData);
if (!render || render === "") {
return noTemplate(schema, record);
}
return stripHtml(render.slice(0, 300));
}
function noTemplate(schema, record) {
if (schema?.type === "files") {
return file.path;
}
let title = stripHtml(
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name],
).slice(0, 300);
if (title.trim() === "") {
return "~Untitled~";
}
return title;
}
@@ -1,21 +1,13 @@
<script>
import {previewTitle} from "./Preview";
import {getContext} from "svelte";
import { getContext } from "svelte";
const channel = getContext("channel");
export let record;
export let graph;
$: schema = channel.schemas.find((aschema) => aschema.name === record.schema);
$: title = previewTitle(channel.schemas, record, graph);
$: title = record.data.name;
</script>
{#if record?.data}
<a
href="{channel.lucentUrl}/records/{record.id}"
{title}
class="reference"
>
<a href="{channel.lucentUrl}/records/{record.id}" {title} class="reference">
{title}
</a>
{/if}
@@ -1,9 +1,8 @@
<script>
import {getContext} from "svelte";
import {debounce} from "lodash";
import {previewTitle} from "../Preview";
import {getErrorMessage} from "./errorMessage";
import {insertEdges} from "./reference.js";
import { getContext } from "svelte";
import { debounce } from "lodash";
import { getErrorMessage } from "./errorMessage";
import { insertEdges } from "./reference.js";
import Icon from "../../common/Icon.svelte";
const channel = getContext("channel");
@@ -15,20 +14,24 @@
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
$: references = graph.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id == edge.target && record.id == edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let search = ""
$: searchOptions = []
$: references =
graph.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find(
(increc) =>
increc.id == edge.target && record.id == edge.source,
);
})
.filter((rec) => (rec?.id ? true : false)) ?? [];
let search = "";
$: searchOptions = [];
function removeReference(e, recordId) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === recordId && edge.field === field.name)
(edge) => !(edge.target === recordId && edge.field === field.name),
);
}
@@ -41,14 +44,14 @@
schema: field.collections[0],
status: "published",
data: {
[field.searchField]: newValue
}
[field.searchField]: newValue,
},
},
})
.then((response) => {
searchOptions = [];
insert(e, response.data.records[0]);
console.log(response)
console.log(response);
})
.catch((error) => {
searchOptions = [];
@@ -58,10 +61,16 @@
function insert(e, insertRecord) {
e.preventDefault();
graph = insertEdges(graph, record, [insertRecord], field.name, e.detail.action);
search = ""
searchEl.focus()
searchEl.blur()
graph = insertEdges(
graph,
record,
[insertRecord],
field.name,
e.detail.action,
);
search = "";
searchEl.focus();
searchEl.blur();
}
const updateResults = debounce((e) => {
@@ -82,9 +91,8 @@
console.log(error);
});
}, 500);
</script>
<div class="reference-tags">
{#if errorMessage}
<div class="invalid-feedback d-block mb-3">
@@ -93,44 +101,39 @@
{/if}
<input
type="search"
bind:this={searchEl}
{id}
on:keyup={updateResults}
class:is-invalid={errorMessage}
bind:value={search}
placeholder={"Search for "+field.label}
autocomplete="off"
type="search"
bind:this={searchEl}
{id}
on:keyup={updateResults}
class:is-invalid={errorMessage}
bind:value={search}
placeholder={"Search for " + field.label}
autocomplete="off"
/>
<div class="reference-tags-results">
{#if searchOptions}
{#each searchOptions as option (option.id)}
<div
class="reference-tags-option"
role="button"
tabindex="0"
on:click={(e) => insert(e, option)}
on:keypress={(e) => insert(e, option)}
>
{previewTitle(channel.schemas, option ,graph)}
</div>
{:else}
<div
class="start-typing">
Start typing...
</div>
{/each}
{/if}
{#if search }
<div
class="reference-tags-option"
role="button"
tabindex="0"
on:click={(e) => saveNew(e,search)}
on:keypress={(e) => saveNew(e,search)}
on:click={(e) => insert(e, option)}
on:keypress={(e) => insert(e, option)}
>
{option.data.name}
</div>
{:else}
<div class="start-typing">Start typing...</div>
{/each}
{/if}
{#if search}
<div
class="reference-tags-option"
role="button"
tabindex="0"
on:click={(e) => saveNew(e, search)}
on:keypress={(e) => saveNew(e, search)}
>
Add "{search}"
</div>
@@ -142,26 +145,23 @@
<div style="display: flex;align-items: center;gap: 4px">
{#each references as record (record.id)}
<span class="reference-tags-selected-value">
<a
class="record-title"
href="{channel.lucentUrl}/records/{record.id}"
>
{previewTitle(channel.schemas, record)}
</a>
<a
class="record-title"
href="{channel.lucentUrl}/records/{record.id}"
>
{record.data.name}
</a>
<button
on:click|preventDefault={(e) => removeReference(e, record.id)}
type="button"
class="button-text"
aria-label="Close"
on:click|preventDefault={(e) =>
removeReference(e, record.id)}
type="button"
class="button-text"
aria-label="Close"
>
<Icon width={12} height={12} icon="close"></Icon>
</button>
<Icon width={12} height={12} icon="close"></Icon>
</button>
</span>
{/each}
</div>
{/if}
+5 -11
View File
@@ -1,27 +1,21 @@
<script>
import {getContext} from "svelte";
import {previewTitle} from "./../Preview";
import { getContext } from "svelte";
const channel = getContext("channel");
export let schema;
export let record;
export let isCreateMode;
</script>
<div class="record-header">
<a
class="schema-name"
href="{channel.lucentUrl}/content/{schema.name}"
>{schema.label.toUpperCase()}</a
<a class="schema-name" href="{channel.lucentUrl}/content/{schema.name}"
>{schema.label.toUpperCase()}</a
>
<span class="record-title">
{#if !isCreateMode}
{previewTitle(channel.schemas, record)}
{record.data.name}
{:else}
New Record
{/if}
</span>
</div>
</div>
@@ -3,9 +3,6 @@
import { createEventDispatcher, getContext } from "svelte";
import Preview from "../../files/Preview.svelte";
import { previewTitle } from "./../Preview";
import { fileurl, htmlurl } from "../../files/imageserver.js";
import Status from "./../Status.svelte";
import Dropdown from "../../common/Dropdown.svelte";
const dispatch = createEventDispatcher();
@@ -1,8 +1,7 @@
<script>
import Icon from "../../common/Icon.svelte";
import {createEventDispatcher, getContext} from "svelte";
import {previewTitle} from "./../Preview";
import { createEventDispatcher, getContext } from "svelte";
import Status from "./../Status.svelte";
import Preview from "../../files/Preview.svelte";
@@ -12,10 +11,15 @@
export let record;
export let hasDelete = false;
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
let cardTitle = previewTitle(channel.schemas, record, graph);
const cardImageEdge = graph.edges.find(e => e.source === record.id && e.field === schema.cardImage);
let cardImageRecord = graph.records.find(r => r.id === cardImageEdge?.target);
let schema = channel.schemas.find(
(aschema) => aschema.name === record.schema,
);
const cardImageEdge = graph.edges.find(
(e) => e.source === record.id && e.field === schema.cardImage,
);
let cardImageRecord = graph.records.find(
(r) => r.id === cardImageEdge?.target,
);
function remove(e) {
e.preventDefault();
@@ -23,43 +27,35 @@
}
</script>
<div class="preview-reference">
<div style="display: flex;align-items: center;gap: 10px;">
{#if cardImageRecord}
<div class="image">
<Preview record={cardImageRecord} size="small"/>
<Preview record={cardImageRecord} size="small" />
</div>
{/if}
<div class="title">
<div>
<a
class="record-title"
href="{channel.lucentUrl}/records/{record.id}"
class="record-title"
href="{channel.lucentUrl}/records/{record.id}"
>
{cardTitle}
{record.data.name}
</a>
<small class="d-block">
from {schema.label}
{#if record.status === "draft"}
<Status status={record.status}/>
<Status status={record.status} />
{/if}
</small>
</div>
</div>
</div>
{#if hasDelete}
<div class="reference-action">
<button
class="button"
on:click={remove}
>
<Icon icon="trash-can"/>
<button class="button" on:click={remove}>
<Icon icon="trash-can" />
</button>
</div>
{/if}
</div>
+2
View File
@@ -2,7 +2,9 @@
return [
"env" => env("LUCENT_ENV", "production"),
// lucent or lunar
"auth" => env("LUCENT_AUTH", "lucent"),
"disk" => env("LUCENT_DISK", "public"),
"schemas_path" => env("LUCENT_SCHEMAS_PATH", "resources/lucent/schemas"),
"database" => env("LUCENT_DB_CONNECTION", env("DB_CONNECTION", "sqlite")),
"name" => env("LUCENT_NAME", "Lucent"),
-3
View File
@@ -16,10 +16,7 @@ class Schema
public array $visible,
public array $groups,
public Collection $fields,
public bool $isEntry = false,
public string $color = "",
public string $sortBy = "-_sys.updatedAt",
public ?string $cardTitle = null,
public ?string $cardImage = null,
public int $revisions = 0,
public array $read = [],
+1 -3
View File
@@ -21,7 +21,6 @@ use Lucent\Schema\System;
use Lucent\Schema\Ui\Reference;
use Lucent\Schema\Validator\ValidatorException;
use Lucent\Svelte\Svelte;
use Lucent\ViewModel\ViewModel;
use function Lucent\Response\fail;
use function Lucent\Response\ok;
@@ -36,7 +35,6 @@ class RecordController extends Controller
private readonly Query $query,
private readonly Manager $recordManager,
private readonly OperatorRegistry $operatorRegistry,
private readonly ViewModel $viewModel,
) {}
public function index(Request $request)
@@ -217,7 +215,7 @@ class RecordController extends Controller
$c[] = "";
} elseif (count($fieldRecords) === 1) {
$c[] = data_get($fieldRecords, "0.id");
$c[] = $this->viewModel->getRecordName($fieldRecords[0]);
$c[] = $fieldRecords[0]->data["name"];
} else {
$c[] = collect($fieldRecords)->pluck("id")->join("::");
$c[] = collect($fieldRecords)
-3
View File
@@ -8,7 +8,6 @@ use Illuminate\Support\Facades\View;
use Lucent\Account\AccountService;
use Lucent\Account\AuthService;
use Lucent\Channel\ChannelService;
use Lucent\ViewModel\ViewModel;
readonly class AuthMiddleware
{
@@ -16,7 +15,6 @@ readonly class AuthMiddleware
private AuthService $authService,
private AccountService $accountService,
private ChannelService $channelService,
private ViewModel $viewModel,
) {}
public function handle(Request $request, Closure $next)
@@ -38,7 +36,6 @@ readonly class AuthMiddleware
)
->values(),
);
View::share("viewModel", $this->viewModel);
return $next($request);
}
-33
View File
@@ -1,33 +0,0 @@
<?php
namespace Lucent\Schema;
use Lucent\Primitive\Collection;
class CollectionSchema implements Schema
{
public Type $type = Type::COLLECTION;
/**
* @param Collection<FieldInterface> $fields
* @param array<string> $visible
*/
function __construct(
public string $name,
public string $label,
public array $visible,
public array $groups,
public Collection $fields,
public bool $isEntry = false,
public string $color = "",
public string $sortBy = "-_sys.updatedAt",
public ?string $cardTitle = null,
public ?string $cardImage = null,
public int $revisions = 0,
public array $read = [],
public array $write = [],
)
{
}
}
-36
View File
@@ -1,36 +0,0 @@
<?php
namespace Lucent\Schema;
use Lucent\Primitive\Collection;
class FilesSchema implements Schema
{
public Type $type = Type::FILES;
/**
* @param Collection<FieldInterface> $fields
* @param array<string> $groups
*/
function __construct(
public string $name,
public string $label,
public Collection $fields,
public string $disk,
public string $path,
public array $groups,
public bool $isEntry = false,
public string $sortBy = "-_sys.updatedAt",
public string $color = "",
public ?string $cardTitle = null,
public ?string $cardImage = null,
public int $revisions = 0,
public array $read = [],
public array $write = [],
)
{
}
}
-9
View File
@@ -1,9 +0,0 @@
<?php
namespace Lucent\Schema;
interface Schema
{
}
-4
View File
@@ -31,11 +31,7 @@ class SchemaService
$this,
"mapFields",
]),
isEntry: $schemaArr["isEntry"] ?? false,
color: $schemaArr["color"] ?? "",
sortBy: $schemaArr["sortBy"] ?? "-_sys.updatedAt",
cardTitle: $schemaArr["titleTemplate"] ??
($schemaArr["cardTitle"] ?? null),
cardImage: $schemaArr["cardImage"] ?? null,
revisions: $schemaArr["revisions"] ?? 0,
read: $schemaArr["read"] ?? [],
+1 -1
View File
@@ -38,7 +38,7 @@ class Slug implements FieldInterface, RequiredInterface
$value = Str::slug($input[$this->source]);
}
$output[$this->name] = new Nullable(
$output[$this->name] = Nullable::make(
$this->nullable,
$value,
"",
-40
View File
@@ -1,40 +0,0 @@
<?php
namespace Lucent\ViewModel;
use Lucent\Channel\ChannelService;
use Lucent\Record\QueryRecord;
use Lucent\Schema\CollectionSchema;
use Lucent\Schema\FieldInterface;
use Lucent\Schema\FilesSchema;
use Mustache_Engine;
class ViewModel
{
public function __construct(
public ChannelService $channelService
)
{
}
public function getRecordName(QueryRecord $record): string
{
$schema = $this->channelService->getSchema($record->schema)->get();
if (empty($schema->cardTitle)) {
$title = match (get_class($schema)) {
CollectionSchema::class => $record->data[$schema->fields->filter(fn(FieldInterface $f) => $f->info->name === "text")->first()->name],
FilesSchema::class => $record->_file->path,
};
if (empty(trim($title))) {
return "~Untitled~";
}
return $title;
}
$m = new Mustache_Engine(array('entity_flags' => ENT_QUOTES));
return $m->render($schema->cardTitle, $record->data);
}
}