Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 843f560710 | |||
| 7574d67d80 | |||
| 19931cb4d1 | |||
| 6458c1e71d | |||
| 63232585ab | |||
| 6d15591601 | |||
| 32c8378020 | |||
| d0cd8228cc | |||
| c45a3847f8 | |||
| c0b3878674 | |||
| f868219981 | |||
| 8ac0567e66 | |||
| 02f8f5970a | |||
| 0cd4e08716 | |||
| cf3d621587 | |||
| 6fc0a65b6f | |||
| a73ee21568 | |||
| ff54bcc2ef | |||
| ab1517cc8f | |||
| 9f724a3243 | |||
| ae65ca47f6 | |||
| 74d2fcc4fa | |||
| 82174afdea | |||
| ffc39f078d | |||
| 7c4e19afbc | |||
| 7b10bfca1d | |||
| 0e5ac08641 | |||
| 1505aaa909 | |||
| d9e2c4954a | |||
| 97ad9de3d2 | |||
| 9e140be0ec | |||
| a737c2d571 | |||
| c43c29eb14 | |||
| 0c00f76657 |
@@ -9,8 +9,8 @@ include_toc: true
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 8.2
|
||||
- Laravel 10
|
||||
- PHP 8.3
|
||||
- Laravel 11
|
||||
- Postgres or Sqlite database
|
||||
- ImageMagick
|
||||
|
||||
@@ -82,7 +82,9 @@ return [
|
||||
### Database
|
||||
|
||||
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:
|
||||
|
||||
@@ -90,6 +92,26 @@ Then run:
|
||||
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
|
||||
|
||||
To create your first user, head to your localhost:8000/lucent
|
||||
|
||||
+32
@@ -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,
|
||||
],
|
||||
```
|
||||
+3
-3
@@ -8,14 +8,14 @@
|
||||
"ext-zip": "*",
|
||||
"ext-sqlite3": "*",
|
||||
"ext-imagick": "*",
|
||||
"ext-pdo": "*",
|
||||
"php": "^8.3",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"intervention/image": "^2.7",
|
||||
"phpoption/phpoption": "^1.9",
|
||||
"spatie/image-optimizer": "^1.6",
|
||||
"staudenmeir/laravel-cte": "^1.0",
|
||||
"ext-pdo": "*",
|
||||
"mustache/mustache": "^2.14"
|
||||
"staudenmeir/laravel-cte": "^1.0"
|
||||
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.8",
|
||||
|
||||
+5
-11
@@ -24,7 +24,8 @@ There are 3 types of schemas
|
||||
- **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_
|
||||
- **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_
|
||||
- **read**: Array of user groups that have read 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
|
||||
- **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_
|
||||
- **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_
|
||||
- **read**: Array of user groups that have read 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:
|
||||
|
||||
```json
|
||||
@@ -74,7 +67,8 @@ A full Collection example without the fields:
|
||||
"SEO"
|
||||
],
|
||||
"sortBy": "-_sys.createdAt",
|
||||
"titleTemplate": "{{name}} {{slug}}",
|
||||
"schemaTitle": "{{name}} {{slug}}",
|
||||
"schemaImage": "cover",
|
||||
"revisions": 15,
|
||||
"read": [
|
||||
"admin",
|
||||
|
||||
Vendored
-198
File diff suppressed because one or more lines are too long
Vendored
+342
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"main.js": {
|
||||
"file": "assets/main-B-nfEWyS.js",
|
||||
"file": "assets/main-BJyanQ7P.js",
|
||||
"name": "main",
|
||||
"src": "main.js",
|
||||
"isEntry": true,
|
||||
"css": [
|
||||
"assets/main-CaexgiEy.css"
|
||||
"assets/main-Dk7njt4m.css"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import Login from "./account/Login.svelte";
|
||||
import Verify from "./account/Verify.svelte";
|
||||
import Profile from "./account/Profile.svelte";
|
||||
import SetupIndex from "./setup/Index.svelte";
|
||||
import {setContext} from "svelte";
|
||||
|
||||
const components = {
|
||||
@@ -10,6 +11,7 @@
|
||||
login: Login,
|
||||
verify: Verify,
|
||||
profile: Profile,
|
||||
setup: SetupIndex,
|
||||
};
|
||||
|
||||
export let title;
|
||||
@@ -22,7 +24,7 @@
|
||||
setContext("user", user);
|
||||
</script>
|
||||
<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>
|
||||
<svelte:component this={components[view]} {title} {...data}/>
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
<script>
|
||||
import {getContext, onMount} from "svelte";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let title;
|
||||
export let command;
|
||||
$: date = "";
|
||||
$: logs = "";
|
||||
|
||||
let anchorEl;
|
||||
let inProgress = false;
|
||||
|
||||
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) {
|
||||
inProgress = true;
|
||||
const data = JSON.parse(event.data);
|
||||
date = data.date;
|
||||
logs = data.logs;
|
||||
|
||||
anchorEl.scrollIntoView()
|
||||
}
|
||||
eventSource.onerror = (e) => {
|
||||
console.log(e)
|
||||
@@ -28,8 +31,7 @@
|
||||
function buildWebsite(e) {
|
||||
e.preventDefault();
|
||||
inProgress = true;
|
||||
|
||||
axios.post(channel.lucentUrl + "/build").then(response => {
|
||||
axios.post(channel.lucentUrl + "/command/" + command.signature).then(response => {
|
||||
connect()
|
||||
})
|
||||
|
||||
@@ -46,26 +48,36 @@
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
{#if inProgress}
|
||||
<span class="badge text-bg-warning">
|
||||
Build in progress
|
||||
Action in progress
|
||||
</span>
|
||||
{/if}
|
||||
{#if !inProgress && logs}
|
||||
<span class="badge text-bg-info">
|
||||
Build completed
|
||||
Action completed
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<pre>{logs}</pre>
|
||||
<pre class="logs">{logs}
|
||||
<div bind:this={anchorEl}> </div>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.logs{
|
||||
max-height: 70vh;
|
||||
overflow: scroll;
|
||||
background: var(--p90);
|
||||
color: var(--p10);
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -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"/>',
|
||||
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 height = 16;
|
||||
export let icon = "";
|
||||
|
||||
@@ -83,7 +83,11 @@
|
||||
{#if record._file?.path}
|
||||
<div class="file-table-row">
|
||||
<Preview record={record} size={record._file?.width > 0 ? "medium" : "small"}/>
|
||||
|
||||
<div>
|
||||
{#if record.status === "draft"}
|
||||
<span style="text-transform: uppercase;font-size:10px">{record.status}</span>
|
||||
{/if}
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
target={inModal ? "_blank" : "_self"}
|
||||
@@ -109,6 +113,9 @@
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
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)}
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<div class="references">
|
||||
{#each recordEdges as recordEdge}
|
||||
<span class="mr-3">
|
||||
<span class="reference">
|
||||
<PreviewCardSmall {schemas} {graph} record={recordEdge}/>
|
||||
</span>
|
||||
{/each}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
export let inModal;
|
||||
export let modalUrl;
|
||||
export let graph;
|
||||
|
||||
let filter = {
|
||||
label: "",
|
||||
operator: "",
|
||||
@@ -58,6 +57,7 @@
|
||||
const filterRecord = extractFilterRecord(graph, value);
|
||||
|
||||
function extractFilterRecord(graph, value) {
|
||||
|
||||
if (!filter.isReference) {
|
||||
return null;
|
||||
}
|
||||
@@ -82,7 +82,7 @@
|
||||
{#if filter.isReference && filterRecord}
|
||||
{filter.label} is {previewTitle(channel.schemas, filterRecord)}
|
||||
{: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}
|
||||
|
||||
<button
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
const channel = getContext("channel");
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let inModal;
|
||||
export let modalUrl;
|
||||
const url = new URL(modalUrl ?? window.location.href);
|
||||
|
||||
function removeFilter(k) {
|
||||
|
||||
const url = new URL(modalUrl ?? window.location.href);
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
export let inModal;
|
||||
export let modalUrl;
|
||||
|
||||
|
||||
let dropdown;
|
||||
let search = "";
|
||||
let systemFieldsFiltered = systemFields;
|
||||
@@ -70,6 +69,13 @@
|
||||
activeOperator = operators.find(o => o.name === "eq")
|
||||
}
|
||||
|
||||
function selectOperator(e, operator) {
|
||||
activeOperator = operator;
|
||||
if (!operator.hasValue) {
|
||||
applyFilter(e)
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilter(e) {
|
||||
e.preventDefault();
|
||||
let filterPrefix = "";
|
||||
@@ -146,7 +152,7 @@
|
||||
<div class="selected-filter">field: {activeField.label}</div>
|
||||
|
||||
{#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}
|
||||
</button>
|
||||
{/each}
|
||||
@@ -214,8 +220,8 @@
|
||||
required
|
||||
/>
|
||||
|
||||
<button class="button applied-filter">
|
||||
Submit
|
||||
<button class="button applied-filter">
|
||||
Submit
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
@@ -135,13 +135,13 @@
|
||||
</div>
|
||||
|
||||
|
||||
{#if Object.entries(filter).length > 0}
|
||||
<div class="applied-filters">
|
||||
<AppliedFilterNotLinked
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
on:refresh
|
||||
/>
|
||||
<div class="applied-filters">
|
||||
<AppliedFilterNotLinked
|
||||
{inModal}
|
||||
{modalUrl}
|
||||
on:refresh
|
||||
></AppliedFilterNotLinked>
|
||||
{#if Object.entries(filter).length > 0}
|
||||
{#each Object.entries(filter) as [k, v]}
|
||||
<AppliedFilter
|
||||
{schema}
|
||||
@@ -154,6 +154,7 @@
|
||||
on:refresh
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
|
||||
export function imgurl(channel,record) {
|
||||
|
||||
export function imgurl(channel, record) {
|
||||
if (record._file.mime === "image/svg+xml") {
|
||||
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) {
|
||||
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 url = fileurl(channel,record)
|
||||
let url = fileurl(channel, record)
|
||||
|
||||
if (record._file.width > 0) {
|
||||
let presetUrl = url;
|
||||
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}" />`
|
||||
} else if (record._file.mime === "image/svg+xml") {
|
||||
html = `<img src="${url}" alt="${record._file.path}"/>`
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
<script>
|
||||
import Avatar from "../account/Avatar.svelte";
|
||||
import {getContext} from "svelte";
|
||||
import Dropdown from "../common/Dropdown.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
const user = getContext("user");
|
||||
console.log( channel.commands)
|
||||
</script>
|
||||
|
||||
|
||||
<div class="top-nav ">
|
||||
<a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a>
|
||||
|
||||
{#if channel.generateCommand}
|
||||
<a href="{channel.lucentUrl}/build-report" class="top-nav-item">Build website</a>
|
||||
{#if channel.commands.length > 0}
|
||||
<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}
|
||||
<!-- <div>-->
|
||||
<!-- <form method="GET">-->
|
||||
@@ -19,8 +27,8 @@
|
||||
<!-- class="form-control" required/>-->
|
||||
<!-- </form>-->
|
||||
<!-- </div>-->
|
||||
<a href="{channel.lucentUrl}/profile">
|
||||
<Avatar side="28" name={user.name}/>
|
||||
</a>
|
||||
<a href="{channel.lucentUrl}/profile">
|
||||
<Avatar side="28" name={user.name}/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script>
|
||||
|
||||
// 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 { autocompletion, completionKeymap } from "@codemirror/autocomplete";
|
||||
import {EditorState, Compartment} from "@codemirror/state";
|
||||
import {autocompletion, completionKeymap} from "@codemirror/autocomplete";
|
||||
import {Compartment, EditorState} from "@codemirror/state";
|
||||
import {keymap} from "@codemirror/view";
|
||||
import {indentWithTab} from "@codemirror/commands";
|
||||
import {markdown} from "@codemirror/lang-markdown";
|
||||
@@ -15,6 +15,29 @@
|
||||
export let value;
|
||||
export let editable = true;
|
||||
|
||||
export function insertMedia(info) {
|
||||
let insertText = "";
|
||||
if (info.record._file.width > 0) {
|
||||
insertText = ``;
|
||||
} 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(() => {
|
||||
let language = new Compartment();
|
||||
let tabSize = new Compartment();
|
||||
@@ -51,7 +74,6 @@
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
|
||||
@@ -100,8 +100,8 @@
|
||||
tinymce.init({...config, ...additionalConfig});
|
||||
});
|
||||
|
||||
export function insertMedia(html){
|
||||
activeEditor.execCommand('InsertHTML', false, html);
|
||||
export function insertMedia(info){
|
||||
activeEditor.execCommand('InsertHTML', false, info.html);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
import {Editor} from '@tiptap/core'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Dropcursor from '@tiptap/extension-dropcursor'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import Heading from '@tiptap/extension-heading'
|
||||
import HardBreak from '@tiptap/extension-hard-break'
|
||||
import Blockquote from '@tiptap/extension-blockquote';
|
||||
import CodeBlock from '@tiptap/extension-code-block';
|
||||
import Bold from '@tiptap/extension-bold';
|
||||
import BulletList from '@tiptap/extension-bullet-list';
|
||||
import Code from '@tiptap/extension-code';
|
||||
@@ -19,6 +22,8 @@
|
||||
import TableCell from '@tiptap/extension-table-cell';
|
||||
import TableHeader from '@tiptap/extension-table-header';
|
||||
import Underline from '@tiptap/extension-underline';
|
||||
import Image from '@tiptap/extension-image';
|
||||
import Icon from "../common/Icon.svelte";
|
||||
|
||||
let element;
|
||||
let editor;
|
||||
@@ -32,19 +37,22 @@
|
||||
Paragraph,
|
||||
Text,
|
||||
Bold,
|
||||
ListItem,
|
||||
BulletList,
|
||||
Code,
|
||||
CodeBlock,
|
||||
History,
|
||||
Italic,
|
||||
ListItem,
|
||||
HardBreak,
|
||||
OrderedList,
|
||||
ListItem,
|
||||
Strike,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
Underline,
|
||||
Dropcursor,
|
||||
Image,
|
||||
Heading.configure({
|
||||
levels: [1, 2, 3],
|
||||
}),
|
||||
@@ -56,7 +64,11 @@
|
||||
// force re-render so `editor.isActive` works as expected
|
||||
editor = editor;
|
||||
},
|
||||
onUpdate: ({editor}) => {
|
||||
value = editor.getHTML()
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -64,33 +76,99 @@
|
||||
editor.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
export function insertMedia(info){
|
||||
editor.chain().focus().setImage({ src: info.url }).run()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if editor}
|
||||
<button
|
||||
on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
class:active={editor.isActive('heading', { level: 1 })}
|
||||
>
|
||||
H1
|
||||
</button>
|
||||
<button
|
||||
on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
class:active={editor.isActive('heading', { level: 2 })}
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
<button
|
||||
on:click={() => editor.chain().focus().setParagraph().run()}
|
||||
class:active={editor.isActive('paragraph')}
|
||||
>
|
||||
P
|
||||
</button>
|
||||
<button
|
||||
on:click={() => editor.chain().focus().toggleBold().run()}
|
||||
class:active={editor.isActive('bold')}
|
||||
>
|
||||
Bold
|
||||
</button>
|
||||
<div class="editor-toolbar">
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
class:active={editor.isActive('heading', { level: 1 })}
|
||||
>
|
||||
H1
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
class:active={editor.isActive('heading', { level: 2 })}
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="button"
|
||||
on:click={() => editor.chain().focus().toggleBold().run()}
|
||||
class:active={editor.isActive('bold')}
|
||||
>
|
||||
B
|
||||
</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}
|
||||
|
||||
<div bind:this={element} class="content"/>
|
||||
@@ -1,21 +1,68 @@
|
||||
<script>
|
||||
import {onMount} from "svelte";
|
||||
import {onDestroy, onMount} from "svelte";
|
||||
import Trix from "trix"
|
||||
import customcss from "./tinymce.css?inline";
|
||||
import "trix/dist/trix.css"
|
||||
|
||||
export let value = "";
|
||||
let textareaEl;
|
||||
let lastVal;
|
||||
let editorWrapper;
|
||||
let activeEditor;
|
||||
export let field;
|
||||
let editor;
|
||||
|
||||
|
||||
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
|
||||
console.log(Trix.config)
|
||||
Trix.config.blockAttributes.heading3 = {
|
||||
tagName: 'h3',
|
||||
terminal: true,
|
||||
breakOnReturn: true,
|
||||
group: false
|
||||
}
|
||||
// console.log(Trix.config)
|
||||
|
||||
</script>
|
||||
|
||||
<div bind:this={editorWrapper} class="tox-wrapper">
|
||||
<input bind:this={textareaEl} id="x" bind:value type="hidden">
|
||||
<trix-editor class="trix-content content" input="x"></trix-editor>
|
||||
<div class="tox-wrapper">
|
||||
<input id="x-{field.name}" {value} type="hidden">
|
||||
<trix-editor
|
||||
bind:this={editor}
|
||||
class=" content"
|
||||
input="x-{field.name}"
|
||||
role="textbox"
|
||||
tabindex="0"
|
||||
on:trix-change={updateValue}
|
||||
|
||||
></trix-editor>
|
||||
</div>
|
||||
|
||||
@@ -98,6 +98,16 @@
|
||||
bind:graph
|
||||
{record}
|
||||
/>
|
||||
{:else if field.info.name === "markdown"}
|
||||
<Markdown
|
||||
bind:value={data[field.name]}
|
||||
{schema}
|
||||
{field}
|
||||
{validationErrors}
|
||||
{isCreateMode}
|
||||
bind:graph
|
||||
{record}
|
||||
/>
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={formElement}
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
let backlinks = graph.parentEdges.map(edge => {
|
||||
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
|
||||
let edgeField = findEdgeField(schema,edge.field);
|
||||
if(!edgeField){
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
field: edgeField.label,
|
||||
record: graph.records.find( record => record.id === edge.source)
|
||||
}
|
||||
})
|
||||
}).filter( edgeOrNull => !!edgeOrNull)
|
||||
</script>
|
||||
<div class="editor-field">
|
||||
{#each backlinks as backlink}
|
||||
|
||||
@@ -13,11 +13,8 @@
|
||||
{#if record?.data}
|
||||
<a
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
class="text-decoration-none rounded py-1 px-2 d-inline-block"
|
||||
{title}
|
||||
style="border:2px solid {!schema.color
|
||||
? '#999'
|
||||
: schema.color}!important;white-space: nowrap;"
|
||||
class="reference"
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
|
||||
@@ -9,13 +9,17 @@
|
||||
export let value;
|
||||
export let isCreateMode;
|
||||
export let validationErrors;
|
||||
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||
|
||||
export let id;
|
||||
let wrapperDiv;
|
||||
let pickerInput;
|
||||
let pickerInstance;
|
||||
let flatpickrOptions = {
|
||||
appendTo: wrapperDiv,
|
||||
static: true,
|
||||
allowInput: true,
|
||||
altInput: true,
|
||||
altFormat: "Y-m-d H:i:S",
|
||||
@@ -40,7 +44,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mb-0">
|
||||
<div class="mb-0" bind:this={wrapperDiv}>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
<script>
|
||||
import Codemirror from "../../libs/CodemirrorMarkdown.svelte";
|
||||
import { getErrorMessage } from "./errorMessage";
|
||||
import RichEditorFiles from "./RichEditorFiles.svelte";
|
||||
|
||||
|
||||
export let value;
|
||||
export let field;
|
||||
export let graph;
|
||||
export let record;
|
||||
export let isCreateMode;
|
||||
// export let id;
|
||||
export let validationErrors;
|
||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||
|
||||
let editor;
|
||||
|
||||
function insertMedia(e){
|
||||
editor.insertMedia(e.detail)
|
||||
}
|
||||
</script>
|
||||
|
||||
<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}
|
||||
<div class="invalid-feedback d-block">
|
||||
{errorMessage}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
|
||||
function createInlineReference(e, schemaUId) {
|
||||
e.preventDefault();
|
||||
inLineCreateRecord = null;
|
||||
axios
|
||||
.get(channel.lucentUrl + "/records/newInline?schema=" + schemaUId)
|
||||
.then((response) => {
|
||||
@@ -59,27 +60,29 @@
|
||||
<div
|
||||
style="display: flex;align-items: center;gap:4px"
|
||||
>
|
||||
{#each schemas as schema}
|
||||
<Dropdown>
|
||||
<div slot="button" class:is-first={!recordId}>
|
||||
{schema.label}
|
||||
</div>
|
||||
<Dropdown>
|
||||
<div slot="button">New</div>
|
||||
{#each schemas as schema}
|
||||
<button
|
||||
class=" button"
|
||||
on:click={(e) =>
|
||||
createInlineReference(e, schema.name)}
|
||||
>Create New Record
|
||||
>{schema.label}
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<div slot="button"> <Icon icon="magnifying-glass"/></div>
|
||||
{#each schemas as schema}
|
||||
<button
|
||||
class="button"
|
||||
on:click={(e) => openBrowseModal(e, schema.name)}
|
||||
>
|
||||
<Icon icon="magnifying-glass"/>
|
||||
Search
|
||||
</button
|
||||
>
|
||||
</Dropdown>
|
||||
{/each}
|
||||
>{schema.label}
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
</Dropdown>
|
||||
</div>
|
||||
{:else}
|
||||
<div style="display:flex;align-items: center;gap: 4px">
|
||||
@@ -100,9 +103,9 @@
|
||||
|
||||
<DialogRecord bind:this={dialogRecord}>
|
||||
{#if inLineCreateRecord}
|
||||
|
||||
<InlineEdit
|
||||
{...inLineCreateRecord}
|
||||
isCreateMode={true}
|
||||
on:cancel={(e) => (inLineCreateRecord = null)}
|
||||
on:inlinesaved={save}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Tinymce from "../../libs/Tinymce.svelte";
|
||||
import RichEditorFiles from "./RichEditorFiles.svelte";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import Trix from "../../libs/Trix.svelte";
|
||||
|
||||
export let value;
|
||||
export let field;
|
||||
@@ -24,8 +25,8 @@
|
||||
|
||||
<div class="mb-0">
|
||||
|
||||
|
||||
<Tinymce bind:this={editor} bind:value {additionalConfig}/>
|
||||
<Trix {field} bind:this={editor} bind:value></Trix>
|
||||
<!-- <Tinymce bind:this={editor} bind:value {additionalConfig}/>-->
|
||||
{#if field.collections.length > 0}
|
||||
<RichEditorFiles
|
||||
bind:graph
|
||||
@@ -38,7 +39,7 @@
|
||||
|
||||
</RichEditorFiles>
|
||||
{/if}
|
||||
<!-- <TipTap bind:value />-->
|
||||
|
||||
|
||||
{#if errorMessage}
|
||||
<div class="invalid-feedback d-block">
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script>
|
||||
import {sortByField} from "../../edges/sortEdges";
|
||||
import Sortable from "../../libs/Sortable.svelte";
|
||||
import PreviewFile from "../previews/PreviewFile.svelte";
|
||||
import Dropdown from "../../common/Dropdown.svelte";
|
||||
import Dialog from "../../dialog/Dialog.svelte";
|
||||
@@ -70,7 +68,8 @@
|
||||
{#each references as reference (reference.id)}
|
||||
<!--This div helps the sorting thing-->
|
||||
<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>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {createEventDispatcher, getContext} from "svelte";
|
||||
import Preview from "../../files/Preview.svelte";
|
||||
import {previewTitle} from "./../Preview";
|
||||
import {htmlurl} from "../../files/imageserver.js"
|
||||
import {fileurl, htmlurl} from "../../files/imageserver.js"
|
||||
import Status from "./../Status.svelte";
|
||||
import Dropdown from "../../common/Dropdown.svelte";
|
||||
|
||||
@@ -25,8 +25,14 @@
|
||||
|
||||
function insert(e, preset) {
|
||||
e.preventDefault();
|
||||
let html = htmlurl(channel,record, preset)
|
||||
dispatch("editor-insert", html);
|
||||
let html = htmlurl(channel, record, preset)
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Generated
+2562
-587
File diff suppressed because it is too large
Load Diff
+3
-20
@@ -11,40 +11,23 @@
|
||||
"@codemirror/lang-markdown": "^6.2.5",
|
||||
"@codemirror/state": "^6.4.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",
|
||||
"codemirror": "^6.0.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"flatpickr": "^4.6.13",
|
||||
"fuse.js": "^7.0.0",
|
||||
"htmx.org": "^2.0.1",
|
||||
"install": "^0.13.0",
|
||||
"laravel-vite-plugin": "^1.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
"npm": "^10.8.2",
|
||||
"postcss": "8.4.31",
|
||||
"sass": "^1.77.8",
|
||||
"sortablejs": "^1.15.2",
|
||||
"svelte": "^4.2.18",
|
||||
"tinymce": "^6.8.4",
|
||||
"trix": "^2.1.5",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "5.2.6"
|
||||
}
|
||||
|
||||
@@ -7,11 +7,16 @@
|
||||
|
||||
.cm-content{
|
||||
background-color: var(--p10);
|
||||
color: var(--p100);
|
||||
}
|
||||
}
|
||||
|
||||
.cm-content{
|
||||
background-color: var(--p20);
|
||||
|
||||
}
|
||||
.ͼ4 .cm-line ::selection, .ͼ4 .cm-line::selection{
|
||||
background: var(--p40) !important;
|
||||
}
|
||||
|
||||
.cm-activeLine{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ body:has(dialog[open]) {
|
||||
|
||||
dialog {
|
||||
margin: 2vh auto;
|
||||
background-color: #fff;
|
||||
background-color: var(--p10);
|
||||
padding: 34px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
@@ -49,6 +49,6 @@ dialog::backdrop {
|
||||
position: sticky;
|
||||
top: -34px;
|
||||
z-index: 999;
|
||||
background: #fff;
|
||||
background-color: var(--p10);
|
||||
padding: 10px 0;
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
border-radius: 12px;
|
||||
z-index: 20;
|
||||
z-index: 22;
|
||||
background: var(--p20);
|
||||
transition: 600ms;
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.record-edit {
|
||||
position: relative;
|
||||
|
||||
max-width: 900px;
|
||||
.invalid-feedback {
|
||||
color: var(--text-error);
|
||||
font-size: 15px;
|
||||
@@ -44,7 +44,7 @@
|
||||
margin: 6px 0;
|
||||
border-color: transparent;
|
||||
|
||||
.button {
|
||||
.button:not(.primary) {
|
||||
background: var(--p30);
|
||||
|
||||
&:hover {
|
||||
@@ -53,9 +53,10 @@
|
||||
}
|
||||
|
||||
dialog {
|
||||
.button {
|
||||
.button:not(.primary) {
|
||||
background: var(--p20);
|
||||
|
||||
|
||||
&:hover {
|
||||
background: var(--p30);
|
||||
}
|
||||
@@ -142,4 +143,5 @@
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,14 @@
|
||||
align-items: center;
|
||||
background: var(--p30);
|
||||
font-size: 16px;
|
||||
padding: 3px 12px 6px;
|
||||
padding: 3px 12px 3px;
|
||||
color: var(--text);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
&:focus{
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
background: var(--p40);
|
||||
|
||||
@@ -113,6 +113,18 @@
|
||||
.field-ui-number {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.references{
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.reference{
|
||||
font-size: 13px;
|
||||
border-radius: 12px;
|
||||
background: var(--p30);
|
||||
padding: 1px 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-table-row {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
margin: 20px 0 20px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
.tab{
|
||||
list-style: none;
|
||||
|
||||
|
||||
@@ -12,6 +12,20 @@
|
||||
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 {
|
||||
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 {
|
||||
|
||||
+2
-1
@@ -53,6 +53,7 @@
|
||||
@import "./table";
|
||||
@import "./avatar";
|
||||
@import "./codemirror";
|
||||
@import "./rich";
|
||||
@import "./layout";
|
||||
@import "./wrappers";
|
||||
@import "./toolbar";
|
||||
@@ -69,6 +70,7 @@
|
||||
@import "./reference-tags";
|
||||
@import "./members";
|
||||
@import "./revisions";
|
||||
@import "./datepicker";
|
||||
|
||||
body {
|
||||
background-color: var(--p10);
|
||||
@@ -103,4 +105,3 @@ a {
|
||||
.lucent-component {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<h2 class="mb-5">Enter Lucent</h2>
|
||||
|
||||
<form hx-post="/lucent/login" >
|
||||
@csrf
|
||||
<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>
|
||||
<div class="mt-5 mb-3">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
@if(config("lucent.env") === "production")
|
||||
<!-- if production -->
|
||||
<link rel="stylesheet" href="/vendor/lucent/dist/{{ $manifest['main.css']["file"] }}"/>
|
||||
<link rel="stylesheet" href="/vendor/lucent/dist/{{ $manifest['main.js']["css"][0] }}"/>
|
||||
<script type="module" src="/vendor/lucent/dist/{{ $manifest['main.js']["file"] }}"></script>
|
||||
@else
|
||||
<!-- if development -->
|
||||
|
||||
+11
-11
@@ -3,7 +3,7 @@
|
||||
namespace Lucent\Account;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Lucent\Database\Database;
|
||||
use Lucent\Primitive\Collection;
|
||||
use PhpOption\Option;
|
||||
|
||||
@@ -12,7 +12,7 @@ class UserRepo
|
||||
|
||||
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
|
||||
{
|
||||
$usersData = DB::table("users")->get();
|
||||
$usersData = Database::make()->table("users")->get();
|
||||
|
||||
$users = array_map(fn($userData) => $this->fromArray((array)$userData), $usersData->toArray());
|
||||
return new Collection($users);
|
||||
@@ -31,14 +31,14 @@ class UserRepo
|
||||
{
|
||||
$userData = toArray($user);
|
||||
$userData["roles"] = json_encode($userData["roles"]);
|
||||
DB::table("users")->insert($userData);
|
||||
Database::make()->table("users")->insert($userData);
|
||||
}
|
||||
|
||||
public function update(User $user): void
|
||||
{
|
||||
$userData = toArray($user);
|
||||
$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);
|
||||
|
||||
DB::table("users")
|
||||
Database::make()->table("users")
|
||||
->where("id", $id)
|
||||
->update([
|
||||
'loggedInAt' => Carbon::now()->toJson(),
|
||||
@@ -62,7 +62,7 @@ class UserRepo
|
||||
*/
|
||||
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)) {
|
||||
return none();
|
||||
@@ -76,7 +76,7 @@ class UserRepo
|
||||
*/
|
||||
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)) {
|
||||
return none();
|
||||
@@ -88,12 +88,12 @@ class UserRepo
|
||||
|
||||
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
|
||||
{
|
||||
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
|
||||
@@ -102,7 +102,7 @@ class UserRepo
|
||||
id: $data["id"],
|
||||
name: new Name($data["name"] ?? ""),
|
||||
email: new Email($data["email"]),
|
||||
roles: json_decode($data["roles"] ?? "[]",true),
|
||||
roles: json_decode($data["roles"] ?? "[]", true),
|
||||
createdAt: $data["createdAt"],
|
||||
updatedAt: $data["updatedAt"],
|
||||
loggedInAt: $data["loggedInAt"] ?? null,
|
||||
|
||||
+26
-3
@@ -2,31 +2,54 @@
|
||||
|
||||
namespace Lucent\Channel;
|
||||
|
||||
use Lucent\Channel\Data\UserCommand;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Schema\FilesSchema;
|
||||
use Lucent\Schema\Schema;
|
||||
|
||||
final class Channel
|
||||
{
|
||||
public string $lucentUrl;
|
||||
public string $filesUrl;
|
||||
public array $disks;
|
||||
public string $previewTargetUrl;
|
||||
|
||||
/**
|
||||
* @param Collection<Schema> $schemas
|
||||
* @param Collection<UserCommand> $commands
|
||||
*/
|
||||
function __construct(
|
||||
public string $name,
|
||||
public string $url,
|
||||
public string $previewTarget,
|
||||
public string $generateCommand,
|
||||
public Collection $commands,
|
||||
public Collection $schemas,
|
||||
public array $imageFilters,
|
||||
public array $roles,
|
||||
)
|
||||
{
|
||||
$this->lucentUrl = $url . "/lucent";
|
||||
$this->filesUrl = $url . "/storage";
|
||||
$this->previewTargetUrl = $url . "/". $previewTarget;
|
||||
$this->filesUrl = $this->makeFilesUrl();
|
||||
$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;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Lucent\Channel;
|
||||
|
||||
|
||||
use Lucent\Channel\Data\UserCommand;
|
||||
use Lucent\Primitive\Collection;
|
||||
use Lucent\Schema\Schema;
|
||||
use Lucent\Schema\SchemaService;
|
||||
@@ -13,7 +14,7 @@ final class ChannelService
|
||||
public Channel $channel;
|
||||
|
||||
private function __construct(
|
||||
public SchemaService $schemaService
|
||||
public SchemaService $schemaService,
|
||||
)
|
||||
{
|
||||
|
||||
@@ -29,11 +30,16 @@ final class ChannelService
|
||||
$schemaService = new SchemaService();
|
||||
$schemasCollection = (new Collection($schemasArray["schemas"] ?? []))->map([$schemaService, 'fromArray']);
|
||||
|
||||
$userCommands = [];
|
||||
foreach (config("lucent.commands") ?? [] as $signature => $desc) {
|
||||
$userCommands[] = new UserCommand($desc, $signature);
|
||||
}
|
||||
|
||||
$channel = new Channel(
|
||||
name: config("lucent.name") ?? "",
|
||||
url: rtrim(config("lucent.url") ?? "", "/"),
|
||||
previewTarget: rtrim(config("lucent.previewTarget") ?? "", "/"),
|
||||
generateCommand: config("lucent.generateCommand") ?? "",
|
||||
commands: Collection::make($userCommands),
|
||||
schemas: $schemasCollection,
|
||||
imageFilters: config("lucent.imageFilters") ?? [],
|
||||
roles: $schemasArray["roles"] ?? []
|
||||
@@ -63,7 +69,7 @@ final class ChannelService
|
||||
*/
|
||||
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");
|
||||
$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();
|
||||
@@ -76,8 +82,8 @@ final class ChannelService
|
||||
*/
|
||||
public function schemasWritableByRoles(array $roles): array
|
||||
{
|
||||
$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");
|
||||
$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");
|
||||
return $schemasAllRead->merge($schemasCanWrite)->unique()->values()->toArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Channel\Data;
|
||||
|
||||
class UserCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $signature,
|
||||
)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,19 +16,13 @@ class CompileSchemas extends Command
|
||||
protected $description = 'Compiles schemas';
|
||||
|
||||
|
||||
public function __construct(
|
||||
public SchemaService $schemaService
|
||||
)
|
||||
public function handle(SchemaService $schemaService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$configDir = base_path(config('lucent.schemas_path'));
|
||||
$schemasDirIterator = new DirectoryIterator($configDir);
|
||||
$schemas = [];
|
||||
|
||||
foreach ($schemasDirIterator as $file) {
|
||||
if ($file->getExtension() !== "json") {
|
||||
continue;
|
||||
@@ -46,7 +40,7 @@ class CompileSchemas extends Command
|
||||
|
||||
$schemas = collect($schemas)->sortBy("label")->values();
|
||||
$roles = $schemas
|
||||
->map([$this->schemaService, 'fromArray'])
|
||||
->map([$schemaService, 'fromArray'])
|
||||
->whereIn("type", [Type::COLLECTION, Type::FILES])
|
||||
->reduce(fn($carry, Schema $schema) => array_merge(
|
||||
$carry,
|
||||
|
||||
@@ -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.");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,9 @@ use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\File\FileService;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Schema\FilesSchema;
|
||||
use Lucent\Schema\Schema;
|
||||
use Lucent\Schema\Type;
|
||||
|
||||
@@ -18,7 +21,10 @@ class RebuildThumbnails extends Command
|
||||
protected $description = 'Rebuilds thumbnails for path';
|
||||
|
||||
|
||||
public function __construct(public ImageManager $imageManager)
|
||||
public function __construct(
|
||||
public Query $query,
|
||||
public FileService $fileService,
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -27,49 +33,19 @@ class RebuildThumbnails extends Command
|
||||
public function handle(ChannelService $channelService): int
|
||||
{
|
||||
$channelService->channel->schemas
|
||||
->where("type", Type::FILES)->values()
|
||||
->filter(fn(Schema $schema) => get_class($schema) === FilesSchema::class)
|
||||
->map([$this, 'rebuildThumbnails']);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function rebuildThumbnails(Schema $schema): void
|
||||
public function rebuildThumbnails(FilesSchema $schema): void
|
||||
{
|
||||
|
||||
$filesDir = storage_path("app/public/" . $schema->path . "/");
|
||||
$thumbDir = storage_path("app/public/thumbs/" . $schema->path . "/");
|
||||
if (!file_exists($thumbDir)) {
|
||||
make_dir_r($thumbDir);
|
||||
}
|
||||
|
||||
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());
|
||||
$this->info("Rebuilding thumbnails for ". $schema->name);
|
||||
$records = $this->query->filter(["schema" => $schema->name])->run()->records;
|
||||
$disk = $this->fileService->loadDisk($schema->disk);
|
||||
foreach ($records as $record) {
|
||||
$this->fileService->createTemplates($disk, $record->_file->path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -1,15 +1,49 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"env" => env("LUCENT_ENV", "production"),
|
||||
"schemas_path" => env("LUCENT_SCHEMAS_PATH", "app/Lucent"),
|
||||
"database" => env('LUCENT_DB_CONNECTION', env('DB_CONNECTION',"sqlite")),
|
||||
"name" => env("LUCENT_NAME", "Lucent"),
|
||||
"env" => env("LUCENT_ENV", "production"),
|
||||
"schemas_path" => env("LUCENT_SCHEMAS_PATH", "resources/lucent/schemas"),
|
||||
"database" => env('LUCENT_DB_CONNECTION', env('DB_CONNECTION', "sqlite")),
|
||||
"name" => env("LUCENT_NAME", "Lucent"),
|
||||
"url" => env("LUCENT_URL", env('APP_URL')),
|
||||
"previewTarget" => env("LUCENT_PREVIEW_TARGET", "previewTarget"),
|
||||
"generateCommand" => env("LUCENT_GENERATE_COMMAND", "generate:static"),
|
||||
"previewTarget" => env("LUCENT_PREVIEW_TARGET", "previewTarget"),
|
||||
/*
|
||||
* 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" => [],
|
||||
"canInvite" => ["admin"],
|
||||
"canBuild" => ["admin"],
|
||||
"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
|
||||
]
|
||||
];
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php namespace Lucent\Edge;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Lucent\Database\Database;
|
||||
use Lucent\LucentException;
|
||||
use PDOException;
|
||||
use stdClass;
|
||||
@@ -15,7 +15,7 @@ class EdgeRepo
|
||||
public function insert(Edge $edge): void
|
||||
{
|
||||
try {
|
||||
DB::table("edges")->insert($edge->toDB());
|
||||
Database::make()->table("edges")->insert($edge->toDB());
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == 23505) {
|
||||
throw new LucentException("Edge already exists");
|
||||
@@ -34,7 +34,7 @@ class EdgeRepo
|
||||
{
|
||||
$edgesDB = collect($edges)->map(fn($e) => $e->toDB())->toArray();
|
||||
try {
|
||||
DB::table("edges")->insert($edgesDB);
|
||||
Database::make()->table("edges")->insert($edgesDB);
|
||||
} catch (PDOException $e) {
|
||||
if ($e->getCode() == 23505) {
|
||||
throw new LucentException("Edge already exists");
|
||||
@@ -52,8 +52,8 @@ class EdgeRepo
|
||||
public function replaceForRecord(string $from, array $edges): void
|
||||
{
|
||||
$edgesDB = collect($edges)->map(fn($e) => $e->toDB())->toArray();
|
||||
DB::table("edges")->where("source", $from)->delete();
|
||||
DB::table("edges")->insert($edgesDB);
|
||||
Database::make()->table("edges")->where("source", $from)->delete();
|
||||
Database::make()->table("edges")->insert($edgesDB);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,13 +62,13 @@ class EdgeRepo
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
$edges = DB::table("edges")->get();
|
||||
$edges = Database::make()->table("edges")->get();
|
||||
return $edges->map([$this, 'mapEdge'])->toArray();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class EdgeRepo
|
||||
|
||||
public function remove(Edge $edge): void
|
||||
{
|
||||
DB::table("edges")
|
||||
Database::make()->table("edges")
|
||||
->where("source", $edge->source)
|
||||
->where("target", $edge->target)
|
||||
->where("sourceSchema", $edge->sourceSchema)
|
||||
@@ -100,7 +100,7 @@ class EdgeRepo
|
||||
|
||||
public function findLastEdgeRank(string $source, string $field): string
|
||||
{
|
||||
$data = DB::table("edges")
|
||||
$data = Database::make()->table("edges")
|
||||
->where("source", $source)
|
||||
->where("field", $field)
|
||||
->orderBy("rank", "desc")
|
||||
|
||||
+23
-44
@@ -2,14 +2,14 @@
|
||||
|
||||
namespace Lucent\File;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Log\Logger;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\ImageManagerStatic;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Database\Database;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Record\FileData as RecordFile;
|
||||
use Lucent\Record\QueryRecord;
|
||||
@@ -21,14 +21,16 @@ class FileService
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
public ChannelService $channelService
|
||||
public ChannelService $channelService,
|
||||
public ImageManager $imageManager,
|
||||
public Logger $logger
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
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
|
||||
@@ -65,8 +67,7 @@ class FileService
|
||||
isDuplicate: true
|
||||
);
|
||||
}
|
||||
|
||||
$disk = $this->loadDisk();
|
||||
$disk = $this->loadDisk($schema);
|
||||
$path = $schema->path . "/" . $filename;
|
||||
$res = $disk->put(
|
||||
$path,
|
||||
@@ -78,13 +79,14 @@ class FileService
|
||||
throw new LucentException("File $filename not uploaded");
|
||||
}
|
||||
|
||||
$this->createThumbnail($disk, $schema->path, $filename, $file);
|
||||
$this->createTemplates($disk, $path, $file);
|
||||
|
||||
list($width, $height) = $this->isImage($mimetype) ? getimagesize($file) : [0, 0];
|
||||
$recordFile = new RecordFile(
|
||||
originalName: $originalFilename,
|
||||
mime: $mimetype,
|
||||
path: $path,
|
||||
disk: $schema->disk,
|
||||
size: $file->getSize(),
|
||||
width: $width,
|
||||
height: $height,
|
||||
@@ -109,27 +111,16 @@ class FileService
|
||||
return in_array($mimetype, $imageMimes);
|
||||
}
|
||||
|
||||
public function loadDisk(): Filesystem
|
||||
public function loadDisk(Schema|string $schema): Filesystem
|
||||
{
|
||||
return Storage::build([
|
||||
'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,
|
||||
]);
|
||||
return Storage::disk($schema->disk ?? $schema);
|
||||
|
||||
}
|
||||
|
||||
private function checkDuplicate(string $schemaName, string $checksum, int $filesize): string
|
||||
{
|
||||
|
||||
$record = DB::table("records")
|
||||
$record = Database::make()->table("records")
|
||||
->where("schema", $schemaName)
|
||||
->where("_file->checksum", $checksum)
|
||||
->where("_file->size", $filesize)
|
||||
@@ -138,30 +129,18 @@ class FileService
|
||||
return $record->id ?? "";
|
||||
}
|
||||
|
||||
private function createThumbnail(Filesystem $disk, string $schemaPath, string $filename, UploadedFile $file): void
|
||||
public function createTemplates(Filesystem $disk, string $path): void
|
||||
{
|
||||
|
||||
|
||||
$thumbDir = storage_path("app/public/thumbs/" . $schemaPath . "/");
|
||||
if (!file_exists($thumbDir)) {
|
||||
make_dir_r($thumbDir);
|
||||
$originalImage = $this->imageManager->make($disk->get($path));
|
||||
foreach (config("lucent.imageFilters") as $preset => $filterClass) {
|
||||
$image = $originalImage->filter(new $filterClass);
|
||||
$templateUri = "/templates/" . $preset . "/" . $path;
|
||||
$disk->put($templateUri, $image->encode('webp', 75));
|
||||
}
|
||||
|
||||
try {
|
||||
ImageManagerStatic::configure(['driver' => 'imagick']);
|
||||
$image = ImageManagerStatic::make($file);
|
||||
} catch (Exception $e) {
|
||||
logger($e->getMessage());
|
||||
return;
|
||||
}
|
||||
$thumbDir = "thumbs/" . $path;
|
||||
|
||||
$image->fit(300, 300);
|
||||
try {
|
||||
$image->encode('webp', 75)->save($thumbDir . $filename);
|
||||
} catch (Exception $e) {
|
||||
logger($e->getMessage());
|
||||
}
|
||||
$image = $originalImage->fit(300, 300);
|
||||
$disk->put($thumbDir, $image->encode('webp', 75));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -5,64 +5,62 @@ namespace Lucent\Http\Controller;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Command\CommandRepo;
|
||||
use Lucent\Command\CommandService;
|
||||
use Lucent\Svelte\Svelte;
|
||||
use function Lucent\Response\ok;
|
||||
|
||||
class BuildController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
public readonly Svelte $svelte,
|
||||
public readonly ChannelService $channelService,
|
||||
private readonly Svelte $svelte,
|
||||
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.3 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(
|
||||
layout: "channel",
|
||||
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) {
|
||||
// sleep(1); // 50ms
|
||||
|
||||
$commandLogItem = $this->commandRepo->findBySignature($commandSignature);
|
||||
$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"]);
|
||||
|
||||
echo 'data: ' .json_encode($data);
|
||||
echo 'data: ' . json_encode($data);
|
||||
echo "\n\n";
|
||||
|
||||
ob_flush();
|
||||
flush();
|
||||
|
||||
if(str_contains($data["logs"],"Finito")){
|
||||
break;
|
||||
}
|
||||
|
||||
if(str_contains($data["logs"],"Exception")){
|
||||
logger($this->commandService->commandIsRunning($commandLogItem->pid));
|
||||
if (!$this->commandService->commandIsRunning($commandLogItem->pid)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -79,5 +77,4 @@ class BuildController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\File\FileService;
|
||||
use Lucent\File\ImageService;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Record\InputData\RecordInputData;
|
||||
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)
|
||||
{
|
||||
$disk = $this->fileService->loadDisk();
|
||||
@@ -39,7 +55,6 @@ class FileController extends Controller
|
||||
$validator = Validator::make($request->all(), [
|
||||
'files.*' => 'required|file|max:100000',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return fail($validator->errors()->first());
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use Lucent\Account\AccountService;
|
||||
use Lucent\Account\AuthService;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\LucentException;
|
||||
use Lucent\Query\Operator;
|
||||
use Lucent\Query\Operator\OperatorRegistry;
|
||||
use Lucent\Query\Query;
|
||||
use Lucent\Record\InputData\EdgeInputData;
|
||||
use Lucent\Record\InputData\RecordInputData;
|
||||
@@ -25,13 +25,13 @@ use function Lucent\Response\ok;
|
||||
class RecordController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RecordService $recordService,
|
||||
private readonly AccountService $accountService,
|
||||
private readonly AuthService $authService,
|
||||
private readonly ChannelService $channelService,
|
||||
private readonly Svelte $svelte,
|
||||
private readonly Query $query,
|
||||
private readonly Manager $recordManager
|
||||
private readonly RecordService $recordService,
|
||||
private readonly AccountService $accountService,
|
||||
private readonly ChannelService $channelService,
|
||||
private readonly Svelte $svelte,
|
||||
private readonly Query $query,
|
||||
private readonly Manager $recordManager,
|
||||
private readonly OperatorRegistry $operatorRegistry
|
||||
)
|
||||
{
|
||||
}
|
||||
@@ -60,10 +60,12 @@ class RecordController extends Controller
|
||||
], $filter);
|
||||
|
||||
|
||||
|
||||
$skip = data_get($urlParams, "skip") ?? 0;
|
||||
$limit = 30;
|
||||
$records = [];
|
||||
$graphArray = null;
|
||||
|
||||
$graph = $this->query
|
||||
->filter($arguments)
|
||||
->notLinked($request->input("notlinked") ?? "")
|
||||
@@ -79,7 +81,6 @@ class RecordController extends Controller
|
||||
|
||||
$records = $graph->getRootRecords()->toArray();
|
||||
|
||||
|
||||
$data = [
|
||||
"schemas" => $this->channelService->channel->schemas,
|
||||
"schema" => $schema,
|
||||
@@ -87,7 +88,7 @@ class RecordController extends Controller
|
||||
"records" => $records,
|
||||
"graph" => toArray($graph),
|
||||
"systemFields" => array_values(System::list()),
|
||||
"operators" => array_values(Operator::list()),
|
||||
"operators" => $this->operatorRegistry->all(),
|
||||
"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),
|
||||
"limit" => $limit,
|
||||
@@ -165,7 +166,7 @@ class RecordController extends Controller
|
||||
|
||||
$schema = $this->channelService->channel->schemas->where("name", $request->input("schema"))->first();
|
||||
$recordHistory = $this->recordManager->fromSession($request->session())->getRecords();
|
||||
$record = $this->recordService->createEmpty($schema, $this->authService->currentUserId());
|
||||
$record = $this->recordService->createEmpty($schema);
|
||||
$queryRecord = QueryRecord::fromRecord($record);
|
||||
return $this->svelte->render(
|
||||
layout: "channel",
|
||||
@@ -200,6 +201,7 @@ class RecordController extends Controller
|
||||
"schema" => $schema,
|
||||
"record" => $queryRecord,
|
||||
"isCreateMode" => true,
|
||||
"isWritable" => in_array($record->schema, $this->accountService->currentWritableSchemas())
|
||||
];
|
||||
}
|
||||
|
||||
@@ -302,7 +304,7 @@ class RecordController extends Controller
|
||||
id: $request->input("record.id"),
|
||||
data: $request->input("record.data"),
|
||||
status: Status::from($request->input("record.status")),
|
||||
edges: array_map(EdgeInputData::fromArray(...), $request->input("edges") ?? []),
|
||||
edges: array_map(EdgeInputData::fromArray(...), $request->input("edges") ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -10,6 +10,11 @@ use Lucent\Http\Controller\HomeController;
|
||||
use Lucent\Http\Controller\MemberController;
|
||||
use Lucent\Http\Controller\RecordController;
|
||||
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([
|
||||
@@ -17,8 +22,11 @@ Route::group([
|
||||
'prefix' => "lucent"
|
||||
], function () {
|
||||
|
||||
|
||||
|
||||
Route::middleware(['lucent.guest'])->group(function () {
|
||||
Route::get('/', [AuthController::class, 'login']);
|
||||
|
||||
Route::get('/register', [AuthController::class, 'register']);
|
||||
Route::post('/register', [AuthController::class, 'postRegister']);
|
||||
Route::get('/login', [AuthController::class, 'login']);
|
||||
@@ -33,9 +41,9 @@ Route::group([
|
||||
Route::get('/profile', [AccountController::class, 'profile']);
|
||||
Route::post('/account/update-name', [AccountController::class, 'updateName']);
|
||||
Route::post('/account/update-email', [AccountController::class, 'updateEmail']);
|
||||
Route::get('/build-report', [BuildController::class, 'report']);
|
||||
Route::get('/build-report-source', [BuildController::class, 'reportSource']);
|
||||
Route::post('/build', [BuildController::class, 'build']);
|
||||
Route::get('/command-report/{signature}', [BuildController::class, 'report']);
|
||||
Route::get('/command-report-source/{signature}', [BuildController::class, 'reportSource']);
|
||||
Route::post('/command/{signature}', [BuildController::class, 'build']);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -9,10 +9,13 @@ use Illuminate\Support\ServiceProvider;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Lucent\Channel\ChannelService;
|
||||
use Lucent\Commands\CompileSchemas;
|
||||
use Lucent\Commands\GenerateCollectionSchema;
|
||||
use Lucent\Commands\GenerateFileSchema;
|
||||
use Lucent\Commands\LiveLink;
|
||||
use Lucent\Commands\RebuildThumbnails;
|
||||
use Lucent\Commands\RemoveOrphanEdges;
|
||||
use Lucent\Event\Dispatcher;
|
||||
use Lucent\Commands\SetupDatabase;
|
||||
use Lucent\Commands\UpgradeFiles122;
|
||||
use Lucent\File\FileService;
|
||||
use Lucent\File\ImageService;
|
||||
use Lucent\Query\DatabaseGraph\DatabaseGraph;
|
||||
@@ -30,22 +33,20 @@ class LucentServiceProvider extends ServiceProvider
|
||||
return ChannelService::fromConfig();
|
||||
});
|
||||
|
||||
$this->app->singleton(Dispatcher::class, function () {
|
||||
return new Dispatcher();
|
||||
});
|
||||
|
||||
$this->app->bind(ImageManager::class, function () {
|
||||
return new ImageManager(['driver' => 'imagick']);
|
||||
});
|
||||
|
||||
|
||||
|
||||
$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(),
|
||||
"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/api.php');
|
||||
|
||||
$this->loadMigrationsFrom(__DIR__ . '/Database/migrations');
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
CompileSchemas::class,
|
||||
RebuildThumbnails::class,
|
||||
LiveLink::class,
|
||||
RemoveOrphanEdges::class,
|
||||
SetupDatabase::class,
|
||||
GenerateCollectionSchema::class,
|
||||
GenerateFileSchema::class,
|
||||
UpgradeFiles122::class,
|
||||
]);
|
||||
}
|
||||
|
||||
View::share('manifest', $manifest);
|
||||
View::share('image', app()->make(ImageService::class));
|
||||
View::share('file', app()->make(FileService::class));
|
||||
Blade::anonymousComponentPath(__DIR__ . '../front/views/components', "lucent");
|
||||
|
||||
$this->publishes([
|
||||
__DIR__ . '/Config/main.php' => config_path('lucent.php'),
|
||||
]);
|
||||
],"lucent-config");
|
||||
|
||||
$this->publishes([
|
||||
__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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
$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
|
||||
{
|
||||
$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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
$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
|
||||
{
|
||||
$value = trim($this->argument->value);
|
||||
if (is_numeric($value)) {
|
||||
return str_contains($value, ".") ? floatval($value) : intval($value);
|
||||
}
|
||||
return $value;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query\BuilderConverter;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Lucent\Query\Filter\Argument;
|
||||
|
||||
readonly class NotEquals implements IBuilderConverter
|
||||
{
|
||||
|
||||
public function __construct(private Argument $argument)
|
||||
{
|
||||
}
|
||||
|
||||
public function toAndQueryBuilder(Builder $builder): Builder
|
||||
{
|
||||
return $builder->whereNot($this->argument->field, $this->formatValue());
|
||||
}
|
||||
|
||||
public function toOrQueryBuilder(Builder $builder): Builder
|
||||
{
|
||||
return $builder->orWhereNot($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 NotEqualsFalse implements IBuilderConverter
|
||||
{
|
||||
|
||||
public function __construct(private Argument $argument)
|
||||
{
|
||||
}
|
||||
|
||||
public function toAndQueryBuilder(Builder $builder): Builder
|
||||
{
|
||||
return $builder->whereNot($this->argument->field, false);
|
||||
}
|
||||
|
||||
public function toOrQueryBuilder(Builder $builder): Builder
|
||||
{
|
||||
return $builder->orWhereNot($this->argument->field, false);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query\BuilderConverter;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Lucent\Query\Filter\Argument;
|
||||
|
||||
readonly class NotEqualsNumber implements IBuilderConverter
|
||||
{
|
||||
|
||||
public function __construct(private Argument $argument)
|
||||
{
|
||||
}
|
||||
|
||||
public function toAndQueryBuilder(Builder $builder): Builder
|
||||
{
|
||||
return $builder->whereNot($this->argument->field, $this->formatValue());
|
||||
}
|
||||
|
||||
public function toOrQueryBuilder(Builder $builder): Builder
|
||||
{
|
||||
return $builder->orWhereNot($this->argument->field, $this->formatValue());
|
||||
}
|
||||
|
||||
private function formatValue(): int|float
|
||||
{
|
||||
$value = trim($this->argument->value);
|
||||
return str_contains($value, ".") ? floatval($value) : intval($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Lucent\Query\BuilderConverter;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Lucent\Query\Filter\Argument;
|
||||
|
||||
readonly class NotEqualsTrue implements IBuilderConverter
|
||||
{
|
||||
|
||||
public function __construct(private Argument $argument)
|
||||
{
|
||||
}
|
||||
|
||||
public function toAndQueryBuilder(Builder $builder): Builder
|
||||
{
|
||||
return $builder->whereNot($this->argument->field, true);
|
||||
}
|
||||
|
||||
public function toOrQueryBuilder(Builder $builder): Builder
|
||||
{
|
||||
return $builder->orWhereNot($this->argument->field, true);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user