Compare commits

...

46 Commits

Author SHA1 Message Date
arvanitakis a78b699a5e Fix 2025-06-16 18:23:14 +03:00
lexx a482ab3c7e login urls 2025-03-20 21:06:40 +02:00
lexx c580882ec0 fix url 2025-03-20 20:57:47 +02:00
lexx 2cf8379cbe urls update 2025-03-20 20:53:51 +02:00
lexx c39ec469df files bug 2025-01-22 20:03:12 +02:00
lexx 232fcc8845 csv render title 2024-12-18 13:02:09 +02:00
lexx 9d5d4dd930 csv relations 2024-12-14 18:56:04 +02:00
lexx c507dc6031 upload fix 2024-10-23 19:34:41 +03:00
lexx 843f560710 new build 2024-09-27 17:42:49 +03:00
lexx 7574d67d80 some styling in tables 2024-09-27 16:48:05 +03:00
lexx 19931cb4d1 update files script 2024-09-27 16:27:37 +03:00
lexx 6458c1e71d rebuilding thumbnails command 2024-09-27 15:32:35 +03:00
lexx 63232585ab storage and image model 2024-09-27 14:28:20 +03:00
lexx 6d15591601 wip stograge 2024-09-20 13:39:45 +03:00
lexx 32c8378020 refactor files 2024-09-19 23:36:43 +03:00
lexx d0cd8228cc fix replacing config 2024-09-13 18:13:15 +03:00
lexx c45a3847f8 fix rich editor embed image original 2024-09-13 18:11:57 +03:00
lexx c0b3878674 file route for template generation 2024-09-13 17:16:04 +03:00
lexx f868219981 fixes and stuff 2024-09-11 16:21:51 +03:00
lexx 8ac0567e66 fix graph ignoring missing fields 2024-09-07 15:57:31 +03:00
lexx 02f8f5970a codemirror insert 2024-09-07 15:31:56 +03:00
lexx 0cd4e08716 fixing database connections 2024-09-07 13:22:58 +03:00
lexx cf3d621587 helper commands 2024-09-07 00:03:11 +03:00
lexx 6fc0a65b6f setup complete 2024-09-06 23:30:12 +03:00
lexx a73ee21568 wip setup guide 2024-09-06 21:00:15 +03:00
lexx ff54bcc2ef wip setup guide 2024-09-06 20:59:56 +03:00
lexx ab1517cc8f tabs and date in modal fix 2024-08-30 13:38:34 +03:00
lexx 9f724a3243 better report 2024-08-27 17:59:12 +03:00
lexx ae65ca47f6 commands and logs to the database 2024-08-27 17:42:06 +03:00
lexx 74d2fcc4fa build assets 2024-08-27 12:25:42 +03:00
lexx 82174afdea fixing multiple references 2024-08-27 12:24:51 +03:00
lexx ffc39f078d trix 2024-08-25 14:45:49 +03:00
lexx 7c4e19afbc tip tap and trix 2024-08-25 14:23:20 +03:00
lexx 7b10bfca1d cleanup 2024-08-24 19:57:17 +03:00
lexx 0e5ac08641 actions 2024-08-24 19:35:07 +03:00
lexx 1505aaa909 multiple commands 2024-08-24 18:51:36 +03:00
lexx d9e2c4954a refactoring of filters 2024-08-24 17:22:40 +03:00
lexx 97ad9de3d2 readme 2024-08-24 01:30:44 +03:00
lexx 9e140be0ec updated readme 2024-08-23 21:06:53 +03:00
lexx a737c2d571 configurable disks 2024-08-23 20:58:45 +03:00
lexx c43c29eb14 modal save button 2024-08-23 19:37:20 +03:00
lexx 0c00f76657 fix tools and layhout 2024-08-23 18:15:18 +03:00
lexx 4165bfb95d build controller 2024-08-23 17:29:08 +03:00
lexx 570dbf747e channel 2024-08-23 17:23:43 +03:00
lexx 14cbd0a845 manifest fix 2024-08-23 17:15:40 +03:00
lexx c99634bb46 query 2024-08-23 16:54:33 +03:00
169 changed files with 5976 additions and 2166 deletions
+25 -3
View File
@@ -9,8 +9,8 @@ include_toc: true
### Requirements ### Requirements
- PHP 8.2 - PHP 8.3
- Laravel 10 - Laravel 11
- Postgres or Sqlite database - Postgres or Sqlite database
- ImageMagick - ImageMagick
@@ -82,7 +82,9 @@ return [
### Database ### Database
The recommended database for small website is sqlite. But you can also use postresql The recommended database for small website is sqlite. But you can also use postresql
Make sure to delete the existing migration scripts in your database/migrations folder.
> [!CAUTION]
> Make sure to delete the existing migration scripts in your database/migrations folder.
Then run: Then run:
@@ -90,6 +92,26 @@ Then run:
php artisan migrate php artisan migrate
``` ```
### File Storage
You can use your local filesystem or s3 compatible storage. Lucent expects you to have a valid configuration inside ``config/filesystems.php``
example:
```php
return [
'disks' => [
'lucent' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => true,
],
],
];
```
### First user ### First user
To create your first user, head to your localhost:8000/lucent To create your first user, head to your localhost:8000/lucent
+32
View File
@@ -0,0 +1,32 @@
# Upgrade from 1.1.* to 1.2.0
## lucent.php config file
There is now an array of commands, accepting more than one.
from
```php
"generateCommand" => env("LUCENT_GENERATE_COMMAND", "generate:static"),
```
to
```php
"commands" => [
"generate:static" => "Build Website",
],
```
## config/filesystems.php
Lucent has its own filesystem.
You should now add:
```
'lucent' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
],
```
+2 -1
View File
@@ -8,14 +8,15 @@
"ext-zip": "*", "ext-zip": "*",
"ext-sqlite3": "*", "ext-sqlite3": "*",
"ext-imagick": "*", "ext-imagick": "*",
"ext-pdo": "*",
"php": "^8.3", "php": "^8.3",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"intervention/image": "^2.7", "intervention/image": "^2.7",
"phpoption/phpoption": "^1.9", "phpoption/phpoption": "^1.9",
"spatie/image-optimizer": "^1.6", "spatie/image-optimizer": "^1.6",
"staudenmeir/laravel-cte": "^1.0", "staudenmeir/laravel-cte": "^1.0",
"ext-pdo": "*",
"mustache/mustache": "^2.14" "mustache/mustache": "^2.14"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^1.8", "phpstan/phpstan": "^1.8",
Generated
+3 -3
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "71cf4c1de3d614ce2f9607763bf5687f", "content-hash": "e8fb1bee28ad339453d50110f7fea2f5",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@@ -5729,8 +5729,8 @@
"ext-zip": "*", "ext-zip": "*",
"ext-sqlite3": "*", "ext-sqlite3": "*",
"ext-imagick": "*", "ext-imagick": "*",
"php": "^8.3", "ext-pdo": "*",
"ext-pdo": "*" "php": "^8.3"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.3.0" "plugin-api-version": "2.3.0"
+5 -11
View File
@@ -24,7 +24,8 @@ There are 3 types of schemas
- **fields**: The list of your fields. Look the field reference for more - **fields**: The list of your fields. Look the field reference for more
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar. Default: false _optional_ - **isEntry**: If this schema is important, it will show be visible on the main the sidebar. Default: false _optional_
- **sortBy**: The default sorting in the content browser _optional_ - **sortBy**: The default sorting in the content browser _optional_
- **titleTemplate**: Mustache code to customize the preview field _optional_ - **cardTitle**: Mustache code to customize the preview field _optional_
- **cardImage**: Field name of image you want to use as a preview image _optional_
- **revisions**: How many revisions are going to be kept for each record _optional_ - **revisions**: How many revisions are going to be kept for each record _optional_
- **read**: Array of user groups that have read permissions _optional_ - **read**: Array of user groups that have read permissions _optional_
- **write**: Array of user groups that have write permissions _optional_ - **write**: Array of user groups that have write permissions _optional_
@@ -40,20 +41,12 @@ There are 3 types of schemas
- **fields**: The list of your fields. Look the field reference for more - **fields**: The list of your fields. Look the field reference for more
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar _optional_ - **isEntry**: If this schema is important, it will show be visible on the main the sidebar _optional_
- **sortBy**: The default sorting in the content browser _optional_ - **sortBy**: The default sorting in the content browser _optional_
- **titleTemplate**: Mustache code to customize the preview field _optional_ - **cardTitle**: Mustache code to customize the preview field _optional_
- **revisions**: How many revisions are going to be kept for each record _optional_ - **revisions**: How many revisions are going to be kept for each record _optional_
- **read**: Array of user groups that have read permissions _optional_ - **read**: Array of user groups that have read permissions _optional_
- **write**: Array of user groups that have write permissions _optional_ - **write**: Array of user groups that have write permissions _optional_
## Block Reference
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
- **label**: The friendly name of the schema
- **type**: The type of the collection. Should be "block"
- **fields**: The list of your fields. Look the field reference for more
A full Collection example without the fields: A full Collection example without the fields:
```json ```json
@@ -74,7 +67,8 @@ A full Collection example without the fields:
"SEO" "SEO"
], ],
"sortBy": "-_sys.createdAt", "sortBy": "-_sys.createdAt",
"titleTemplate": "{{name}} {{slug}}", "schemaTitle": "{{name}} {{slug}}",
"schemaImage": "cover",
"revisions": 15, "revisions": 15,
"read": [ "read": [
"admin", "admin",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,11 +1,11 @@
{ {
"main.js": { "main.js": {
"file": "assets/main-B-nfEWyS.js", "file": "assets/main-BJyanQ7P.js",
"name": "main", "name": "main",
"src": "main.js", "src": "main.js",
"isEntry": true, "isEntry": true,
"css": [ "css": [
"assets/main-CaexgiEy.css" "assets/main-Dk7njt4m.css"
] ]
} }
} }
+3 -1
View File
@@ -3,6 +3,7 @@
import Login from "./account/Login.svelte"; import Login from "./account/Login.svelte";
import Verify from "./account/Verify.svelte"; import Verify from "./account/Verify.svelte";
import Profile from "./account/Profile.svelte"; import Profile from "./account/Profile.svelte";
import SetupIndex from "./setup/Index.svelte";
import {setContext} from "svelte"; import {setContext} from "svelte";
const components = { const components = {
@@ -10,6 +11,7 @@
login: Login, login: Login,
verify: Verify, verify: Verify,
profile: Profile, profile: Profile,
setup: SetupIndex,
}; };
export let title; export let title;
@@ -22,7 +24,7 @@
setContext("user", user); setContext("user", user);
</script> </script>
<div style="text-align: center;background: var(--p20);padding: 20px;color: var(--p90)"> <div style="text-align: center;background: var(--p20);padding: 20px;color: var(--p90)">
<h1><a class="text-decoration-none" href="{channel.lucentUrl}">{channel.name}</a></h1> <h1><a class="text-decoration-none" href="{channel.lucentUrl}">{channel.name ?? "Lucent Setup"}</a></h1>
</div> </div>
<div> <div>
<svelte:component this={components[view]} {title} {...data}/> <svelte:component this={components[view]} {title} {...data}/>
+22 -10
View File
@@ -1,22 +1,25 @@
<script> <script>
import {getContext, onMount} from "svelte"; import {getContext, onMount} from "svelte";
import axios from "axios";
const channel = getContext("channel"); const channel = getContext("channel");
export let title; export let title;
export let command;
$: date = ""; $: date = "";
$: logs = ""; $: logs = "";
let anchorEl;
let inProgress = false; let inProgress = false;
function connect() { function connect() {
const eventSource = new EventSource(channel.lucentUrl + "/build-report-source"); const eventSource = new EventSource(channel.lucentUrl + "/command-report-source/" + command.signature );
eventSource.onmessage = function (event) { eventSource.onmessage = function (event) {
inProgress = true; inProgress = true;
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
date = data.date; date = data.date;
logs = data.logs; logs = data.logs;
anchorEl.scrollIntoView()
} }
eventSource.onerror = (e) => { eventSource.onerror = (e) => {
console.log(e) console.log(e)
@@ -28,8 +31,7 @@
function buildWebsite(e) { function buildWebsite(e) {
e.preventDefault(); e.preventDefault();
inProgress = true; inProgress = true;
axios.post(channel.lucentUrl + "/command/" + command.signature).then(response => {
axios.post(channel.lucentUrl + "/build").then(response => {
connect() connect()
}) })
@@ -46,26 +48,36 @@
<h3 class="header-small mb-5">{title}</h3> <h3 class="header-small mb-5">{title}</h3>
<button on:click={buildWebsite} class="button primary mb-3" disabled={inProgress}>Start Build <button on:click={buildWebsite} class="button primary mb-3" disabled={inProgress}>Start
</button> </button>
<div class="mb-3"> <div class="mb-3">
{#if inProgress} {#if inProgress}
<span class="badge text-bg-warning"> <span class="badge text-bg-warning">
Build in progress Action in progress
</span> </span>
{/if} {/if}
{#if !inProgress && logs} {#if !inProgress && logs}
<span class="badge text-bg-info"> <span class="badge text-bg-info">
Build completed Action completed
</span> </span>
{/if} {/if}
</div> </div>
<pre>{logs}</pre> <pre class="logs">{logs}
<div bind:this={anchorEl}>&nbsp;</div>
</pre>
</div> </div>
</div> </div>
<style>
.logs{
max-height: 70vh;
overflow: scroll;
background: var(--p90);
color: var(--p10);
padding: 10px;
}
</style>
+13
View File
@@ -113,8 +113,21 @@
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12l4-4m-4 4 4 4"/>', path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12l4-4m-4 4 4 4"/>',
viewBox: "0 0 24 24", viewBox: "0 0 24 24",
}, },
"list": {
path: '<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M9 8h10M9 12h10M9 16h10M4.99 8H5m-.02 4h.01m0 4H5"/>',
viewBox: "0 0 24 24",
},
"ordered-list": {
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6h8m-8 6h8m-8 6h8M4 16a2 2 0 1 1 3.321 1.5L4 20h5M4 5l2-1v6m-2 0h4"/>',
viewBox: "0 0 24 24",
},
"italic": {
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m8.874 19 6.143-14M6 19h6.33m-.66-14H18"/>',
viewBox: "0 0 24 24",
}
}; };
export let width = 16; export let width = 16;
export let height = 16; export let height = 16;
export let icon = ""; export let icon = "";
+7
View File
@@ -83,7 +83,11 @@
{#if record._file?.path} {#if record._file?.path}
<div class="file-table-row"> <div class="file-table-row">
<Preview record={record} size={record._file?.width > 0 ? "medium" : "small"}/> <Preview record={record} size={record._file?.width > 0 ? "medium" : "small"}/>
<div> <div>
{#if record.status === "draft"}
<span style="text-transform: uppercase;font-size:10px">{record.status}</span>
{/if}
<a <a
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/records/{record.id}"
target={inModal ? "_blank" : "_self"} target={inModal ? "_blank" : "_self"}
@@ -109,6 +113,9 @@
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/records/{record.id}"
target={inModal ? "_blank" : "_self"} target={inModal ? "_blank" : "_self"}
> >
{#if record.status === "draft"}
<span style="text-transform: uppercase;font-size:10px">{record.status}</span>
{/if}
{previewTitle(channel.schemas, record, graph)} {previewTitle(channel.schemas, record, graph)}
</a> </a>
{/if} {/if}
@@ -18,7 +18,7 @@
<div class="references"> <div class="references">
{#each recordEdges as recordEdge} {#each recordEdges as recordEdge}
<span class="mr-3"> <span class="reference">
<PreviewCardSmall {schemas} {graph} record={recordEdge}/> <PreviewCardSmall {schemas} {graph} record={recordEdge}/>
</span> </span>
{/each} {/each}
@@ -12,7 +12,6 @@
export let inModal; export let inModal;
export let modalUrl; export let modalUrl;
export let graph; export let graph;
let filter = { let filter = {
label: "", label: "",
operator: "", operator: "",
@@ -58,6 +57,7 @@
const filterRecord = extractFilterRecord(graph, value); const filterRecord = extractFilterRecord(graph, value);
function extractFilterRecord(graph, value) { function extractFilterRecord(graph, value) {
if (!filter.isReference) { if (!filter.isReference) {
return null; return null;
} }
@@ -82,7 +82,7 @@
{#if filter.isReference && filterRecord} {#if filter.isReference && filterRecord}
{filter.label} is {previewTitle(channel.schemas, filterRecord)} {filter.label} is {previewTitle(channel.schemas, filterRecord)}
{:else} {:else}
{filter.label} {operators.find((o) => o.name === filter.operator)?.symbol ?? ""} {value} {filter.label} {operators.find((o) => o.name === filter.operator)?.symbol ?? ""} {operators.find((o) => o.name === filter.operator)?.hasValue ? value : ""}
{/if} {/if}
<button <button
@@ -4,10 +4,10 @@
const channel = getContext("channel"); const channel = getContext("channel");
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let inModal; export let inModal;
export let modalUrl; export let modalUrl;
const url = new URL(modalUrl ?? window.location.href); const url = new URL(modalUrl ?? window.location.href);
function removeFilter(k) { function removeFilter(k) {
const url = new URL(modalUrl ?? window.location.href); const url = new URL(modalUrl ?? window.location.href);
@@ -11,7 +11,6 @@
export let inModal; export let inModal;
export let modalUrl; export let modalUrl;
let dropdown; let dropdown;
let search = ""; let search = "";
let systemFieldsFiltered = systemFields; let systemFieldsFiltered = systemFields;
@@ -70,6 +69,13 @@
activeOperator = operators.find(o => o.name === "eq") activeOperator = operators.find(o => o.name === "eq")
} }
function selectOperator(e, operator) {
activeOperator = operator;
if (!operator.hasValue) {
applyFilter(e)
}
}
function applyFilter(e) { function applyFilter(e) {
e.preventDefault(); e.preventDefault();
let filterPrefix = ""; let filterPrefix = "";
@@ -146,7 +152,7 @@
<div class="selected-filter">field: {activeField.label}</div> <div class="selected-filter">field: {activeField.label}</div>
{#each activeOperators as operator} {#each activeOperators as operator}
<button class="dropdown-item button" on:click={e => activeOperator = operator }> <button class="dropdown-item button" on:click={e => selectOperator(e,operator)}>
{operator.label} {operator.label}
</button> </button>
{/each} {/each}
@@ -214,8 +220,8 @@
required required
/> />
<button class="button applied-filter"> <button class="button applied-filter">
Submit Submit
</button> </button>
</form> </form>
+10 -9
View File
@@ -135,13 +135,13 @@
</div> </div>
{#if Object.entries(filter).length > 0} <div class="applied-filters">
<div class="applied-filters"> <AppliedFilterNotLinked
<AppliedFilterNotLinked {inModal}
{inModal} {modalUrl}
{modalUrl} on:refresh
on:refresh ></AppliedFilterNotLinked>
/> {#if Object.entries(filter).length > 0}
{#each Object.entries(filter) as [k, v]} {#each Object.entries(filter) as [k, v]}
<AppliedFilter <AppliedFilter
{schema} {schema}
@@ -154,6 +154,7 @@
on:refresh on:refresh
/> />
{/each} {/each}
</div> {/if}
{/if} </div>
+6 -9
View File
@@ -1,27 +1,24 @@
export function imgurl(channel, record) {
export function imgurl(channel,record) {
if (record._file.mime === "image/svg+xml") { if (record._file.mime === "image/svg+xml") {
return fileurl(channel, record); return fileurl(channel, record);
} }
return channel.filesUrl + `/thumbs/${record._file.path}`; return channel.disks[record._file.disk] + `/thumbs/${record._file.path}`;
} }
export function fileurl(channel, record) { export function fileurl(channel, record) {
return channel.filesUrl + `/${record._file.path}`; return channel.disks[record._file.disk] + `/${record._file.path}`;
} }
export function htmlurl(channel,record, preset) { export function htmlurl(channel, record, preset) {
let html = ""; let html = "";
let url = fileurl(channel,record) let url = fileurl(channel, record)
if (record._file.width > 0) { if (record._file.width > 0) {
let presetUrl = url; let presetUrl = url;
if (preset) { if (preset) {
presetUrl = channel.filesUrl + `/templates/${preset}/${record._file.path}`; presetUrl = channel.disks[record._file.disk] + `/templates/${preset}/${record._file.path}`;
} }
html = `<img src="${presetUrl}" alt="${record._file.path}" />` html = `<img src="${presetUrl}" alt="${record._file.path}" />`
} else if (record._file.mime === "image/svg+xml") { } else if (record._file.mime === "image/svg+xml") {
html = `<img src="${url}" alt="${record._file.path}"/>` html = `<img src="${url}" alt="${record._file.path}"/>`
+13 -5
View File
@@ -1,17 +1,25 @@
<script> <script>
import Avatar from "../account/Avatar.svelte"; import Avatar from "../account/Avatar.svelte";
import {getContext} from "svelte"; import {getContext} from "svelte";
import Dropdown from "../common/Dropdown.svelte";
const channel = getContext("channel"); const channel = getContext("channel");
const user = getContext("user"); const user = getContext("user");
console.log( channel.commands)
</script> </script>
<div class="top-nav "> <div class="top-nav ">
<a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a> <a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a>
{#if channel.generateCommand} {#if channel.commands.length > 0}
<a href="{channel.lucentUrl}/build-report" class="top-nav-item">Build website</a> <Dropdown>
<div slot="button">Actions</div>
{#each channel.commands as command}
<a href="{channel.lucentUrl}/command-report/{command.signature}" class="top-nav-item">{command.name}</a>
{/each}
</Dropdown>
{/if} {/if}
<!-- <div>--> <!-- <div>-->
<!-- <form method="GET">--> <!-- <form method="GET">-->
@@ -19,8 +27,8 @@
<!-- class="form-control" required/>--> <!-- class="form-control" required/>-->
<!-- </form>--> <!-- </form>-->
<!-- </div>--> <!-- </div>-->
<a href="{channel.lucentUrl}/profile"> <a href="{channel.lucentUrl}/profile">
<Avatar side="28" name={user.name}/> <Avatar side="28" name={user.name}/>
</a> </a>
</div> </div>
+26 -4
View File
@@ -1,10 +1,10 @@
<script> <script>
// https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387 // https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387
import {onMount, onDestroy} from "svelte"; import {onDestroy, onMount} from "svelte";
import {basicSetup, EditorView} from "codemirror"; import {basicSetup, EditorView} from "codemirror";
import { autocompletion, completionKeymap } from "@codemirror/autocomplete"; import {autocompletion, completionKeymap} from "@codemirror/autocomplete";
import {EditorState, Compartment} from "@codemirror/state"; import {Compartment, EditorState} from "@codemirror/state";
import {keymap} from "@codemirror/view"; import {keymap} from "@codemirror/view";
import {indentWithTab} from "@codemirror/commands"; import {indentWithTab} from "@codemirror/commands";
import {markdown} from "@codemirror/lang-markdown"; import {markdown} from "@codemirror/lang-markdown";
@@ -15,6 +15,29 @@
export let value; export let value;
export let editable = true; export let editable = true;
export function insertMedia(info) {
let insertText = "";
if (info.record._file.width > 0) {
insertText = `![${info.record._file.originalName}](${info.url})`;
} else {
insertText = `[${info.record._file.originalName}](${info.originalUrl})`;
}
const cursor = codeMirrorView.state.selection.main.head;
const transaction = codeMirrorView.state.update({
changes: {
from: cursor,
insert: insertText,
},
// the next 2 lines will set the appropriate cursor position after inserting the new text.
selection: {anchor: cursor + 1},
scrollIntoView: true,
});
if (transaction) {
codeMirrorView.dispatch(transaction);
}
}
onMount(() => { onMount(() => {
let language = new Compartment(); let language = new Compartment();
let tabSize = new Compartment(); let tabSize = new Compartment();
@@ -51,7 +74,6 @@
}); });
}); });
onDestroy(() => { onDestroy(() => {
+2 -2
View File
@@ -100,8 +100,8 @@
tinymce.init({...config, ...additionalConfig}); tinymce.init({...config, ...additionalConfig});
}); });
export function insertMedia(html){ export function insertMedia(info){
activeEditor.execCommand('InsertHTML', false, html); activeEditor.execCommand('InsertHTML', false, info.html);
} }
</script> </script>
+104 -26
View File
@@ -3,9 +3,12 @@
import {Editor} from '@tiptap/core' import {Editor} from '@tiptap/core'
import Document from '@tiptap/extension-document' import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph' import Paragraph from '@tiptap/extension-paragraph'
import Dropcursor from '@tiptap/extension-dropcursor'
import Text from '@tiptap/extension-text' import Text from '@tiptap/extension-text'
import Heading from '@tiptap/extension-heading' import Heading from '@tiptap/extension-heading'
import HardBreak from '@tiptap/extension-hard-break'
import Blockquote from '@tiptap/extension-blockquote'; import Blockquote from '@tiptap/extension-blockquote';
import CodeBlock from '@tiptap/extension-code-block';
import Bold from '@tiptap/extension-bold'; import Bold from '@tiptap/extension-bold';
import BulletList from '@tiptap/extension-bullet-list'; import BulletList from '@tiptap/extension-bullet-list';
import Code from '@tiptap/extension-code'; import Code from '@tiptap/extension-code';
@@ -19,6 +22,8 @@
import TableCell from '@tiptap/extension-table-cell'; import TableCell from '@tiptap/extension-table-cell';
import TableHeader from '@tiptap/extension-table-header'; import TableHeader from '@tiptap/extension-table-header';
import Underline from '@tiptap/extension-underline'; import Underline from '@tiptap/extension-underline';
import Image from '@tiptap/extension-image';
import Icon from "../common/Icon.svelte";
let element; let element;
let editor; let editor;
@@ -32,19 +37,22 @@
Paragraph, Paragraph,
Text, Text,
Bold, Bold,
ListItem,
BulletList, BulletList,
Code, Code,
CodeBlock,
History, History,
Italic, Italic,
ListItem, HardBreak,
OrderedList, OrderedList,
ListItem,
Strike, Strike,
Table, Table,
TableRow, TableRow,
TableCell, TableCell,
TableHeader, TableHeader,
Underline, Underline,
Dropcursor,
Image,
Heading.configure({ Heading.configure({
levels: [1, 2, 3], levels: [1, 2, 3],
}), }),
@@ -56,7 +64,11 @@
// force re-render so `editor.isActive` works as expected // force re-render so `editor.isActive` works as expected
editor = editor; editor = editor;
}, },
onUpdate: ({editor}) => {
value = editor.getHTML()
},
}); });
}); });
onDestroy(() => { onDestroy(() => {
@@ -64,33 +76,99 @@
editor.destroy(); editor.destroy();
} }
}); });
export function insertMedia(info){
editor.chain().focus().setImage({ src: info.url }).run()
}
</script> </script>
{#if editor} {#if editor}
<button <div class="editor-toolbar">
on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} <button
class:active={editor.isActive('heading', { level: 1 })} class="button"
> on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
H1 class:active={editor.isActive('heading', { level: 1 })}
</button> >
<button H1
on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} </button>
class:active={editor.isActive('heading', { level: 2 })} <button
> class="button"
H2 on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
</button> class:active={editor.isActive('heading', { level: 2 })}
<button >
on:click={() => editor.chain().focus().setParagraph().run()} H2
class:active={editor.isActive('paragraph')} </button>
>
P <button
</button> class="button"
<button on:click={() => editor.chain().focus().toggleBold().run()}
on:click={() => editor.chain().focus().toggleBold().run()} class:active={editor.isActive('bold')}
class:active={editor.isActive('bold')} >
> B
Bold </button>
</button> <button
class="button"
on:click={() => editor.chain().focus().toggleItalic().run()}
class:active={editor.isActive('italic')}
>
<em>IT</em>
</button>
<button
class="button"
on:click={() => editor.chain().focus().toggleUnderline().run()}
class:active={editor.isActive('underline')}
>
<u>U</u>
</button>
<button
class="button"
on:click={() => editor.chain().focus().toggleStrike().run()}
class:active={editor.isActive('strike')}
>
<s>S</s>
</button>
<button
class="button"
on:click={() => editor.commands.unsetAllMarks()}
>
Clear
</button>
<button
class="button"
on:click={() => editor.chain().focus().toggleCode().run()}
class:active={editor.isActive('code')}
>
Code
</button>
<button
class="button"
on:click={() => editor.chain().focus().toggleBulletList().run()}
class:active={editor.isActive('bulletList')}
>
<Icon icon="list"></Icon>
</button>
<button
class="button"
on:click={() => editor.chain().focus().toggleOrderedList().run()}
class:active={editor.isActive('orderedList')}
>
<Icon icon="ordered-list"></Icon>
</button>
<button
class="button"
on:click={() => editor.chain().focus().toggleBlockquote().run()}
class:active={editor.isActive('blockquote')}
>
""
</button>
<button
class="button"
on:click={() => editor.chain().focus().toggleCodeBlock().run()}
class:active={editor.isActive('codeBlock')}
>
cb
</button>
</div>
{/if} {/if}
<div bind:this={element} class="content"/> <div bind:this={element} class="content"/>
+57 -10
View File
@@ -1,21 +1,68 @@
<script> <script>
import {onMount} from "svelte"; import {onDestroy, onMount} from "svelte";
import Trix from "trix" import Trix from "trix"
import customcss from "./tinymce.css?inline";
import "trix/dist/trix.css" import "trix/dist/trix.css"
export let value = ""; export let value = "";
let textareaEl; export let field;
let lastVal; let editor;
let editorWrapper;
let activeEditor;
function updateValue(e) {
value = e.target.value;
}
export function insertMedia(info){
if(info.record._file.width > 0){
var attachment = new Trix.Attachment({ content: info.html })
editor.editor.insertAttachment(attachment)
}else{
editor.editor.insertHTML(`<a href="${info.originalUrl}">${info.record._file.originalName}</a>`)
}
}
onMount(() => {
editor.addEventListener("trix-file-accept", (e) => {
e.preventDefault();
})
editor.addEventListener("trix-before-initialize", (e) => {
Trix.config.blockAttributes.heading1.tagName = 'h2';
const { toolbarElement } = e.target
const h1Button = toolbarElement.querySelector("[data-trix-attribute=heading1]")
h1Button.insertAdjacentHTML("afterend", `<button style="text-indent: initial;padding: 14px 10px !important;" type="button" class="trix-button trix-button--icon" data-trix-attribute="heading3" title="Heading 3" tabindex="-1" data-trix-active="">H3</button>`)
})
})
// onDestroy(() => {
// editor.removeEventListener("trix-before-initialize")
// })
Trix.config.blockAttributes.default.breakOnReturn = false Trix.config.blockAttributes.default.breakOnReturn = false
console.log(Trix.config) Trix.config.blockAttributes.heading3 = {
tagName: 'h3',
terminal: true,
breakOnReturn: true,
group: false
}
// console.log(Trix.config)
</script> </script>
<div bind:this={editorWrapper} class="tox-wrapper"> <div class="tox-wrapper">
<input bind:this={textareaEl} id="x" bind:value type="hidden"> <input id="x-{field.name}" {value} type="hidden">
<trix-editor class="trix-content content" input="x"></trix-editor> <trix-editor
bind:this={editor}
class=" content"
input="x-{field.name}"
role="textbox"
tabindex="0"
on:trix-change={updateValue}
></trix-editor>
</div> </div>
+10
View File
@@ -98,6 +98,16 @@
bind:graph bind:graph
{record} {record}
/> />
{:else if field.info.name === "markdown"}
<Markdown
bind:value={data[field.name]}
{schema}
{field}
{validationErrors}
{isCreateMode}
bind:graph
{record}
/>
{:else} {:else}
<svelte:component <svelte:component
this={formElement} this={formElement}
+4 -1
View File
@@ -15,11 +15,14 @@
let backlinks = graph.parentEdges.map(edge => { let backlinks = graph.parentEdges.map(edge => {
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema); let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
let edgeField = findEdgeField(schema,edge.field); let edgeField = findEdgeField(schema,edge.field);
if(!edgeField){
return null;
}
return { return {
field: edgeField.label, field: edgeField.label,
record: graph.records.find( record => record.id === edge.source) record: graph.records.find( record => record.id === edge.source)
} }
}) }).filter( edgeOrNull => !!edgeOrNull)
</script> </script>
<div class="editor-field"> <div class="editor-field">
{#each backlinks as backlink} {#each backlinks as backlink}
@@ -13,11 +13,8 @@
{#if record?.data} {#if record?.data}
<a <a
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/records/{record.id}"
class="text-decoration-none rounded py-1 px-2 d-inline-block"
{title} {title}
style="border:2px solid {!schema.color class="reference"
? '#999'
: schema.color}!important;white-space: nowrap;"
> >
{title} {title}
</a> </a>
@@ -9,13 +9,17 @@
export let value; export let value;
export let isCreateMode; export let isCreateMode;
export let validationErrors; export let validationErrors;
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
$: errorMessage = getErrorMessage(validationErrors, field.name); $: errorMessage = getErrorMessage(validationErrors, field.name);
export let id; export let id;
let wrapperDiv;
let pickerInput; let pickerInput;
let pickerInstance; let pickerInstance;
let flatpickrOptions = { let flatpickrOptions = {
appendTo: wrapperDiv,
static: true,
allowInput: true, allowInput: true,
altInput: true, altInput: true,
altFormat: "Y-m-d H:i:S", altFormat: "Y-m-d H:i:S",
@@ -40,7 +44,7 @@
}); });
</script> </script>
<div class="mb-0"> <div class="mb-0" bind:this={wrapperDiv}>
<input <input
type="text" type="text"
@@ -1,21 +1,37 @@
<script> <script>
import Codemirror from "../../libs/CodemirrorMarkdown.svelte"; import Codemirror from "../../libs/CodemirrorMarkdown.svelte";
import { getErrorMessage } from "./errorMessage"; import { getErrorMessage } from "./errorMessage";
import RichEditorFiles from "./RichEditorFiles.svelte";
export let value; export let value;
export let field; export let field;
export let graph;
export let record;
export let isCreateMode; export let isCreateMode;
// export let id; // export let id;
export let validationErrors; export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name); $: errorMessage = getErrorMessage(validationErrors, field.name);
let editor;
function insertMedia(e){
editor.insertMedia(e.detail)
}
</script> </script>
<div class="mb-3"> <div class="mb-3">
<Codemirror bind:value editable={!field.readonly || isCreateMode} /> <Codemirror bind:this={editor} bind:value editable={!field.readonly || isCreateMode} />
{#if field.collections.length > 0}
<RichEditorFiles
bind:graph
{record}
{field}
{validationErrors}
on:editor-insert={insertMedia}
>
</RichEditorFiles>
{/if}
{#if errorMessage} {#if errorMessage}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">
{errorMessage} {errorMessage}
@@ -43,6 +43,7 @@
function createInlineReference(e, schemaUId) { function createInlineReference(e, schemaUId) {
e.preventDefault(); e.preventDefault();
inLineCreateRecord = null;
axios axios
.get(channel.lucentUrl + "/records/newInline?schema=" + schemaUId) .get(channel.lucentUrl + "/records/newInline?schema=" + schemaUId)
.then((response) => { .then((response) => {
@@ -59,27 +60,29 @@
<div <div
style="display: flex;align-items: center;gap:4px" style="display: flex;align-items: center;gap:4px"
> >
{#each schemas as schema} <Dropdown>
<Dropdown> <div slot="button">New</div>
<div slot="button" class:is-first={!recordId}> {#each schemas as schema}
{schema.label}
</div>
<button <button
class=" button" class=" button"
on:click={(e) => on:click={(e) =>
createInlineReference(e, schema.name)} createInlineReference(e, schema.name)}
>Create New Record >{schema.label}
</button> </button>
{/each}
</Dropdown>
<Dropdown>
<div slot="button"> <Icon icon="magnifying-glass"/></div>
{#each schemas as schema}
<button <button
class="button" class="button"
on:click={(e) => openBrowseModal(e, schema.name)} on:click={(e) => openBrowseModal(e, schema.name)}
> >{schema.label}
<Icon icon="magnifying-glass"/> </button>
Search {/each}
</button
> </Dropdown>
</Dropdown>
{/each}
</div> </div>
{:else} {:else}
<div style="display:flex;align-items: center;gap: 4px"> <div style="display:flex;align-items: center;gap: 4px">
@@ -100,9 +103,9 @@
<DialogRecord bind:this={dialogRecord}> <DialogRecord bind:this={dialogRecord}>
{#if inLineCreateRecord} {#if inLineCreateRecord}
<InlineEdit <InlineEdit
{...inLineCreateRecord} {...inLineCreateRecord}
isCreateMode={true}
on:cancel={(e) => (inLineCreateRecord = null)} on:cancel={(e) => (inLineCreateRecord = null)}
on:inlinesaved={save} on:inlinesaved={save}
/> />
@@ -2,6 +2,7 @@
import Tinymce from "../../libs/Tinymce.svelte"; import Tinymce from "../../libs/Tinymce.svelte";
import RichEditorFiles from "./RichEditorFiles.svelte"; import RichEditorFiles from "./RichEditorFiles.svelte";
import {getErrorMessage} from "./errorMessage"; import {getErrorMessage} from "./errorMessage";
import Trix from "../../libs/Trix.svelte";
export let value; export let value;
export let field; export let field;
@@ -24,8 +25,8 @@
<div class="mb-0"> <div class="mb-0">
<Trix {field} bind:this={editor} bind:value></Trix>
<Tinymce bind:this={editor} bind:value {additionalConfig}/> <!-- <Tinymce bind:this={editor} bind:value {additionalConfig}/>-->
{#if field.collections.length > 0} {#if field.collections.length > 0}
<RichEditorFiles <RichEditorFiles
bind:graph bind:graph
@@ -38,7 +39,7 @@
</RichEditorFiles> </RichEditorFiles>
{/if} {/if}
<!-- <TipTap bind:value />-->
{#if errorMessage} {#if errorMessage}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">
@@ -1,6 +1,4 @@
<script> <script>
import {sortByField} from "../../edges/sortEdges";
import Sortable from "../../libs/Sortable.svelte";
import PreviewFile from "../previews/PreviewFile.svelte"; import PreviewFile from "../previews/PreviewFile.svelte";
import Dropdown from "../../common/Dropdown.svelte"; import Dropdown from "../../common/Dropdown.svelte";
import Dialog from "../../dialog/Dialog.svelte"; import Dialog from "../../dialog/Dialog.svelte";
@@ -70,7 +68,8 @@
{#each references as reference (reference.id)} {#each references as reference (reference.id)}
<!--This div helps the sorting thing--> <!--This div helps the sorting thing-->
<div> <div>
<PreviewFile record={reference} hasDelete={true} hasInsert={true} on:remove={removeReference} on:editor-insert></PreviewFile> <PreviewFile record={reference} hasDelete={true} hasInsert={true} on:remove={removeReference}
on:editor-insert></PreviewFile>
</div> </div>
{/each} {/each}
{/if} {/if}
@@ -4,7 +4,7 @@
import {createEventDispatcher, getContext} from "svelte"; import {createEventDispatcher, getContext} from "svelte";
import Preview from "../../files/Preview.svelte"; import Preview from "../../files/Preview.svelte";
import {previewTitle} from "./../Preview"; import {previewTitle} from "./../Preview";
import {htmlurl} from "../../files/imageserver.js" import {fileurl, htmlurl} from "../../files/imageserver.js"
import Status from "./../Status.svelte"; import Status from "./../Status.svelte";
import Dropdown from "../../common/Dropdown.svelte"; import Dropdown from "../../common/Dropdown.svelte";
@@ -25,8 +25,14 @@
function insert(e, preset) { function insert(e, preset) {
e.preventDefault(); e.preventDefault();
let html = htmlurl(channel,record, preset) let html = htmlurl(channel, record, preset)
dispatch("editor-insert", html); let url = !preset ? `/${record._file.path}` : `/templates/${preset}/${record._file.path}`;
dispatch("editor-insert", {
html: html,
url: channel.filesUrl + url,
originalUrl: channel.filesUrl + "/" + record._file.path,
record: record
});
} }
</script> </script>
+20
View File
@@ -0,0 +1,20 @@
<script>
import Step from "./Step.svelte"
export let steps;
export let allSuccess = false;
console.log(steps);
</script>
<div class="wrapper-tiny">
{#each steps as step}
<Step {step}></Step>
{/each}
<div style="text-align: center;margin-top: 30px;">
{#if allSuccess}
<a href="/lucent/register" class="bt">Create the first user</a>
{/if}
</div>
</div>
+67
View File
@@ -0,0 +1,67 @@
<script>
import Icon from "../common/Icon.svelte"
export let step;
</script>
<div class="step step-{step.status}">
<div class="step-icon">
{#if step.status === "success"}
<Icon icon="check"></Icon>
{:else}
<Icon icon="close"></Icon>
{/if}
</div>
<div style="width:100%">
<h4>{step.name}</h4>
<details>
<summary>Instuctions</summary>
<code class="instructions">{step.instructions}</code>
</details>
</div>
</div>
<style>
.step-success .step-icon{
background: var(--suc10);
color: var(--suc100);
}
.step-fail .step-icon{
background: var(--err10);
color: var(--err100);
}
.step-icon{
padding: 12px;
border-radius: 12px;
}
.step {
width: 100%;
display: flex;
align-items: start;
gap: 10px;
justify-content: space-between;
padding: 12px;
border-radius: 12px;
}
details {
width: 100%;
}
.instructions {
margin-top: 20px;
padding: 12px;
border-radius: 12px;
background: var(--p10);
white-space: break-spaces;
display: block;
}
</style>
+2562 -587
View File
File diff suppressed because it is too large Load Diff
+3 -20
View File
@@ -11,40 +11,23 @@
"@codemirror/lang-markdown": "^6.2.5", "@codemirror/lang-markdown": "^6.2.5",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@sveltejs/vite-plugin-svelte": "^3.1.1", "@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tiptap/core": "^2.6.4",
"@tiptap/extension-blockquote": "^2.6.4",
"@tiptap/extension-bold": "^2.6.4",
"@tiptap/extension-bullet-list": "^2.6.4",
"@tiptap/extension-code": "^2.6.4",
"@tiptap/extension-document": "^2.6.4",
"@tiptap/extension-heading": "^2.6.4",
"@tiptap/extension-history": "^2.6.4",
"@tiptap/extension-italic": "^2.6.4",
"@tiptap/extension-list-item": "^2.6.4",
"@tiptap/extension-ordered-list": "^2.6.4",
"@tiptap/extension-paragraph": "^2.6.4",
"@tiptap/extension-strike": "^2.6.4",
"@tiptap/extension-table": "^2.6.4",
"@tiptap/extension-table-cell": "^2.6.4",
"@tiptap/extension-table-header": "^2.6.4",
"@tiptap/extension-table-row": "^2.6.4",
"@tiptap/extension-text": "^2.6.4",
"@tiptap/extension-underline": "^2.6.4",
"@tiptap/pm": "^2.6.4",
"axios": "^1.7.4", "axios": "^1.7.4",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"flatpickr": "^4.6.13", "flatpickr": "^4.6.13",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"htmx.org": "^2.0.1", "htmx.org": "^2.0.1",
"install": "^0.13.0",
"laravel-vite-plugin": "^1.0.5", "laravel-vite-plugin": "^1.0.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"npm": "^10.8.2",
"postcss": "8.4.31", "postcss": "8.4.31",
"sass": "^1.77.8", "sass": "^1.77.8",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
"svelte": "^4.2.18", "svelte": "^4.2.18",
"tinymce": "^6.8.4", "tinymce": "^6.8.4",
"trix": "^2.1.5",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"vite": "5.2.6" "vite": "5.2.6"
} }
+5
View File
@@ -7,11 +7,16 @@
.cm-content{ .cm-content{
background-color: var(--p10); background-color: var(--p10);
color: var(--p100);
} }
} }
.cm-content{ .cm-content{
background-color: var(--p20); background-color: var(--p20);
}
.ͼ4 .cm-line ::selection, .ͼ4 .cm-line::selection{
background: var(--p40) !important;
} }
.cm-activeLine{ .cm-activeLine{
+43
View File
@@ -0,0 +1,43 @@
.flatpickr-wrapper {
display: block !important;
}
.editor-field {
.flatpickr-calendar {
border-radius: 12px !important;
}
.flatpickr-months .flatpickr-month {
background: var(--p30);
color: var(--text);
font-size: 12px;
}
.flatpickr-current-month .flatpickr-monthDropdown-months {
background: var(--p30);
}
.flatpickr-weekdays{
background: var(--p30);
color: var(--text);
}
.flatpickr-weekdaycontainer .flatpickr-weekday{
background: var(--p30);
color: var(--text);
}
.flatpickr-days{
background: var(--p10);
color: var(--text);
}
.flatpickr-time{
background: var(--p10);
color: var(--text);
}
}
+2 -2
View File
@@ -15,7 +15,7 @@ body:has(dialog[open]) {
dialog { dialog {
margin: 2vh auto; margin: 2vh auto;
background-color: #fff; background-color: var(--p10);
padding: 34px; padding: 34px;
border: none; border: none;
border-radius: 12px; border-radius: 12px;
@@ -49,6 +49,6 @@ dialog::backdrop {
position: sticky; position: sticky;
top: -34px; top: -34px;
z-index: 999; z-index: 999;
background: #fff; background-color: var(--p10);
padding: 10px 0; padding: 10px 0;
} }
+1 -1
View File
@@ -16,7 +16,7 @@
overflow: visible; overflow: visible;
position: absolute; position: absolute;
border-radius: 12px; border-radius: 12px;
z-index: 20; z-index: 22;
background: var(--p20); background: var(--p20);
transition: 600ms; transition: 600ms;
flex-grow: 1; flex-grow: 1;
+6 -4
View File
@@ -1,6 +1,6 @@
.record-edit { .record-edit {
position: relative; position: relative;
max-width: 900px;
.invalid-feedback { .invalid-feedback {
color: var(--text-error); color: var(--text-error);
font-size: 15px; font-size: 15px;
@@ -44,7 +44,7 @@
margin: 6px 0; margin: 6px 0;
border-color: transparent; border-color: transparent;
.button { .button:not(.primary) {
background: var(--p30); background: var(--p30);
&:hover { &:hover {
@@ -53,9 +53,10 @@
} }
dialog { dialog {
.button { .button:not(.primary) {
background: var(--p20); background: var(--p20);
&:hover { &:hover {
background: var(--p30); background: var(--p30);
} }
@@ -142,4 +143,5 @@
} }
} }
} }
+150
View File
@@ -0,0 +1,150 @@
.tiptap {
width: 100%;
background: var(--p20);
border: 1px solid var(--p50);
border-radius: 0 0 5px 5px;
padding: 15px 15px;
font-size: 16px;
:first-child {
margin-top: 0;
}
&:focus {
background: var(--p10);
}
img {
&.ProseMirror-selectednode {
box-shadow: 0 0 1px 2px var(--p70);
}
}
}
.editor-field {
.editor-toolbar {
display: flex;
gap: 4px;
background: var(--p30);
border-radius: 5px 5px 0 0;
padding: 5px 7px;
.button:not(.primary) {
font-weight: 700;
&.active {
background: var(--p40);
}
}
}
}
.content {
.tiptap {
li > p {
display: inline;
}
}
}
trix-editor {
background: var(--p20)!important;
border: 1px solid var(--p50)!important;
border-radius: 0 0 5px 5px!important;
padding: 15px 15px!important;
& > div {
margin-bottom: 14px;
font-size: 16px;
line-height: 23px;
}
&:focus {
background: var(--p10)!important;
}
figure.attachment{
display: flex!important;
flex-direction: column!important;
justify-content: center;
align-items: center;
gap: 10px;
}
.attachment {
background: var(--p20);
padding: 12px 0;
text-align: center;
display: flex;
justify-content: center;
img {
margin-bottom: 0;
}
}
[data-trix-mutable].attachment img {
box-shadow: 0 0 1px 2px var(--p70) !important;
}
.trix-button--remove {
box-shadow: none !important;
border: 2px solid var(--p40) !important;
}
.trix-button--remove:hover {
border: 2px solid var(--p40);
}
a {
color: var(--p80);
}
}
trix-toolbar {
.trix-button-row {
display: flex;
}
.trix-button-group {
background: transparent !important;
border: none !important;
display: flex !important;
gap: 4px;
}
.trix-button-group--history-tools,.trix-button-group--file-tools
{
display: none !important;
}
.trix-button {
border-radius: 6px !important;
background: var(--p30) !important;
padding: 14px 22px !important;
margin: 0 !important;
cursor: pointer;
border: 0px solid var(--p30) !important;
font-size: 14px !important;
min-height: 27px !important;
display: flex !important;
align-items: center !important;
gap: 4px;
color: var(--text) !important;
&:before{
background-size: 22px!important;
}
&:hover{
background: var(--p40) !important;
}
&.trix-active{
background: var(--p50) !important;
}
}
}
+5 -1
View File
@@ -29,10 +29,14 @@
align-items: center; align-items: center;
background: var(--p30); background: var(--p30);
font-size: 16px; font-size: 16px;
padding: 3px 12px 6px; padding: 3px 12px 3px;
color: var(--text); color: var(--text);
border: none; border: none;
border-radius: 12px; border-radius: 12px;
&:focus{
box-shadow: none;
}
&:hover { &:hover {
background: var(--p40); background: var(--p40);
+12
View File
@@ -113,6 +113,18 @@
.field-ui-number { .field-ui-number {
text-align: right; text-align: right;
} }
.references{
display: flex;
gap: 4px;
.reference{
font-size: 13px;
border-radius: 12px;
background: var(--p30);
padding: 1px 5px;
}
}
} }
.file-table-row { .file-table-row {
+1
View File
@@ -3,6 +3,7 @@
margin: 20px 0 20px; margin: 20px 0 20px;
display: flex; display: flex;
gap: 4px; gap: 4px;
flex-wrap: wrap;
.tab{ .tab{
list-style: none; list-style: none;
+62
View File
@@ -12,6 +12,20 @@
margin-bottom: 0; margin-bottom: 0;
} }
} }
h1{
font-size: 24px;
line-height: 34px;
}
h2{
font-size: 20px;
line-height: 30px;
}
h3{
font-size: 18px;
line-height: 28px;
}
ul { ul {
padding: 0 0 0 16px; padding: 0 0 0 16px;
@@ -31,6 +45,54 @@
} }
} }
code{
background: var(--p30);
padding: 0 6px;
border-radius: 12px;
}
img{
margin-bottom: 14px;
}
blockquote{
border:1px solid var(--p30);
border-radius: 12px;
padding: 12px 40px;
position: relative;
&::before{
content: "\201C";
color: var(--p60);
font-size:4em;
position: absolute;
left: 10px;
top: 20px;
}
&::after{
content: '';
}
}
pre {
background: var(--grey-light);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
} }
.lx-small-text { .lx-small-text {
+2 -1
View File
@@ -53,6 +53,7 @@
@import "./table"; @import "./table";
@import "./avatar"; @import "./avatar";
@import "./codemirror"; @import "./codemirror";
@import "./rich";
@import "./layout"; @import "./layout";
@import "./wrappers"; @import "./wrappers";
@import "./toolbar"; @import "./toolbar";
@@ -69,6 +70,7 @@
@import "./reference-tags"; @import "./reference-tags";
@import "./members"; @import "./members";
@import "./revisions"; @import "./revisions";
@import "./datepicker";
body { body {
background-color: var(--p10); background-color: var(--p10);
@@ -103,4 +105,3 @@ a {
.lucent-component { .lucent-component {
position: relative; position: relative;
} }
+2 -1
View File
@@ -9,7 +9,8 @@
<div class="form"> <div class="form">
<h2 class="mb-5">Enter Lucent</h2> <h2 class="mb-5">Enter Lucent</h2>
<form hx-post="/lucent/login" > <form hx-post="{{url('lucent/login')}}" >
@csrf
<p>Submit your email address and you will receive a <b>login link</b> to your email</p> <p>Submit your email address and you will receive a <b>login link</b> to your email</p>
<p>Don't forget to check your spam folder</p> <p>Don't forget to check your spam folder</p>
<div class="mt-5 mb-3"> <div class="mt-5 mb-3">
+1 -1
View File
@@ -9,7 +9,7 @@
<div class="form"> <div class="form">
<h2 class="mb-5">Welcome to Lucent</h2> <h2 class="mb-5">Welcome to Lucent</h2>
<form hx-post="/lucent/verify" hx-redirect="/lucent" hx-target-error=".form-errors" > <form hx-post="{{url('lucent/verify')}}" hx-redirect="{{url('lucent')}}" hx-target-error=".form-errors" >
<input type="hidden" value="{{$email}}" name="email" /> <input type="hidden" value="{{$email}}" name="email" />
<input type="hidden" value="{{$token}}" name="token" /> <input type="hidden" value="{{$token}}" name="token" />
@csrf @csrf
+3 -3
View File
@@ -9,8 +9,8 @@
@if(config("lucent.env") === "production") @if(config("lucent.env") === "production")
<!-- if production --> <!-- if production -->
<link rel="stylesheet" href="/vendor/lucent/dist/{{ $manifest['main.css']["file"] }}"/> <link rel="stylesheet" href="{{url('vendor/lucent/dist/'.$manifest['main.js']["css"][0])}}"/>
<script type="module" src="/vendor/lucent/dist/{{ $manifest['main.js']["file"] }}"></script> <script type="module" src="{{url('vendor/lucent/dist/'.$manifest['main.js']["file"])}}"></script>
@else @else
<!-- if development --> <!-- if development -->
@php @php
@@ -20,7 +20,7 @@
@endif @endif
{{-- <link rel="icon" type="image/x-icon" href="/favicon.ico"/>--}} <link rel="icon" type="image/x-icon" href="{{url('favicon.ico')}}">
</head> </head>
+5 -7
View File
@@ -8,8 +8,8 @@
<title>@yield('title') - Lucent Data Platform</title> <title>@yield('title') - Lucent Data Platform</title>
@if(config("lucent.env") == "production") @if(config("lucent.env") == "production")
<!-- if production --> <!-- if production -->
<link rel="stylesheet" href="/vendor/lucent/dist/{{ $manifest['main.css']["file"] }}"/> <link rel="stylesheet" href="{{url('vendor/lucent/dist/'.$manifest['main.js']["css"][0])}}"/>
<script type="module" src="/vendor/lucent/dist/{{ $manifest['main.js']["file"] }}"></script> <script type="module" src="{{url('vendor/lucent/dist/'.$manifest['main.js']["file"])}}"></script>
@else @else
<!-- if development --> <!-- if development -->
@php @php
@@ -18,16 +18,14 @@
<script type="module" crossorigin src="http://127.0.0.1:5173/main.js"></script> <script type="module" crossorigin src="http://127.0.0.1:5173/main.js"></script>
@endif @endif
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/x-icon" href="{{url('favicon.ico')}}">
</head> </head>
<body> <body>
@yield('content') @yield('content')
</body> </body>
</html> </html>
+1 -1
View File
@@ -10,7 +10,7 @@ export default defineConfig({
outDir: '../dist', outDir: '../dist',
emptyOutDir: true, emptyOutDir: true,
manifest: true, manifest: "manifest.json",
rollupOptions: { rollupOptions: {
// overwrite default .html entry // overwrite default .html entry
input: path.resolve(__dirname, 'js/main.js') input: path.resolve(__dirname, 'js/main.js')
+11 -11
View File
@@ -3,7 +3,7 @@
namespace Lucent\Account; namespace Lucent\Account;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\DB; use Lucent\Database\Database;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use PhpOption\Option; use PhpOption\Option;
@@ -12,7 +12,7 @@ class UserRepo
public function count(): int public function count(): int
{ {
return DB::table("users")->count(); return Database::make()->table("users")->count();
} }
/** /**
@@ -20,7 +20,7 @@ class UserRepo
*/ */
public function all(): Collection public function all(): Collection
{ {
$usersData = DB::table("users")->get(); $usersData = Database::make()->table("users")->get();
$users = array_map(fn($userData) => $this->fromArray((array)$userData), $usersData->toArray()); $users = array_map(fn($userData) => $this->fromArray((array)$userData), $usersData->toArray());
return new Collection($users); return new Collection($users);
@@ -31,14 +31,14 @@ class UserRepo
{ {
$userData = toArray($user); $userData = toArray($user);
$userData["roles"] = json_encode($userData["roles"]); $userData["roles"] = json_encode($userData["roles"]);
DB::table("users")->insert($userData); Database::make()->table("users")->insert($userData);
} }
public function update(User $user): void public function update(User $user): void
{ {
$userData = toArray($user); $userData = toArray($user);
$userData["roles"] = json_encode($userData["roles"]); $userData["roles"] = json_encode($userData["roles"]);
DB::table("users")->where("id", $user->id)->update($userData); Database::make()->table("users")->where("id", $user->id)->update($userData);
} }
@@ -46,7 +46,7 @@ class UserRepo
{ {
$newToken = Token::new(32); $newToken = Token::new(32);
DB::table("users") Database::make()->table("users")
->where("id", $id) ->where("id", $id)
->update([ ->update([
'loggedInAt' => Carbon::now()->toJson(), 'loggedInAt' => Carbon::now()->toJson(),
@@ -62,7 +62,7 @@ class UserRepo
*/ */
public function findByEmail(Email $email): Option public function findByEmail(Email $email): Option
{ {
$user = DB::table("users")->where("email", $email->value())->first(); $user = Database::make()->table("users")->where("email", $email->value())->first();
if (empty($user)) { if (empty($user)) {
return none(); return none();
@@ -76,7 +76,7 @@ class UserRepo
*/ */
public function findById(string $id): Option public function findById(string $id): Option
{ {
$user = DB::table("users")->where("id", $id)->first(); $user = Database::make()->table("users")->where("id", $id)->first();
if (empty($user)) { if (empty($user)) {
return none(); return none();
@@ -88,12 +88,12 @@ class UserRepo
public function updateName(string $userId, Name $name): void public function updateName(string $userId, Name $name): void
{ {
DB::table("users")->where("id", $userId)->update(["name" => $name->value]); Database::make()->table("users")->where("id", $userId)->update(["name" => $name->value]);
} }
public function updateEmail(string $userId, Email $email): void public function updateEmail(string $userId, Email $email): void
{ {
DB::table("users")->where("id", $userId)->update(["email" => $email->value()]); Database::make()->table("users")->where("id", $userId)->update(["email" => $email->value()]);
} }
public function fromArray(array $data): User public function fromArray(array $data): User
@@ -102,7 +102,7 @@ class UserRepo
id: $data["id"], id: $data["id"],
name: new Name($data["name"] ?? ""), name: new Name($data["name"] ?? ""),
email: new Email($data["email"]), email: new Email($data["email"]),
roles: json_decode($data["roles"] ?? "[]",true), roles: json_decode($data["roles"] ?? "[]", true),
createdAt: $data["createdAt"], createdAt: $data["createdAt"],
updatedAt: $data["updatedAt"], updatedAt: $data["updatedAt"],
loggedInAt: $data["loggedInAt"] ?? null, loggedInAt: $data["loggedInAt"] ?? null,
+26 -3
View File
@@ -2,31 +2,54 @@
namespace Lucent\Channel; namespace Lucent\Channel;
use Lucent\Channel\Data\UserCommand;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use Lucent\Schema\FilesSchema;
use Lucent\Schema\Schema; use Lucent\Schema\Schema;
final class Channel final class Channel
{ {
public string $lucentUrl; public string $lucentUrl;
public string $filesUrl; public string $filesUrl;
public array $disks;
public string $previewTargetUrl; public string $previewTargetUrl;
/** /**
* @param Collection<Schema> $schemas * @param Collection<Schema> $schemas
* @param Collection<UserCommand> $commands
*/ */
function __construct( function __construct(
public string $name, public string $name,
public string $url, public string $url,
public string $previewTarget, public string $previewTarget,
public string $generateCommand, public Collection $commands,
public Collection $schemas, public Collection $schemas,
public array $imageFilters, public array $imageFilters,
public array $roles, public array $roles,
) )
{ {
$this->lucentUrl = $url . "/lucent"; $this->lucentUrl = $url . "/lucent";
$this->filesUrl = $url . "/storage"; $this->filesUrl = $this->makeFilesUrl();
$this->previewTargetUrl = $url . "/". $previewTarget; $this->disks = $this->getDisksFromSchemas();
$this->previewTargetUrl = $url . "/" . $previewTarget;
} }
private function makeFilesUrl(): string
{
return match (config("filesystems.disks.lucent.driver")) {
"s3" => config("filesystems.disks.lucent.endpoint") . "/" . config("filesystems.disks.lucent.bucket"),
"local" => $this->url . "/storage" . config("filesystems.disks.lucent.endpoint"),
default => ""
};
}
private function getDisksFromSchemas()
{
return $this->schemas->filter(fn(Schema $schema) => get_class($schema) === FilesSchema::class)->reduce(function (array $carry, Schema $schema) {
$carry[$schema->disk] = config("filesystems.disks." . $schema->disk . ".url");
return $carry;
}, []);
}
} }
+11 -5
View File
@@ -3,6 +3,7 @@
namespace Lucent\Channel; namespace Lucent\Channel;
use Lucent\Channel\Data\UserCommand;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use Lucent\Schema\Schema; use Lucent\Schema\Schema;
use Lucent\Schema\SchemaService; use Lucent\Schema\SchemaService;
@@ -13,7 +14,7 @@ final class ChannelService
public Channel $channel; public Channel $channel;
private function __construct( private function __construct(
public SchemaService $schemaService public SchemaService $schemaService,
) )
{ {
@@ -29,11 +30,16 @@ final class ChannelService
$schemaService = new SchemaService(); $schemaService = new SchemaService();
$schemasCollection = (new Collection($schemasArray["schemas"] ?? []))->map([$schemaService, 'fromArray']); $schemasCollection = (new Collection($schemasArray["schemas"] ?? []))->map([$schemaService, 'fromArray']);
$userCommands = [];
foreach (config("lucent.commands") ?? [] as $signature => $desc) {
$userCommands[] = new UserCommand($desc, $signature);
}
$channel = new Channel( $channel = new Channel(
name: config("lucent.name") ?? "", name: config("lucent.name") ?? "",
url: rtrim(config("lucent.url") ?? "", "/"), url: rtrim(config("lucent.url") ?? "", "/"),
previewTarget: rtrim(config("lucent.previewTarget") ?? "", "/"), previewTarget: rtrim(config("lucent.previewTarget") ?? "", "/"),
generateCommand: config("lucent.generateCommand") ?? "", commands: Collection::make($userCommands),
schemas: $schemasCollection, schemas: $schemasCollection,
imageFilters: config("lucent.imageFilters") ?? [], imageFilters: config("lucent.imageFilters") ?? [],
roles: $schemasArray["roles"] ?? [] roles: $schemasArray["roles"] ?? []
@@ -63,7 +69,7 @@ final class ChannelService
*/ */
public function schemasReadableByRoles(array $roles): array public function schemasReadableByRoles(array $roles): array
{ {
$schemasAllRead = $this->channel->schemas->filter(fn(Schema $schema) => empty($schema->read))->values()->pluck("name"); $schemasAllRead = $this->channel->schemas->filter(fn(Schema $schema) => empty($schema->read))->values()->pluck("name");
$schemasCanRead = $this->channel->schemas->filter(fn(Schema $schema) => count(array_intersect($schema->read ?? [], $roles)) > 0)->values()->pluck("name"); $schemasCanRead = $this->channel->schemas->filter(fn(Schema $schema) => count(array_intersect($schema->read ?? [], $roles)) > 0)->values()->pluck("name");
$schemasCanWrite = $this->channel->schemas->filter(fn(Schema $schema) => count(array_intersect($schema->write ?? [], $roles)) > 0)->values()->pluck("name"); $schemasCanWrite = $this->channel->schemas->filter(fn(Schema $schema) => count(array_intersect($schema->write ?? [], $roles)) > 0)->values()->pluck("name");
return $schemasAllRead->merge($schemasCanRead)->merge($schemasCanWrite)->unique()->values()->toArray(); return $schemasAllRead->merge($schemasCanRead)->merge($schemasCanWrite)->unique()->values()->toArray();
@@ -76,8 +82,8 @@ final class ChannelService
*/ */
public function schemasWritableByRoles(array $roles): array public function schemasWritableByRoles(array $roles): array
{ {
$schemasAllRead = $this->channel->schemas->filter(fn(Schema $schema) => empty($schema->write ?? []))->values()->pluck("name"); $schemasAllRead = $this->channel->schemas->filter(fn(Schema $schema) => empty($schema->write ?? []))->values()->pluck("name");
$schemasCanWrite = $this->channel->schemas->filter(fn(Schema $schema) => count(array_intersect($schema->write ?? [], $roles)) > 0)->values()->pluck("name"); $schemasCanWrite = $this->channel->schemas->filter(fn(Schema $schema) => count(array_intersect($schema->write ?? [], $roles)) > 0)->values()->pluck("name");
return $schemasAllRead->merge($schemasCanWrite)->unique()->values()->toArray(); return $schemasAllRead->merge($schemasCanWrite)->unique()->values()->toArray();
} }
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Lucent\Channel\Data;
class UserCommand
{
public function __construct(
public string $name,
public string $signature,
)
{
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace Lucent\Command;
use Lucent\Command\Data\CommandLogItem;
use Lucent\Database\Database;
class CommandRepo
{
public function findBySignature($signature): ?CommandLogItem
{
$row = Database::make()->table("command_logs")->where("signature", $signature)->first();
if (empty($row)) {
return null;
}
return CommandLogItem::fromDB($row);
}
public function upsertCommand(CommandLogItem $commandLogItem): void
{
$foundCommandLogItem = $this->findBySignature($commandLogItem->signature);
if (empty($foundCommandLogItem)) {
Database::make()->table("command_logs")->insert(toArray($commandLogItem));
return;
}
Database::make()->table("command_logs")->where("signature", $commandLogItem->signature)->update(toArray($commandLogItem));
}
public function appendToLogs(string $signature, string $line): void
{
Database::make()->update(
'update command_logs set logs = logs || ? where signature = ?',
[$line, $signature]
);
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace Lucent\Command;
use Lucent\Command\Data\CommandLogItem;
use Lucent\Id\Id;
use Lucent\LucentException;
use Symfony\Component\Process\PhpExecutableFinder;
class CommandService
{
public function __construct(
private CommandRepo $commandRepo,
)
{
}
public function run(string $signature): CommandLogItem
{
$commandLogItem = $this->commandRepo->findBySignature($signature);
if (empty($commandLogItem)) {
$commandLogItem = new CommandLogItem(
id: Id::new(),
signature: $signature,
pid: null,
logs: ""
);
} elseif ($this->commandIsRunning($commandLogItem->pid)) {
throw new LucentException('Command is already running');
}
$commandLogItem->pid = $this->runCommand($signature);
$commandLogItem->logs = "";
$this->commandRepo->upsertCommand($commandLogItem);
return $commandLogItem;
}
public function logWriter(string $signature): callable
{
return function (string $line) use($signature) {
$this->commandRepo->appendToLogs($signature, $line.PHP_EOL);
};
}
private function runCommand(string $signature): int
{
$phpBinaryFinder = new PhpExecutableFinder();
$phpBinaryPath = $phpBinaryFinder->find();
$pid = (int)shell_exec("cd " . base_path() . " && $phpBinaryPath artisan {$signature} > /dev/null 2>&1 & echo $!");
return $pid;
}
public function commandIsRunning(int $pid): bool
{
return file_exists("/proc/$pid");
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Lucent\Command\Data;
use stdClass;
class CommandLogItem
{
public function __construct(
public string $id,
public string $signature,
public ?int $pid,
public string $logs,
)
{
}
public static function fromDB(stdClass $data): self
{
return new self(
id: $data->id,
signature: $data->signature,
pid: $data->pid,
logs: $data->logs,
);
}
}
+3 -9
View File
@@ -16,19 +16,13 @@ class CompileSchemas extends Command
protected $description = 'Compiles schemas'; protected $description = 'Compiles schemas';
public function __construct( public function handle(SchemaService $schemaService)
public SchemaService $schemaService
)
{ {
parent::__construct();
}
public function handle()
{
$configDir = base_path(config('lucent.schemas_path')); $configDir = base_path(config('lucent.schemas_path'));
$schemasDirIterator = new DirectoryIterator($configDir); $schemasDirIterator = new DirectoryIterator($configDir);
$schemas = []; $schemas = [];
foreach ($schemasDirIterator as $file) { foreach ($schemasDirIterator as $file) {
if ($file->getExtension() !== "json") { if ($file->getExtension() !== "json") {
continue; continue;
@@ -46,7 +40,7 @@ class CompileSchemas extends Command
$schemas = collect($schemas)->sortBy("label")->values(); $schemas = collect($schemas)->sortBy("label")->values();
$roles = $schemas $roles = $schemas
->map([$this->schemaService, 'fromArray']) ->map([$schemaService, 'fromArray'])
->whereIn("type", [Type::COLLECTION, Type::FILES]) ->whereIn("type", [Type::COLLECTION, Type::FILES])
->reduce(fn($carry, Schema $schema) => array_merge( ->reduce(fn($carry, Schema $schema) => array_merge(
$carry, $carry,
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace Lucent\Commands;
use Illuminate\Console\Command;
use Lucent\Primitive\Collection;
use Lucent\Schema\CollectionSchema;
class GenerateCollectionSchema extends Command
{
protected $signature = 'lucent:generate:collection {name}';
protected $description = 'Generate a lucent collection';
public function handle()
{
$name = $this->argument('name');
$schema = new CollectionSchema(
name: $name,
label: $name,
visible: [],
groups: [],
fields: Collection::make(),
);
$json = json_encode($schema, JSON_PRETTY_PRINT);
$configDir = base_path(config('lucent.schemas_path'));
$schemaPath = $configDir . "/" . $name . '.json';
if(file_exists($schemaPath)){
$this->error("The schema file already exists.");
return 0;
}
file_put_contents($schemaPath, $json);
$this->info("The schema file has been created.");
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace Lucent\Commands;
use Illuminate\Console\Command;
use Lucent\Primitive\Collection;
use Lucent\Schema\FilesSchema;
class GenerateFileSchema extends Command
{
protected $signature = 'lucent:generate:file {name}';
protected $description = 'Generate a lucent file schema';
public function handle()
{
$name = $this->argument('name');
$schema = new FilesSchema(
name: $name,
label: $name,
fields: Collection::make(),
disk: "lucent",
path: $name,
groups: []
);
$json = json_encode($schema, JSON_PRETTY_PRINT);
$configDir = base_path(config('lucent.schemas_path'));
$schemaPath = $configDir . "/" . $name . '.json';
if (file_exists($schemaPath)) {
$this->error("The schema file already exists.");
return 0;
}
file_put_contents($schemaPath, $json);
$this->info("The schema file has been created.");
}
}
+14 -38
View File
@@ -7,6 +7,9 @@ use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use Lucent\Channel\ChannelService; use Lucent\Channel\ChannelService;
use Lucent\File\FileService;
use Lucent\Query\Query;
use Lucent\Schema\FilesSchema;
use Lucent\Schema\Schema; use Lucent\Schema\Schema;
use Lucent\Schema\Type; use Lucent\Schema\Type;
@@ -18,7 +21,10 @@ class RebuildThumbnails extends Command
protected $description = 'Rebuilds thumbnails for path'; protected $description = 'Rebuilds thumbnails for path';
public function __construct(public ImageManager $imageManager) public function __construct(
public Query $query,
public FileService $fileService,
)
{ {
parent::__construct(); parent::__construct();
} }
@@ -27,49 +33,19 @@ class RebuildThumbnails extends Command
public function handle(ChannelService $channelService): int public function handle(ChannelService $channelService): int
{ {
$channelService->channel->schemas $channelService->channel->schemas
->where("type", Type::FILES)->values() ->filter(fn(Schema $schema) => get_class($schema) === FilesSchema::class)
->map([$this, 'rebuildThumbnails']); ->map([$this, 'rebuildThumbnails']);
return 0; return 0;
} }
public function rebuildThumbnails(Schema $schema): void public function rebuildThumbnails(FilesSchema $schema): void
{ {
$this->info("Rebuilding thumbnails for ". $schema->name);
$filesDir = storage_path("app/public/" . $schema->path . "/"); $records = $this->query->filter(["schema" => $schema->name])->run()->records;
$thumbDir = storage_path("app/public/thumbs/" . $schema->path . "/"); $disk = $this->fileService->loadDisk($schema->disk);
if (!file_exists($thumbDir)) { foreach ($records as $record) {
make_dir_r($thumbDir); $this->fileService->createTemplates($disk, $record->_file->path);
}
if (!file_exists($filesDir)) {
make_dir_r($filesDir);
}
$filesDirIterator = new DirectoryIterator($filesDir);
foreach ($filesDirIterator as $file) {
if ($file->isDot()) {
continue;
}
try {
$image = $this->imageManager->make($filesDir . $file->getFilename());
} catch (Exception $e) {
$this->error($e->getMessage());
continue;
}
$image->fit(300, 300);
try {
$image->encode('webp', 75)->save($thumbDir . $file->getFilename());
} catch (Exception $e) {
$this->error($e->getMessage());
continue;
}
$this->info($file->getFilename());
} }
} }
+114
View File
@@ -0,0 +1,114 @@
<?php
namespace Lucent\Commands;
use Illuminate\Console\Command;
use Illuminate\Database\Schema\Blueprint;
use Lucent\Database\Database;
class SetupDatabase extends Command
{
protected $signature = 'lucent:setup-db';
protected $description = 'Run to setup a new database';
public function handle()
{
$dbConnection = config("lucent.database");
$databasePath = config("database.connections.$dbConnection.database");
if(file_exists($databasePath)){
$this->error("Database already exists.");
return 0;
}
touch($databasePath);
$this->tableUsers();
$this->tableRecords();
$this->tableRevisions();
$this->tableSessions();
$this->tableCommandLogs();
$this->info("Lucent Database Setup Completed");
}
private function tableUsers(): void
{
Database::make()->getSchemaBuilder()->create('users', function (Blueprint $table) {
$table->uuid("id")->primary();
$table->string('name')->nullable();
$table->string('email')->unique();
$table->jsonb('roles');
$table->string('createdAt');
$table->string('updatedAt');
$table->string('loggedInAt');
$table->string('mailToken')->nullable();
});
}
private function tableSessions(): void
{
Database::make()->getSchemaBuilder()->create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
private function tableRecords(): void
{
Database::make()->getSchemaBuilder()->create('records', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('schema');
$table->string('status');
$table->jsonb('data');
$table->jsonb('_sys');
$table->jsonb('_file');
$table->text('search')->default("");
$table->index(['schema', '_sys->updatedAt', 'status']);
$table->index('search');
});
Database::make()->getSchemaBuilder()->create('edges', function (Blueprint $table) {
$table->uuid('source');
$table->uuid('target');
$table->string('sourceSchema');
$table->string('targetSchema');
$table->string('field');
$table->string('rank');
$table->unique(['source', 'target', "field"]);
});
}
private function tableRevisions(): void
{
Database::make()->getSchemaBuilder()->create('revisions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->uuid('recordId');
$table->string('schema');
$table->jsonb('data');
$table->jsonb('_sys');
$table->jsonb('_file');
$table->jsonb('_edges');
});
}
private function tableCommandLogs(): void
{
Database::make()->getSchemaBuilder()->create('command_logs', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('signature');
$table->integer('pid')->nullable();
$table->text('logs');
});
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Lucent\Commands;
use Illuminate\Console\Command;
use Lucent\Database\Database;
class UpgradeFiles122 extends Command
{
protected $signature = 'lucent:upgrade:files_1_2_2 {schema} {disk}';
protected $description = 'Upgrade to the new filesystem';
public function handle()
{
$schema = $this->argument('schema');
$disk = $this->argument('disk');
$db = Database::make();
$records = $db->table("records")->where("schema", $schema)->get();
foreach ($records as $record) {
$array = json_decode($record->_file, true);
$array["disk"] = $disk;
$db->table("records")->where("id", $record->id)->update(["_file" => json_encode($array)]);
}
}
}
+40 -6
View File
@@ -1,15 +1,49 @@
<?php <?php
return [ return [
"env" => env("LUCENT_ENV", "production"), "env" => env("LUCENT_ENV", "production"),
"schemas_path" => env("LUCENT_SCHEMAS_PATH", "app/Lucent"), "schemas_path" => env("LUCENT_SCHEMAS_PATH", "resources/lucent/schemas"),
"database" => env('LUCENT_DB_CONNECTION', env('DB_CONNECTION',"sqlite")), "database" => env('LUCENT_DB_CONNECTION', env('DB_CONNECTION', "sqlite")),
"name" => env("LUCENT_NAME", "Lucent"), "name" => env("LUCENT_NAME", "Lucent"),
"url" => env("LUCENT_URL", env('APP_URL')), "url" => env("LUCENT_URL", env('APP_URL')),
"previewTarget" => env("LUCENT_PREVIEW_TARGET", "previewTarget"), "previewTarget" => env("LUCENT_PREVIEW_TARGET", "previewTarget"),
"generateCommand" => env("LUCENT_GENERATE_COMMAND", "generate:static"), /*
* Make available laravel artisan commands for admin users
* example:
* [
* "command1:signature" => "Description 1"
* "command2:signature" => "Description 2"
* ]
*
* */
"commands" => [],
/*
* Image filter will be available both for rich editor fields
* and throughout your application
*
* example:
* [
* "filterName" => Filter::class
* ]
*
* */
"imageFilters" => [], "imageFilters" => [],
"canInvite" => ["admin"], "canInvite" => ["admin"],
"canBuild" => ["admin"], "canBuild" => ["admin"],
"systemUserId" => "", "systemUserId" => "",
"schemaFields" => [
\Lucent\Schema\Ui\Checkbox::class,
\Lucent\Schema\Ui\Color::class,
\Lucent\Schema\Ui\Date::class,
\Lucent\Schema\Ui\Datetime::class,
\Lucent\Schema\Ui\File::class,
\Lucent\Schema\Ui\Json::class,
\Lucent\Schema\Ui\Markdown::class,
\Lucent\Schema\Ui\Number::class,
\Lucent\Schema\Ui\Reference::class,
\Lucent\Schema\Ui\Rich::class,
\Lucent\Schema\Ui\Slug::class,
\Lucent\Schema\Ui\Text::class,
\Lucent\Schema\Ui\Textarea::class
]
]; ];
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Lucent\Database;
use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB;
class Database
{
public static function make(): Connection{
$dbConnection = config("lucent.database");
return DB::connection($dbConnection);
}
}
@@ -1,37 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->uuid("id")->primary();
$table->string('name')->nullable();
$table->string('email')->unique();
$table->jsonb('roles');
$table->string('createdAt');
$table->string('updatedAt');
$table->string('loggedInAt');
$table->string('mailToken')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
};
@@ -1,35 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('sessions');
}
};
@@ -1,48 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('records', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('schema');
$table->string('status');
$table->jsonb('data');
$table->jsonb('_sys');
$table->jsonb('_file');
$table->index(['schema', 'status']);
});
Schema::create('edges', function (Blueprint $table) {
$table->uuid('source');
$table->uuid('target');
$table->string('sourceSchema');
$table->string('targetSchema');
$table->string('field');
$table->string('rank');
$table->unique(['source', 'target', "field"]);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('records');
Schema::dropIfExists('edges');
}
};
@@ -1,35 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('revisions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->uuid('recordId');
$table->string('schema');
$table->jsonb('data');
$table->jsonb('_sys');
$table->jsonb('_file');
$table->jsonb('_edges');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('revisions');
}
};
@@ -1,36 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('records', function (Blueprint $table) {
$table->text('search')->default("");
$table->index('search');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('records', function (Blueprint $table) {
$table->dropColumn('search');
$table->dropIndex('search');
});
}
};
@@ -1,34 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('records', function (Blueprint $table) {
$table->dropIndex(['schema', 'status']);
$table->index(['schema', '_sys->updatedAt','status']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('records', function (Blueprint $table) {
$table->dropIndex(['schema', '_sys->updatedAt','status']);
$table->index(['schema', 'status']);
});
}
};
+9 -9
View File
@@ -1,6 +1,6 @@
<?php namespace Lucent\Edge; <?php namespace Lucent\Edge;
use Illuminate\Support\Facades\DB; use Lucent\Database\Database;
use Lucent\LucentException; use Lucent\LucentException;
use PDOException; use PDOException;
use stdClass; use stdClass;
@@ -15,7 +15,7 @@ class EdgeRepo
public function insert(Edge $edge): void public function insert(Edge $edge): void
{ {
try { try {
DB::table("edges")->insert($edge->toDB()); Database::make()->table("edges")->insert($edge->toDB());
} catch (PDOException $e) { } catch (PDOException $e) {
if ($e->getCode() == 23505) { if ($e->getCode() == 23505) {
throw new LucentException("Edge already exists"); throw new LucentException("Edge already exists");
@@ -34,7 +34,7 @@ class EdgeRepo
{ {
$edgesDB = collect($edges)->map(fn($e) => $e->toDB())->toArray(); $edgesDB = collect($edges)->map(fn($e) => $e->toDB())->toArray();
try { try {
DB::table("edges")->insert($edgesDB); Database::make()->table("edges")->insert($edgesDB);
} catch (PDOException $e) { } catch (PDOException $e) {
if ($e->getCode() == 23505) { if ($e->getCode() == 23505) {
throw new LucentException("Edge already exists"); throw new LucentException("Edge already exists");
@@ -52,8 +52,8 @@ class EdgeRepo
public function replaceForRecord(string $from, array $edges): void public function replaceForRecord(string $from, array $edges): void
{ {
$edgesDB = collect($edges)->map(fn($e) => $e->toDB())->toArray(); $edgesDB = collect($edges)->map(fn($e) => $e->toDB())->toArray();
DB::table("edges")->where("source", $from)->delete(); Database::make()->table("edges")->where("source", $from)->delete();
DB::table("edges")->insert($edgesDB); Database::make()->table("edges")->insert($edgesDB);
} }
@@ -62,13 +62,13 @@ class EdgeRepo
*/ */
public function findAll(): array public function findAll(): array
{ {
$edges = DB::table("edges")->get(); $edges = Database::make()->table("edges")->get();
return $edges->map([$this, 'mapEdge'])->toArray(); return $edges->map([$this, 'mapEdge'])->toArray();
} }
public function findForSource(string $recordId): array public function findForSource(string $recordId): array
{ {
$edges = DB::table("edges")->where("source", $recordId)->get(); $edges = Database::make()->table("edges")->where("source", $recordId)->get();
return $edges->map([$this, 'mapEdge'])->toArray(); return $edges->map([$this, 'mapEdge'])->toArray();
} }
@@ -89,7 +89,7 @@ class EdgeRepo
public function remove(Edge $edge): void public function remove(Edge $edge): void
{ {
DB::table("edges") Database::make()->table("edges")
->where("source", $edge->source) ->where("source", $edge->source)
->where("target", $edge->target) ->where("target", $edge->target)
->where("sourceSchema", $edge->sourceSchema) ->where("sourceSchema", $edge->sourceSchema)
@@ -100,7 +100,7 @@ class EdgeRepo
public function findLastEdgeRank(string $source, string $field): string public function findLastEdgeRank(string $source, string $field): string
{ {
$data = DB::table("edges") $data = Database::make()->table("edges")
->where("source", $source) ->where("source", $source)
->where("field", $field) ->where("field", $field)
->orderBy("rank", "desc") ->orderBy("rank", "desc")
+27 -44
View File
@@ -2,14 +2,14 @@
namespace Lucent\File; namespace Lucent\File;
use Exception;
use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB; use Illuminate\Log\Logger;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Intervention\Image\ImageManagerStatic; use Intervention\Image\ImageManager;
use Lucent\Channel\ChannelService; use Lucent\Channel\ChannelService;
use Lucent\Database\Database;
use Lucent\LucentException; use Lucent\LucentException;
use Lucent\Record\FileData as RecordFile; use Lucent\Record\FileData as RecordFile;
use Lucent\Record\QueryRecord; use Lucent\Record\QueryRecord;
@@ -21,14 +21,16 @@ class FileService
{ {
public function __construct( public function __construct(
public ChannelService $channelService public ChannelService $channelService,
public ImageManager $imageManager,
public Logger $logger
) )
{ {
} }
public function getPath(QueryRecord $file): string public function getPath(QueryRecord $file): string
{ {
return $this->channelService->channel->url . "/storage/" . $file->_file->path; return $this->channelService->channel->filesUrl . "/" . $file->_file->path;
} }
public function createFromUrl(FilesSchema $schema, string $url): FileUploadResult public function createFromUrl(FilesSchema $schema, string $url): FileUploadResult
@@ -65,8 +67,7 @@ class FileService
isDuplicate: true isDuplicate: true
); );
} }
$disk = $this->loadDisk($schema);
$disk = $this->loadDisk();
$path = $schema->path . "/" . $filename; $path = $schema->path . "/" . $filename;
$res = $disk->put( $res = $disk->put(
$path, $path,
@@ -78,13 +79,17 @@ class FileService
throw new LucentException("File $filename not uploaded"); throw new LucentException("File $filename not uploaded");
} }
$this->createThumbnail($disk, $schema->path, $filename, $file); if($this->isImage($mimetype)){
$this->createTemplates($disk, $path);
}
list($width, $height) = $this->isImage($mimetype) ? getimagesize($file) : [0, 0]; list($width, $height) = $this->isImage($mimetype) ? getimagesize($file) : [0, 0];
$recordFile = new RecordFile( $recordFile = new RecordFile(
originalName: $originalFilename, originalName: $originalFilename,
mime: $mimetype, mime: $mimetype,
path: $path, path: $path,
disk: $schema->disk,
size: $file->getSize(), size: $file->getSize(),
width: $width, width: $width,
height: $height, height: $height,
@@ -109,27 +114,16 @@ class FileService
return in_array($mimetype, $imageMimes); return in_array($mimetype, $imageMimes);
} }
public function loadDisk(): Filesystem public function loadDisk(Schema|string $schema): Filesystem
{ {
return Storage::build([ return Storage::disk($schema->disk ?? $schema);
'driver' => 'local',
// 'key' => config("filesystems.disks.s3.key"),
// 'secret' => config("filesystems.disks.s3.secret"),
// 'region' => config("filesystems.disks.s3.region"),
// 'bucket' => config("filesystems.disks.s3.bucket"),
// // 'url' => $schema->objectStorageUrl,
// 'endpoint' => $schema->objectStorageEndpoint,
'use_path_style_endpoint' => false,
'visibility' => 'public', // now managed by aws policy
'root' => storage_path('app/public'),
'throw' => true,
]);
} }
private function checkDuplicate(string $schemaName, string $checksum, int $filesize): string private function checkDuplicate(string $schemaName, string $checksum, int $filesize): string
{ {
$record = DB::table("records") $record = Database::make()->table("records")
->where("schema", $schemaName) ->where("schema", $schemaName)
->where("_file->checksum", $checksum) ->where("_file->checksum", $checksum)
->where("_file->size", $filesize) ->where("_file->size", $filesize)
@@ -138,30 +132,19 @@ class FileService
return $record->id ?? ""; return $record->id ?? "";
} }
private function createThumbnail(Filesystem $disk, string $schemaPath, string $filename, UploadedFile $file): void public function createTemplates(Filesystem $disk, string $path): void
{ {
$originalImage = $this->imageManager->make($disk->get($path));
foreach (config("lucent.imageFilters") as $preset => $filterClass) {
$thumbDir = storage_path("app/public/thumbs/" . $schemaPath . "/"); $imageClone = clone $originalImage;
if (!file_exists($thumbDir)) { $image = $imageClone->filter(new $filterClass);
make_dir_r($thumbDir); $templateUri = "/templates/" . $preset . "/" . $path;
$disk->put($templateUri, $image->encode('webp', 75));
} }
try { $thumbDir = "thumbs/" . $path;
ImageManagerStatic::configure(['driver' => 'imagick']);
$image = ImageManagerStatic::make($file);
} catch (Exception $e) {
logger($e->getMessage());
return;
}
$image->fit(300, 300); $image = $originalImage->fit(300, 300);
try { $disk->put($thumbDir, $image->encode('webp', 75));
$image->encode('webp', 75)->save($thumbDir . $filename);
} catch (Exception $e) {
logger($e->getMessage());
}
} }
} }
-89
View File
@@ -1,89 +0,0 @@
<?php
namespace Lucent\File;
use Exception;
use Illuminate\Log\Logger;
use Intervention\Image\ImageManager;
use Lucent\Channel\ChannelService;
use Lucent\Record\QueryRecord;
class ImageService
{
private string $notFoundImage = "/not-found.jpg";
public function __construct(
public ImageManager $imageManager,
public ChannelService $channelService,
public Logger $logger
)
{
}
public function file(?QueryRecord $record, string $template = ""): string
{
if (empty($record)) {
return $this->notFoundImage;
}
$originalPath = $record->_file->path;
$templateUri = $this->findTemplate($originalPath, $template);
if ($templateUri === false) {
$templateUri = $this->createTemplate($originalPath, $template);
}
return $this->channelService->channel->filesUrl . "/" . $templateUri;
}
private function findTemplate(string $originalPath, string $template): string|false
{
$templateUri = "templates/" . $template . "/" . $originalPath;
$templateFilePath = public_path("storage/" . $templateUri);
if (file_exists($templateFilePath)) {
return $templateUri;
}
return false;
}
private function createTemplate(string $originalPath, string $template): string
{
$originalFilePath = public_path("storage/" . $originalPath);
$templateUri = "/templates/" . $template . "/" . $originalPath;
$templateFilePath = public_path("storage/" . $templateUri);
if (!file_exists($originalFilePath)) {
return $this->notFoundImage;
}
if (!file_exists(pathinfo($templateFilePath, PATHINFO_DIRNAME))) {
$this->make_dir(pathinfo($templateFilePath, PATHINFO_DIRNAME));
}
try {
$image = $this->imageManager->make($originalFilePath);
} catch (Exception $e) {
$this->logger->error($e->getMessage());
return $this->notFoundImage;
}
$image = $image->filter(new $this->channelService->channel->imageFilters[$template]);
try {
$image = $this->imageManager->make((string)$image->encode('webp', 75));
$image->save($templateFilePath);
} catch (Exception $e) {
$this->logger->error($e->getMessage());
return $this->notFoundImage;
}
return $templateUri;
}
private function make_dir(string $path): void
{
is_dir($path) || mkdir($path, 0777, true);
}
}
+25 -28
View File
@@ -5,64 +5,62 @@ namespace Lucent\Http\Controller;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Artisan;
use Lucent\Channel\ChannelService; use Lucent\Channel\ChannelService;
use Lucent\Command\CommandRepo;
use Lucent\Command\CommandService;
use Lucent\Svelte\Svelte; use Lucent\Svelte\Svelte;
use function Lucent\Response\ok;
class BuildController extends Controller class BuildController extends Controller
{ {
public function __construct( public function __construct(
public readonly Svelte $svelte, private readonly Svelte $svelte,
public readonly ChannelService $channelService, private readonly ChannelService $channelService,
private readonly CommandService $commandService,
private readonly CommandRepo $commandRepo,
) )
{ {
} }
public function build() public function build(Request $request)
{ {
$commandSignature = $request->route("signature");
$this->commandService->run($commandSignature);
$buildLogFile = storage_path("lucent/build.log");
if(file_exists($buildLogFile)){
unlink($buildLogFile);
}
exec("cd " . base_path() . " && php8.2 artisan {$this->channelService->channel->generateCommand} > " . $buildLogFile . " 2>&1 & echo $!", $op);
$pid = (int)$op[0];
return redirect($this->channelService->channel->lucentUrl . "/build-report");
} }
public function report(): View public function report(Request $request): View
{ {
$commandSignature = $request->route("signature");
$command = $this->channelService->channel->commands->firstWhere("signature", $commandSignature);
return $this->svelte->render( return $this->svelte->render(
layout: "channel", layout: "channel",
view: "buildReport", view: "buildReport",
title: "Build Report", title: $command->name,
data: [
"command" => $command,
]
); );
} }
public function reportSource() public function reportSource(Request $request)
{ {
return response()->stream(function () { $commandSignature = $request->route("signature");
return response()->stream(function () use ($commandSignature) {
while (true) { while (true) {
// sleep(1); // 50ms
$commandLogItem = $this->commandRepo->findBySignature($commandSignature);
$data["date"] = date("Y-m-d H:i:s"); $data["date"] = date("Y-m-d H:i:s");
$data["logs"] = file_get_contents(storage_path("lucent/build.log")); $data["logs"] = $commandLogItem->logs ?? "";
// $lines = explode("\n",$data["logs"]); // $lines = explode("\n",$data["logs"]);
echo 'data: ' .json_encode($data); echo 'data: ' . json_encode($data);
echo "\n\n"; echo "\n\n";
ob_flush(); ob_flush();
flush(); flush();
logger($this->commandService->commandIsRunning($commandLogItem->pid));
if(str_contains($data["logs"],"Finito")){ if (!$this->commandService->commandIsRunning($commandLogItem->pid)) {
break;
}
if(str_contains($data["logs"],"Exception")){
break; break;
} }
@@ -79,5 +77,4 @@ class BuildController extends Controller
]); ]);
} }
} }
+16 -1
View File
@@ -7,6 +7,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Lucent\Channel\ChannelService; use Lucent\Channel\ChannelService;
use Lucent\File\FileService; use Lucent\File\FileService;
use Lucent\File\ImageService;
use Lucent\Query\Query; use Lucent\Query\Query;
use Lucent\Record\InputData\RecordInputData; use Lucent\Record\InputData\RecordInputData;
use Lucent\Record\RecordService; use Lucent\Record\RecordService;
@@ -27,6 +28,21 @@ class FileController extends Controller
{ {
} }
public function fromDisk(Request $request, string $disk)
{
$imagePath = $request->route("any");
$disk = $this->fileService->loadDisk($disk);
return response()->file($disk->path($imagePath));
}
public function thumb(Request $request, string $disk)
{
$imagePath = "thumbs/".$request->route("any");
$disk = $this->fileService->loadDisk($disk);
return response()->file($disk->path($imagePath));
}
public function download(Request $request) public function download(Request $request)
{ {
$disk = $this->fileService->loadDisk(); $disk = $this->fileService->loadDisk();
@@ -39,7 +55,6 @@ class FileController extends Controller
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
'files.*' => 'required|file|max:100000', 'files.*' => 'required|file|max:100000',
]); ]);
if ($validator->fails()) { if ($validator->fails()) {
return fail($validator->errors()->first()); return fail($validator->errors()->first());
} }
+49 -14
View File
@@ -8,7 +8,7 @@ use Lucent\Account\AccountService;
use Lucent\Account\AuthService; use Lucent\Account\AuthService;
use Lucent\Channel\ChannelService; use Lucent\Channel\ChannelService;
use Lucent\LucentException; use Lucent\LucentException;
use Lucent\Query\Operator; use Lucent\Query\Operator\OperatorRegistry;
use Lucent\Query\Query; use Lucent\Query\Query;
use Lucent\Record\InputData\EdgeInputData; use Lucent\Record\InputData\EdgeInputData;
use Lucent\Record\InputData\RecordInputData; use Lucent\Record\InputData\RecordInputData;
@@ -17,21 +17,24 @@ use Lucent\Record\QueryRecord;
use Lucent\Record\RecordService; use Lucent\Record\RecordService;
use Lucent\Record\Status; use Lucent\Record\Status;
use Lucent\Schema\System; use Lucent\Schema\System;
use Lucent\Schema\Ui\Reference;
use Lucent\Schema\Validator\ValidatorException; use Lucent\Schema\Validator\ValidatorException;
use Lucent\Svelte\Svelte; use Lucent\Svelte\Svelte;
use Lucent\ViewModel\ViewModel;
use function Lucent\Response\fail; use function Lucent\Response\fail;
use function Lucent\Response\ok; use function Lucent\Response\ok;
class RecordController extends Controller class RecordController extends Controller
{ {
public function __construct( public function __construct(
private readonly RecordService $recordService, private readonly RecordService $recordService,
private readonly AccountService $accountService, private readonly AccountService $accountService,
private readonly AuthService $authService, private readonly ChannelService $channelService,
private readonly ChannelService $channelService, private readonly Svelte $svelte,
private readonly Svelte $svelte, private readonly Query $query,
private readonly Query $query, private readonly Manager $recordManager,
private readonly Manager $recordManager private readonly OperatorRegistry $operatorRegistry,
private readonly ViewModel $viewModel,
) )
{ {
} }
@@ -60,10 +63,12 @@ class RecordController extends Controller
], $filter); ], $filter);
$skip = data_get($urlParams, "skip") ?? 0; $skip = data_get($urlParams, "skip") ?? 0;
$limit = 30; $limit = 30;
$records = []; $records = [];
$graphArray = null; $graphArray = null;
$graph = $this->query $graph = $this->query
->filter($arguments) ->filter($arguments)
->notLinked($request->input("notlinked") ?? "") ->notLinked($request->input("notlinked") ?? "")
@@ -79,7 +84,6 @@ class RecordController extends Controller
$records = $graph->getRootRecords()->toArray(); $records = $graph->getRootRecords()->toArray();
$data = [ $data = [
"schemas" => $this->channelService->channel->schemas, "schemas" => $this->channelService->channel->schemas,
"schema" => $schema, "schema" => $schema,
@@ -87,7 +91,7 @@ class RecordController extends Controller
"records" => $records, "records" => $records,
"graph" => toArray($graph), "graph" => toArray($graph),
"systemFields" => array_values(System::list()), "systemFields" => array_values(System::list()),
"operators" => array_values(Operator::list()), "operators" => $this->operatorRegistry->all(),
"sortParam" => $sort, "sortParam" => $sort,
"sortField" => $schema->fields->merge(array_values(System::list()))->firstWhere(fn($field) => $field->name === $sort || "-" . $field->name === $sort || "data." . $field->name === $sort || "-data." . $field->name === $sort), "sortField" => $schema->fields->merge(array_values(System::list()))->firstWhere(fn($field) => $field->name === $sort || "-" . $field->name === $sort || "data." . $field->name === $sort || "-data." . $field->name === $sort),
"limit" => $limit, "limit" => $limit,
@@ -133,18 +137,22 @@ class RecordController extends Controller
->filter($arguments) ->filter($arguments)
->limit(-1) ->limit(-1)
->status(explode(",", $arguments["status_in"])) ->status(explode(",", $arguments["status_in"]))
->childrenDepth(1)
// ->skip($skip) // ->skip($skip)
->sort($sort) ->sort($sort)
->run() ->run()
->records; ->tree();
header('Content-Type: application/csv'); header('Content-Type: application/csv');
header('Content-Disposition: attachment; filename="' . $schemaName . '.csv";'); header('Content-Disposition: attachment; filename="' . $schemaName . '.csv";');
$handle = fopen('php://output', 'w'); $handle = fopen('php://output', 'w');
$csvRow = ["id", ...array_keys($records[0]->data->toArray())]; $relationColumns = $this->makeCsvRelationColumns($schema);
$csvRow = ["id", ...array_keys($records[0]->data->toArray()),...$relationColumns];
fputcsv($handle, $csvRow, ','); fputcsv($handle, $csvRow, ',');
foreach ($records as $record) { foreach ($records as $record) {
$csvRow = [$record->id, ...$record->data->toArray()]; $csvRow = [$record->id, ...$record->data->toArray()];
$csvRow = array_merge($csvRow,$this->makeCsvRelationColumnValues($schema,$record->_children));
$csvRow = array_values($csvRow); $csvRow = array_values($csvRow);
fputcsv($handle, $csvRow, ','); fputcsv($handle, $csvRow, ',');
} }
@@ -153,6 +161,32 @@ class RecordController extends Controller
exit; exit;
} }
private function makeCsvRelationColumns($schema):array{
return $schema->fields->filter(fn($f) => get_class($f) === Reference::class)->reduce(function($c,$f){
$c[] = $f->name." id";
$c[] = $f->name." name";
return $c;
},[]);
}
private function makeCsvRelationColumnValues($schema, $children):array{
return $schema->fields->filter(fn($f) => get_class($f) === Reference::class)->reduce(function($c,$f) use($children){
$fieldRecords = data_get($children,$f->name);
if(empty($fieldRecords)){
$c[] = "";
$c[] = "";
}elseif (count($fieldRecords) === 1){
$c[] = data_get($fieldRecords,"0.id");
$c[] = $this->viewModel->getRecordName($fieldRecords[0]);
}else{
$c[] = collect($fieldRecords)->pluck("id")->join("::");
$c[] = collect($fieldRecords)->pluck("data.name")->join("::");
}
return $c;
},[]);
}
public function new(Request $request) public function new(Request $request)
{ {
if (!in_array($request->input("schema"), $this->accountService->currentWritableSchemas())) { if (!in_array($request->input("schema"), $this->accountService->currentWritableSchemas())) {
@@ -165,7 +199,7 @@ class RecordController extends Controller
$schema = $this->channelService->channel->schemas->where("name", $request->input("schema"))->first(); $schema = $this->channelService->channel->schemas->where("name", $request->input("schema"))->first();
$recordHistory = $this->recordManager->fromSession($request->session())->getRecords(); $recordHistory = $this->recordManager->fromSession($request->session())->getRecords();
$record = $this->recordService->createEmpty($schema, $this->authService->currentUserId()); $record = $this->recordService->createEmpty($schema);
$queryRecord = QueryRecord::fromRecord($record); $queryRecord = QueryRecord::fromRecord($record);
return $this->svelte->render( return $this->svelte->render(
layout: "channel", layout: "channel",
@@ -200,6 +234,7 @@ class RecordController extends Controller
"schema" => $schema, "schema" => $schema,
"record" => $queryRecord, "record" => $queryRecord,
"isCreateMode" => true, "isCreateMode" => true,
"isWritable" => in_array($record->schema, $this->accountService->currentWritableSchemas())
]; ];
} }
@@ -302,7 +337,7 @@ class RecordController extends Controller
id: $request->input("record.id"), id: $request->input("record.id"),
data: $request->input("record.data"), data: $request->input("record.data"),
status: Status::from($request->input("record.status")), status: Status::from($request->input("record.status")),
edges: array_map(EdgeInputData::fromArray(...), $request->input("edges") ?? []), edges: array_map(EdgeInputData::fromArray(...), $request->input("edges") ?? []),
); );
} }
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace Lucent\Http\Controller;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Lucent\Account\AccountService;
use Lucent\Channel\ChannelService;
use Lucent\LucentException;
use Lucent\Setup\Data\SetupStep;
use Lucent\Setup\Data\SetupStepStatus;
use Lucent\Setup\Setup;
use Lucent\Setup\Step\ComposerStep;
use Lucent\Setup\Step\DatabaseSetupStep;
use Lucent\Setup\Step\IStep;
use Lucent\Setup\Step\LaravelEnvStep;
use Lucent\Setup\Step\LucentConfigStep;
use Lucent\Setup\Step\StorageLinkSetupStep;
use Lucent\Setup\Step\StorageSetupStep;
use Lucent\Svelte\Svelte;
use function Lucent\Response\fail;
use function Lucent\Response\ok;
class SetupController
{
public function __construct(
private readonly AccountService $accountService,
private readonly ChannelService $channelService,
private readonly Svelte $svelte,
)
{
}
public function setup(Request $request): View|RedirectResponse
{
$steps = array_reduce([
new ComposerStep,
new LucentConfigStep,
new LaravelEnvStep,
new StorageSetupStep,
new StorageLinkSetupStep,
new DatabaseSetupStep,
], fn(array $carry, IStep $setupStep) => array_merge($carry, [$setupStep()]), []);
$allSuccess = array_reduce($steps, fn(bool $carry, SetupStep $step) => !$carry ? false : $step->status === SetupStepStatus::SUCCESS, true);
if($allSuccess){
if ($this->accountService->countUsers() > 0) {
return redirect($this->channelService->channel->lucentUrl . "/login");
}
}
return $this->svelte->render(
layout: "account",
view: "setup",
title: "Setup Lucent",
data: [
"steps" => $steps,
"allSuccess" => $allSuccess,
]
);
}
}
+11 -3
View File
@@ -10,6 +10,11 @@ use Lucent\Http\Controller\HomeController;
use Lucent\Http\Controller\MemberController; use Lucent\Http\Controller\MemberController;
use Lucent\Http\Controller\RecordController; use Lucent\Http\Controller\RecordController;
use Lucent\Http\Controller\RevisionController; use Lucent\Http\Controller\RevisionController;
use Lucent\Http\Controller\SetupController;
Route::get('/lucent/setup', [SetupController::class, 'setup']);
Route::get('/lfs-{disk}/{any}', [FileController::class, 'fromDisk'])->where('any', '.*');
Route::group([ Route::group([
@@ -17,8 +22,11 @@ Route::group([
'prefix' => "lucent" 'prefix' => "lucent"
], function () { ], function () {
Route::middleware(['lucent.guest'])->group(function () { Route::middleware(['lucent.guest'])->group(function () {
Route::get('/', [AuthController::class, 'login']); Route::get('/', [AuthController::class, 'login']);
Route::get('/register', [AuthController::class, 'register']); Route::get('/register', [AuthController::class, 'register']);
Route::post('/register', [AuthController::class, 'postRegister']); Route::post('/register', [AuthController::class, 'postRegister']);
Route::get('/login', [AuthController::class, 'login']); Route::get('/login', [AuthController::class, 'login']);
@@ -33,9 +41,9 @@ Route::group([
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', [AccountController::class, 'updateName']);
Route::post('/account/update-email', [AccountController::class, 'updateEmail']); Route::post('/account/update-email', [AccountController::class, 'updateEmail']);
Route::get('/build-report', [BuildController::class, 'report']); Route::get('/command-report/{signature}', [BuildController::class, 'report']);
Route::get('/build-report-source', [BuildController::class, 'reportSource']); Route::get('/command-report-source/{signature}', [BuildController::class, 'reportSource']);
Route::post('/build', [BuildController::class, 'build']); Route::post('/command/{signature}', [BuildController::class, 'build']);
}); });
-15
View File
@@ -1,15 +0,0 @@
<?php
namespace Lucent;
use Illuminate\Support\ServiceProvider;
use Lucent\Edge\Event\EdgesUpdated;
class LucentEventServiceProvider extends ServiceProvider
{
protected $listen = [
EdgesUpdated::class => [
SendEmailVerificationNotification::class,
],
];
}
+13 -11
View File
@@ -9,10 +9,13 @@ use Illuminate\Support\ServiceProvider;
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use Lucent\Channel\ChannelService; use Lucent\Channel\ChannelService;
use Lucent\Commands\CompileSchemas; use Lucent\Commands\CompileSchemas;
use Lucent\Commands\GenerateCollectionSchema;
use Lucent\Commands\GenerateFileSchema;
use Lucent\Commands\LiveLink; use Lucent\Commands\LiveLink;
use Lucent\Commands\RebuildThumbnails; use Lucent\Commands\RebuildThumbnails;
use Lucent\Commands\RemoveOrphanEdges; use Lucent\Commands\RemoveOrphanEdges;
use Lucent\Event\Dispatcher; use Lucent\Commands\SetupDatabase;
use Lucent\Commands\UpgradeFiles122;
use Lucent\File\FileService; use Lucent\File\FileService;
use Lucent\File\ImageService; use Lucent\File\ImageService;
use Lucent\Query\DatabaseGraph\DatabaseGraph; use Lucent\Query\DatabaseGraph\DatabaseGraph;
@@ -30,22 +33,20 @@ class LucentServiceProvider extends ServiceProvider
return ChannelService::fromConfig(); return ChannelService::fromConfig();
}); });
$this->app->singleton(Dispatcher::class, function () {
return new Dispatcher();
});
$this->app->bind(ImageManager::class, function () { $this->app->bind(ImageManager::class, function () {
return new ImageManager(['driver' => 'imagick']); return new ImageManager(['driver' => 'imagick']);
}); });
$this->app->bind(DatabaseGraph::class, function () { $this->app->bind(DatabaseGraph::class, function () {
return match (config("lucent.database")) { $dbConnection = config("lucent.database");
return match (config("database.connections.$dbConnection.driver")) {
"sqlite" => new SqliteDatabaseGraph(), "sqlite" => new SqliteDatabaseGraph(),
"pgsql" => new PgsqlDatabaseGraph(), "pgsql" => new PgsqlDatabaseGraph(),
}; };
}); });
$this->app->register(LucentEventServiceProvider::class);
} }
@@ -70,25 +71,26 @@ class LucentServiceProvider extends ServiceProvider
$this->loadRoutesFrom(__DIR__ . '/Http/web.php'); $this->loadRoutesFrom(__DIR__ . '/Http/web.php');
$this->loadRoutesFrom(__DIR__ . '/Http/api.php'); $this->loadRoutesFrom(__DIR__ . '/Http/api.php');
$this->loadMigrationsFrom(__DIR__ . '/Database/migrations');
if ($this->app->runningInConsole()) { if ($this->app->runningInConsole()) {
$this->commands([ $this->commands([
CompileSchemas::class, CompileSchemas::class,
RebuildThumbnails::class, RebuildThumbnails::class,
LiveLink::class, LiveLink::class,
RemoveOrphanEdges::class, RemoveOrphanEdges::class,
SetupDatabase::class,
GenerateCollectionSchema::class,
GenerateFileSchema::class,
UpgradeFiles122::class,
]); ]);
} }
View::share('manifest', $manifest); View::share('manifest', $manifest);
View::share('image', app()->make(ImageService::class));
View::share('file', app()->make(FileService::class)); View::share('file', app()->make(FileService::class));
Blade::anonymousComponentPath(__DIR__ . '../front/views/components', "lucent"); Blade::anonymousComponentPath(__DIR__ . '../front/views/components', "lucent");
$this->publishes([ $this->publishes([
__DIR__ . '/Config/main.php' => config_path('lucent.php'), __DIR__ . '/Config/main.php' => config_path('lucent.php'),
]); ],"lucent-config");
$this->publishes([ $this->publishes([
__DIR__ . '/../front/dist' => public_path('vendor/lucent/dist'), __DIR__ . '/../front/dist' => public_path('vendor/lucent/dist'),
@@ -0,0 +1,13 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Lucent\Query\Filter\Argument;
class BuilderConverter
{
public function for(Argument $argument): IBuilderConverter
{
return new ($argument->operator->converter)($argument);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class Equals implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->where($this->argument->field, $this->formatValue());
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->orWhere($this->argument->field, $this->formatValue());
}
private function formatValue(): string
{
return trim($this->argument->value);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class EqualsFalse implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->where($this->argument->field, false);
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->orWhere($this->argument->field, false);
}
}
@@ -0,0 +1,31 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class EqualsNumber implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->where($this->argument->field, $this->formatValue());
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->orWhere($this->argument->field, $this->formatValue());
}
private function formatValue(): int|float
{
$value = trim($this->argument->value);
return str_contains($value, ".") ? floatval($value) : intval($value);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class EqualsTrue implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->where($this->argument->field, true);
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->orWhere($this->argument->field, true);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class Exists implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->whereNot($this->argument->field, "")->whereNotNull($this->argument->field);
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->whereNot($this->argument->field, "")->whereNotNull($this->argument->field);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class Filter implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->whereJsonContains($this->argument->field, [$this->argument->value]);
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->orWhereJsonContains($this->argument->field, [$this->argument->value]);
}
}
@@ -0,0 +1,35 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class GreaterThan implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->where($this->argument->field, ">", $this->formatValue());
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->orWhere($this->argument->field, ">", $this->formatValue());
}
private function formatValue(): int|float|string
{
$value = trim($this->argument->value);
if (is_numeric($value)) {
return str_contains($value, ".") ? floatval($value) : intval($value);
}
return $value;
}
}
@@ -0,0 +1,35 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class GreaterThanEquals implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->where($this->argument->field, ">=", $this->formatValue());
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->orWhere($this->argument->field, ">=", $this->formatValue());
}
private function formatValue(): int|float|string
{
$value = trim($this->argument->value);
if (is_numeric($value)) {
return str_contains($value, ".") ? floatval($value) : intval($value);
}
return $value;
}
}
@@ -0,0 +1,13 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
interface IBuilderConverter
{
public function toAndQueryBuilder(Builder $builder): Builder;
public function toOrQueryBuilder(Builder $builder): Builder;
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class In implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->whereIn($this->argument->field, $this->formatValue());
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->orWhereIn($this->argument->field, $this->formatValue());
}
private function formatValue(): array
{
$value = $this->argument->value;
if (is_string($value)) {
$value = explode(",", $value);
}
return array_map(fn($v) => trim($v), $value);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class InNum implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->whereIn($this->argument->field, $this->formatValue());
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->orWhereIn($this->argument->field, $this->formatValue());
}
private function formatValue(): array
{
$value = $this->argument->value;
if (is_string($value)) {
$value = explode(",", $value);
}
return array_map(fn($v) => $this->formatNumber($v), $value);
}
private function formatNumber(string $value): float|int
{
$value = trim($value);
return str_contains($value, ".") ? floatval($value) : intval($value);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class IsNull implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->whereNull($this->argument->field);
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->orWhereNull($this->argument->field);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class LessThan implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->where($this->argument->field, "<", $this->formatValue());
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->orWhere($this->argument->field, "<", $this->formatValue());
}
private function formatValue(): int|float|string
{
$value = trim($this->argument->value);
if (is_numeric($value)) {
return str_contains($value, ".") ? floatval($value) : intval($value);
}
return $value;
}
}
@@ -0,0 +1,35 @@
<?php
namespace Lucent\Query\BuilderConverter;
use Illuminate\Database\Query\Builder;
use Lucent\Query\Filter\Argument;
readonly class LessThanEquals implements IBuilderConverter
{
public function __construct(private Argument $argument)
{
}
public function toAndQueryBuilder(Builder $builder): Builder
{
return $builder->where($this->argument->field, "<=", $this->formatValue());
}
public function toOrQueryBuilder(Builder $builder): Builder
{
return $builder->orWhere($this->argument->field, "<=", $this->formatValue());
}
private function formatValue(): int|float|string
{
$value = trim($this->argument->value);
if (is_numeric($value)) {
return str_contains($value, ".") ? floatval($value) : intval($value);
}
return $value;
}
}

Some files were not shown because too many files have changed in this diff Show More