Compare commits

..

17 Commits

Author SHA1 Message Date
lexx 4d2cafdf11 stuff 2024-07-22 18:00:58 +03:00
lexx e9c2e82bc3 who knows 2024-04-03 16:25:59 +03:00
lexx 49c1d5efd0 edge schema validation 2024-03-30 18:02:17 +02:00
lexx 0e19c38f23 query rewrite 2024-03-30 13:42:38 +02:00
lexx b4521e92b8 lexorank 2024-03-26 12:56:14 +02:00
lexx 2e95fca8ad edit form refactor 2024-03-26 01:58:05 +02:00
lexx 02224eb580 refactor edit and edges 2024-03-25 21:26:21 +02:00
lexx e74e1e7956 index remove graph 2024-03-24 13:46:34 +02:00
lexx d824e52dce create doc 2024-03-23 22:26:02 +02:00
lexx 322c48b78b create document flow 2024-03-23 22:15:06 +02:00
lexx c649077e37 query and graph 2024-03-23 21:12:07 +02:00
lexx b8efa5f586 result 2024-03-23 12:12:13 +02:00
lexx 8526fd471f refactor fields 2024-03-21 22:33:41 +02:00
lexx bb77a37ff7 sidebar refactor 2024-03-20 01:57:17 +02:00
lexx 1f03eebd08 sidebar 2024-03-19 23:05:57 +02:00
lexx 137c338719 fixed json schema recursion issues 2024-03-14 23:01:21 +02:00
lexx 842bd71a18 Json Schema transformer is done
Form builder stuck on infinite recurrsion
2024-03-14 22:12:26 +02:00
441 changed files with 13940 additions and 13982 deletions
+3 -25
View File
@@ -9,8 +9,8 @@ include_toc: true
### Requirements ### Requirements
- PHP 8.3 - PHP 8.2
- Laravel 11 - Laravel 10
- Postgres or Sqlite database - Postgres or Sqlite database
- ImageMagick - ImageMagick
@@ -82,9 +82,7 @@ return [
### Database ### Database
The recommended database for small website is sqlite. But you can also use postresql The recommended database for small website is sqlite. But you can also use postresql
Make sure to delete the existing migration scripts in your database/migrations folder.
> [!CAUTION]
> Make sure to delete the existing migration scripts in your database/migrations folder.
Then run: Then run:
@@ -92,26 +90,6 @@ Then run:
php artisan migrate php artisan migrate
``` ```
### File Storage
You can use your local filesystem or s3 compatible storage. Lucent expects you to have a valid configuration inside ``config/filesystems.php``
example:
```php
return [
'disks' => [
'lucent' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => true,
],
],
];
```
### First user ### First user
To create your first user, head to your localhost:8000/lucent To create your first user, head to your localhost:8000/lucent
-32
View File
@@ -1,32 +0,0 @@
# 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,
],
```
+8 -6
View File
@@ -8,14 +8,15 @@
"ext-zip": "*", "ext-zip": "*",
"ext-sqlite3": "*", "ext-sqlite3": "*",
"ext-imagick": "*", "ext-imagick": "*",
"ext-pdo": "*", "php": "^8.2",
"php": "^8.3",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"intervention/image": "^2.7", "intervention/image": "^2.7",
"phpoption/phpoption": "^1.9",
"spatie/image-optimizer": "^1.6", "spatie/image-optimizer": "^1.6",
"staudenmeir/laravel-cte": "^1.0" "staudenmeir/laravel-cte": "^1.0",
"ext-pdo": "*",
"opis/json-schema": "^2.3",
"symfony/yaml": "^7.0",
"spatie/laravel-data": "^4.4"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^1.8", "phpstan/phpstan": "^1.8",
@@ -27,7 +28,8 @@
}, },
"files": [ "files": [
"src/Response.php", "src/Response.php",
"src/macros.php" "src/macros.php",
"src/File/Uploader.php"
] ]
}, },
"extra": { "extra": {
Generated
+2271 -413
View File
File diff suppressed because it is too large Load Diff
+11 -5
View File
@@ -24,8 +24,7 @@ There are 3 types of schemas
- **fields**: The list of your fields. Look the field reference for more - **fields**: The list of your fields. Look the field reference for more
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar. Default: false _optional_ - **isEntry**: If this schema is important, it will show be visible on the main the sidebar. Default: false _optional_
- **sortBy**: The default sorting in the content browser _optional_ - **sortBy**: The default sorting in the content browser _optional_
- **cardTitle**: Mustache code to customize the preview field _optional_ - **titleTemplate**: Mustache code to customize the preview field _optional_
- **cardImage**: Field name of image you want to use as a preview image _optional_
- **revisions**: How many revisions are going to be kept for each record _optional_ - **revisions**: How many revisions are going to be kept for each record _optional_
- **read**: Array of user groups that have read permissions _optional_ - **read**: Array of user groups that have read permissions _optional_
- **write**: Array of user groups that have write permissions _optional_ - **write**: Array of user groups that have write permissions _optional_
@@ -41,12 +40,20 @@ There are 3 types of schemas
- **fields**: The list of your fields. Look the field reference for more - **fields**: The list of your fields. Look the field reference for more
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar _optional_ - **isEntry**: If this schema is important, it will show be visible on the main the sidebar _optional_
- **sortBy**: The default sorting in the content browser _optional_ - **sortBy**: The default sorting in the content browser _optional_
- **cardTitle**: Mustache code to customize the preview field _optional_ - **titleTemplate**: Mustache code to customize the preview field _optional_
- **revisions**: How many revisions are going to be kept for each record _optional_ - **revisions**: How many revisions are going to be kept for each record _optional_
- **read**: Array of user groups that have read permissions _optional_ - **read**: Array of user groups that have read permissions _optional_
- **write**: Array of user groups that have write permissions _optional_ - **write**: Array of user groups that have write permissions _optional_
## Block Reference
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
- **label**: The friendly name of the schema
- **type**: The type of the collection. Should be "block"
- **fields**: The list of your fields. Look the field reference for more
A full Collection example without the fields: A full Collection example without the fields:
```json ```json
@@ -67,8 +74,7 @@ A full Collection example without the fields:
"SEO" "SEO"
], ],
"sortBy": "-_sys.createdAt", "sortBy": "-_sys.createdAt",
"schemaTitle": "{{name}} {{slug}}", "titleTemplate": "{{name}} {{slug}}",
"schemaImage": "cover",
"revisions": 15, "revisions": 15,
"read": [ "read": [
"admin", "admin",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6 -3
View File
@@ -1,11 +1,14 @@
{ {
"main.js": { "main.js": {
"file": "assets/main-BJyanQ7P.js", "file": "assets/main.7c3e8b7b.js",
"name": "main",
"src": "main.js", "src": "main.js",
"isEntry": true, "isEntry": true,
"css": [ "css": [
"assets/main-Dk7njt4m.css" "assets/main.587d6006.css"
] ]
},
"main.css": {
"file": "assets/main.587d6006.css",
"src": "main.css"
} }
} }
-3
View File
@@ -5,9 +5,6 @@
*/ */
import axios from "axios"; import axios from "axios";
import {loadHtmxFormsBehaviour} from "./htmx-form.js";
loadHtmxFormsBehaviour();
window.axios = axios; window.axios = axios;
export const axiosInstance = axios; export const axiosInstance = axios;
-20
View File
@@ -30,23 +30,3 @@ export function stripHtml(html = "") {
export function randomId(length = 10) { export function randomId(length = 10) {
return Math.random().toString(36).substring(2, length + 2); return Math.random().toString(36).substring(2, length + 2);
} }
export function clickOutside(node) {
const handleClick = event => {
if (node && !node.contains(event.target) && !event.defaultPrevented) {
node.dispatchEvent(
new CustomEvent('click_outside', node)
)
}
}
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
}
}
-24
View File
@@ -1,24 +0,0 @@
export function loadHtmxFormsBehaviour(){
document.querySelectorAll(".form").forEach(el => {
initHtmxForm(el);
})
}
function initHtmxForm(el){
el.addEventListener("htmx:responseError", (e) => {
el.querySelector(".form-errors").innerHTML = e.detail.xhr.response;
});
const formEl = el.querySelector("form");
if(!formEl.getAttribute("hx-redirect")){
return;
}
el.addEventListener("htmx:afterOnLoad", (e) => {
if(e.detail.successful){
return window.location.href = formEl.getAttribute("hx-redirect");
}
});
}
+11 -1
View File
@@ -2,13 +2,22 @@ import {axiosInstance} from "./bootstrap";
import "../sass/app.scss"; import "../sass/app.scss";
import Account from "./svelte/Account.svelte"; import Account from "./svelte/Account.svelte";
import Channel from "./svelte/Channel.svelte"; import Channel from "./svelte/Channel.svelte";
import * as bootstrap from "bootstrap";
import Mustache from "mustache"; import Mustache from "mustache";
import 'htmx.org';
Mustache.escape = function (value) { Mustache.escape = function (value) {
return value; return value;
}; };
function enableTooltipsAnywhere() {
// Enable tooltips everywhere
let tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]')
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
}
// Define all components // Define all components
const entryComponents = { const entryComponents = {
@@ -52,3 +61,4 @@ let loadSvelte = function () {
// document.addEventListener("turbo:load", loadSvelte); // document.addEventListener("turbo:load", loadSvelte);
document.addEventListener("DOMContentLoaded", loadSvelte); document.addEventListener("DOMContentLoaded", loadSvelte);
document.addEventListener("DOMContentLoaded", enableTooltipsAnywhere);
+2 -4
View File
@@ -3,7 +3,6 @@
import Login from "./account/Login.svelte"; import Login from "./account/Login.svelte";
import Verify from "./account/Verify.svelte"; import Verify from "./account/Verify.svelte";
import Profile from "./account/Profile.svelte"; import Profile from "./account/Profile.svelte";
import SetupIndex from "./setup/Index.svelte";
import {setContext} from "svelte"; import {setContext} from "svelte";
const components = { const components = {
@@ -11,7 +10,6 @@
login: Login, login: Login,
verify: Verify, verify: Verify,
profile: Profile, profile: Profile,
setup: SetupIndex,
}; };
export let title; export let title;
@@ -23,8 +21,8 @@
setContext("channel", channel); setContext("channel", channel);
setContext("user", user); setContext("user", user);
</script> </script>
<div style="text-align: center;background: var(--p20);padding: 20px;color: var(--p90)"> <div class="text-center">
<h1><a class="text-decoration-none" href="{channel.lucentUrl}">{channel.name ?? "Lucent Setup"}</a></h1> <h1><a class="text-decoration-none" href="{channel.lucentUrl}">{channel.name}</a></h1>
</div> </div>
<div> <div>
<svelte:component this={components[view]} {title} {...data}/> <svelte:component this={components[view]} {title} {...data}/>
+4 -14
View File
@@ -4,10 +4,9 @@
import RecordEdit from "./records/Edit.svelte"; import RecordEdit from "./records/Edit.svelte";
import ContentIndex from "./content/Index.svelte"; import ContentIndex from "./content/Index.svelte";
import {setContext} from "svelte"; import {setContext} from "svelte";
import Navbar from "./layout/Navbar.svelte"; import Navbar from "./Navbar.svelte";
import HomeIndex from "./home/Index.svelte"; import HomeIndex from "./home/Index.svelte";
import BuildReport from "./build/Report.svelte"; import BuildReport from "./build/Report.svelte";
import Header from "./layout/Header.svelte";
const components = { const components = {
members: Members, members: Members,
@@ -24,7 +23,7 @@
export let data; export let data;
// export let layout; // export let layout;
export let channel; export let channel;
export let sidebar;
export let axios; export let axios;
export let readableSchemas; export let readableSchemas;
@@ -36,16 +35,7 @@
</script> </script>
<Navbar {sidebar}/>
<div class="main-wrapper"> <svelte:component this={components[view]} {title} {...data}/>
<div class="sidebar-content">
<Navbar schema={data.schema}/>
</div>
<div class="main-content">
<Header />
<svelte:component this={components[view]} {title} {...data}/>
</div>
</div>
+47
View File
@@ -0,0 +1,47 @@
<script>
import Avatar from "./account/Avatar.svelte";
import {getContext} from "svelte";
export let sidebar;
const channel = getContext("channel");
const user = getContext("user");
let contentIsOpen = false;
</script>
<nav class="lx-nav">
<div>
<button on:click={(e) => contentIsOpen = true} class="btn btn-primary btn-sm d-xxl-none">« Content</button>
</div>
<div class="d-flex align-items-center ">
<!-- <div>-->
<!-- <form method="GET">-->
<!-- <input type="search" name="filter[search_regex]" placeholder="Search"-->
<!-- class="form-control" required/>-->
<!-- </form>-->
<!-- </div>-->
</div>
<div class="d-flex align-items-center ">
<a class="nav-item" href="{channel.lucentUrl}/members">Members</a>
{#if channel.generateCommand}
<a href="{channel.lucentUrl}/build-report" class="btn btn-outline-primary btn-sm d-">Build website</a>
{/if}
<a class="nav-item" href="{channel.lucentUrl}/profile">
<Avatar side="28" name={user.name}/>
</a>
</div>
</nav>
<div class="offcanvas offcanvas-start d-xxl-block show border-0 bg-primary-subtle " class:d-none={!contentIsOpen}
data-bs-scroll="true"
data-bs-backdrop="false"
tabindex="-1" aria-labelledby="offcanvasScrollingLabel">
<div class="offcanvas-body">
<button on:click={(e) => contentIsOpen = false} class="btn btn-primary btn-sm d-xxl-none mb-4">« close</button>
{@html sidebar}
</div>
</div>
+6 -6
View File
@@ -55,28 +55,28 @@
<Avatar name={user.name}/> <Avatar name={user.name}/>
</h3> </h3>
<form on:submit={saveName}> <form on:submit={saveName}>
<div class="input-group mb-5"> <div class="input-group mb-3">
<input <input
type="text" type="text"
bind:value={name} bind:value={name}
class="form-control mb-3" class="form-control"
placeholder="Name" placeholder="Name"
required required
/> />
<SpinnerButton label="Update Name"/> <SpinnerButton label="Update"/>
</div> </div>
</form> </form>
<form on:submit={saveEmail}> <form on:submit={saveEmail}>
<div class="input-group mb-5"> <div class="input-group mb-3">
<input <input
type="email" type="email"
bind:value={email} bind:value={email}
class="form-control mb-3" class="form-control"
placeholder="Email" placeholder="Email"
required required
/> />
<SpinnerButton label="Update Email"/> <SpinnerButton label="Update"/>
</div> </div>
</form> </form>
@@ -1,52 +0,0 @@
<script>
import Selectlist from "./Selectlist.svelte";
import Icon from "../common/Icon.svelte";
let searchEl;
let search;
export let value;
export let field;
function handleSelect(){
searchEl.focus();
searchEl.blur()
}
</script>
<div class="autocomplete">
<input
type="search"
bind:value={search}
bind:this={searchEl}
placeholder="Search for options"
autocomplete="off"
/>
<div class="autocomplete-results">
<Selectlist
{field}
bind:value
bind:search
on:selected={handleSelect}
/>
</div>
</div>
{#if value}
<div class="autocomplete-selected-value">
{#if Array.isArray(field.selectOptions)}
{value}
{:else}
{field.selectOptions[value]}
{/if}
<button
on:click|preventDefault={(e) => (value = "")}
type="button"
class="button-text"
aria-label="Close"
>
<Icon width={12} height={12} icon="close"></Icon>
</button>
</div>
{/if}
@@ -1,58 +0,0 @@
<script>
import Fuse from "fuse.js";
import {createEventDispatcher} from "svelte";
export let field;
export let value;
export let search = "";
const dispatch = createEventDispatcher();
function select(e, option) {
e.preventDefault();
value = option.value;
search = "";
dispatch("selected", {option: option})
}
function formatOptionsForSearch(listOptions) {
if (Array.isArray(listOptions)) {
return listOptions.map(value => {
return {
value: value,
label: value,
}
})
}
return Object.entries(listOptions).map(([k, v]) => {
return {
value: k,
label: v,
}
})
}
let formattedOptions = formatOptionsForSearch(field.selectOptions);
const fuse = new Fuse(formattedOptions, {
includeScore: false,
keys: ['value', 'label']
})
$: result = search === "" ? formattedOptions : fuse.search(search).map(resItem => resItem.item)
</script>
{#if result}
{#each result as suggestion (suggestion.value)}
<div
class="autocomplete-option"
role="button"
tabindex="0"
on:click={(e) => select(e, suggestion)}
on:keypress={(e) => select(e, suggestion)}
>
{suggestion.label}
</div>
{/each}
{/if}
+11 -23
View File
@@ -1,25 +1,22 @@
<script> <script>
import {getContext, onMount} from "svelte"; import {getContext, onMount} from "svelte";
import axios from "axios";
const channel = getContext("channel"); const channel = getContext("channel");
export let title; export let title;
export let command;
$: date = ""; $: date = "";
$: logs = ""; $: logs = "";
let anchorEl;
let inProgress = false; let inProgress = false;
function connect() { function connect() {
const eventSource = new EventSource(channel.lucentUrl + "/command-report-source/" + command.signature ); const eventSource = new EventSource(channel.lucentUrl + "/build-report-source");
eventSource.onmessage = function (event) { eventSource.onmessage = function (event) {
inProgress = true; inProgress = true;
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
date = data.date; date = data.date;
logs = data.logs; logs = data.logs;
anchorEl.scrollIntoView()
} }
eventSource.onerror = (e) => { eventSource.onerror = (e) => {
console.log(e) console.log(e)
@@ -31,7 +28,8 @@
function buildWebsite(e) { function buildWebsite(e) {
e.preventDefault(); e.preventDefault();
inProgress = true; inProgress = true;
axios.post(channel.lucentUrl + "/command/" + command.signature).then(response => {
axios.post(channel.lucentUrl + "/build").then(response => {
connect() connect()
}) })
@@ -43,41 +41,31 @@
</script> </script>
<div class="common-wrapper"> <div class="wrapper-tiny transparent mb-5">
<div class="lx-card mt-5"> <div class="lx-card mt-5">
<h3 class="header-small mb-5">{title}</h3> <h3 class="header-small mb-5">{title}</h3>
<button on:click={buildWebsite} class="button primary mb-3" disabled={inProgress}>Start <button on:click={buildWebsite} class="btn btn-outline-primary btn-sm mb-3" disabled={inProgress}>Start Build
</button> </button>
<div class="mb-3"> <div class="mb-3">
{#if inProgress} {#if inProgress}
<span class="badge text-bg-warning"> <span class="badge text-bg-warning">
Action in progress Build in progress
</span> </span>
{/if} {/if}
{#if !inProgress && logs} {#if !inProgress && logs}
<span class="badge text-bg-info"> <span class="badge text-bg-info">
Action completed Build completed
</span> </span>
{/if} {/if}
</div> </div>
<pre class="logs">{logs} <pre>{logs}</pre>
<div bind:this={anchorEl}>&nbsp;</div>
</pre>
</div> </div>
</div> </div>
<style>
.logs{
max-height: 70vh;
overflow: scroll;
background: var(--p90);
color: var(--p10);
padding: 10px;
}
</style>
-14
View File
@@ -1,14 +0,0 @@
<script>
let checkboxEl = null;
export let indeterminate = false;
export let value;
export let checked = false;
</script>
<div class="checkbox-wrapper">
<input bind:this={checkboxEl} on:change id="c1-13" type="checkbox" {value} {indeterminate} checked={checked}/>
</div>
+18 -28
View File
@@ -1,33 +1,23 @@
<script> <script>
import {clickOutside} from "../../helpers.js";
let dropdownMenu;
export let orientation = "left";
export function open() {
dropdownMenu.classList.remove("hide")
}
export function close() {
dropdownMenu.classList.add("hide")
}
function handleClickOutside() {
dropdownMenu.classList.add("hide")
}
export let width = "300";
let dropdownMenu;
export function hide(){
dropdownMenu.classList.remove("show")
}
</script> </script>
<div class="dropdown">
<button
class="button dropdown-button"
type="button"
on:click={open}
aria-expanded="false"
>
<slot name="button">Dropdown</slot>
</button>
<div bind:this={dropdownMenu} class="dropdown-menu hide orientation-{orientation}" use:clickOutside on:click_outside={handleClickOutside}>
<slot/>
</div>
<button
class="btn btn-sm btn-outline-primary dropdown-toggle d-flex align-items-center"
type="button"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false"
>
<slot name="button">Dropdown</slot>
</button>
<div bind:this={dropdownMenu} class="dropdown-menu" style="width:{width}px;">
<slot/>
</div> </div>
+2 -3
View File
@@ -3,8 +3,7 @@
</script> </script>
{#if message} {#if message}
<div class="notice notice-error" role="alert"> <div class="alert alert-danger" role="alert">
<div class="title">Submission Errors</div> {message}
<div class="content"> {message}</div>
</div> </div>
{/if} {/if}
-24
View File
@@ -105,29 +105,7 @@
path: '<path d="M438.6 105.4C451.1 117.9 451.1 138.1 438.6 150.6L182.6 406.6C170.1 419.1 149.9 419.1 137.4 406.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4C21.87 220.9 42.13 220.9 54.63 233.4L159.1 338.7L393.4 105.4C405.9 92.88 426.1 92.88 438.6 105.4H438.6z"/>', path: '<path d="M438.6 105.4C451.1 117.9 451.1 138.1 438.6 150.6L182.6 406.6C170.1 419.1 149.9 419.1 137.4 406.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4C21.87 220.9 42.13 220.9 54.63 233.4L159.1 338.7L393.4 105.4C405.9 92.88 426.1 92.88 438.6 105.4H438.6z"/>',
viewBox: "0 0 448 512", viewBox: "0 0 448 512",
}, },
"close": {
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/>',
viewBox: "0 0 24 24",
},
"arrow-left": {
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 width = 16;
export let height = 16; export let height = 16;
export let icon = ""; export let icon = "";
@@ -147,7 +125,6 @@
role="presentation" role="presentation"
{stroke} {stroke}
{fill} {fill}
> >
{@html selectedIcon.path} {@html selectedIcon.path}
</svg> </svg>
@@ -155,6 +132,5 @@
<style> <style>
svg { svg {
vertical-align: text-top; vertical-align: text-top;
} }
</style> </style>
+33
View File
@@ -0,0 +1,33 @@
<script>
import {onDestroy, onMount} from "svelte";
import offcanvas from "bootstrap/js/src/offcanvas.js";
export let title = "";
let offCanvasEl;
let offCanvasInstance;
export function show() {
if(!offCanvasInstance){
offCanvasInstance = new offcanvas(offCanvasEl);
}
offCanvasInstance.show();
}
onMount(()=>{
offCanvasInstance = new offcanvas(offCanvasEl);
});
export function hide() {
offCanvasInstance.hide();
}
</script>
<div bind:this={offCanvasEl} class="offcanvas offcanvas-end" tabindex="-1"
aria-labelledby="offcanvasEditContent">
<div class="offcanvas-header">
<h5 class="offcanvas-title">{title}</h5>
<button type="button" on:click={hide} class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body" style="overflow: auto">
<slot/>
</div>
</div>
+1 -1
View File
@@ -3,7 +3,7 @@
export let disabled = false; export let disabled = false;
</script> </script>
<button type="submit" class="button secondary btn-spinner" {disabled}> <button type="submit" class="btn btn-primary btn-spinner" {disabled}>
<span <span
class="spinner-border spinner-border-sm" class="spinner-border spinner-border-sm"
role="status" role="status"
+15 -4
View File
@@ -12,10 +12,21 @@
</script> </script>
{#if isVisible} {#if isVisible}
<div class="notice notice-success" transition:fly={{ duration: 500 }} role="alert"> <div
<div class="title">Success</div> transition:fly={{ duration: 500 }}
<div class="content"> {message}</div> class="lx-alert text-white bg-success border-1 border rounded px-3 py-0 text-center"
role="alert"
>
{message}
</div> </div>
{/if} {/if}
<style>
.lx-alert {
position: fixed;
left: 50%;
transform: translateX(-50%);
top: 45px;
margin: 0 auto;
}
</style>
-9
View File
@@ -1,9 +0,0 @@
<script>
export let value;
export let checked = false;
</script>
<input type="checkbox" {value} on:change
class="switch" {checked}/>
@@ -0,0 +1,13 @@
<script>
import {uniqueId} from "lodash";
export let label = "";
let id = uniqueId();
</script>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id={id} checked>
<label class="form-check-label" for={id}>{label}</label>
</div>
@@ -35,47 +35,49 @@
} }
</script> </script>
<div style="display: flex;align-items: center; gap: 8px"> <div class="d-flex align-items-center mb-3">
<span class="me-2">{selected.length} records selected</span> <span class="me-2">{selected.length} records selected</span>
<div class="btn-group " role="group" aria-label="Basic example">
<button <button
on:click|preventDefault={(e) => changeStatus(e, "published")} on:click|preventDefault={(e) => changeStatus(e, "published")}
type="button" type="button"
class="button">Publish class="btn btn-sm btn-outline-primary">Publish
</button </button
> >
<button <button
on:click|preventDefault={(e) => changeStatus(e, "draft")} on:click|preventDefault={(e) => changeStatus(e, "draft")}
type="button" type="button"
class="button">Make Draft class="btn btn-sm btn-outline-primary">Make Draft
</button </button
> >
{#if filter["status_in"] === "trashed"} {#if filter["status_in"] === "trashed"}
<button <button
on:click|preventDefault={(e) => changeStatus(e, "published")} on:click|preventDefault={(e) => changeStatus(e, "published")}
type="button" type="button"
class="button">Publish class="btn btn-sm btn-outline-primary">Publish
</button </button
> >
{#if schema.hasDrafts} {#if schema.hasDrafts}
<button <button
on:click|preventDefault={(e) => changeStatus(e, "draft")} on:click|preventDefault={(e) => changeStatus(e, "draft")}
type="button" type="button"
class="button">Make Draft class="btn btn-sm btn-outline-primary">Make Draft
</button </button
> >
{/if} {/if}
<button <button
on:click|preventDefault={deleteRecords} on:click|preventDefault={deleteRecords}
type="button" type="button"
class="button">Delete forever class="btn btn-sm btn-outline-primary">Delete forever
</button </button
> >
{:else} {:else}
<button <button
type="button" type="button"
on:click|preventDefault={(e) => changeStatus(e, "trashed")} on:click|preventDefault={(e) => changeStatus(e, "trashed")}
class="button">Move to trash class="btn btn-sm btn-outline-primary">Move to trash
</button </button
> >
{/if} {/if}
</div>
</div> </div>
+11 -12
View File
@@ -1,11 +1,10 @@
<script> <script>
import {getContext} from "svelte"; import {getContext} from "svelte";
import Preview from "../files/Preview.svelte";
import {selectRecord} from "./functions/recordSelect.js"; import {selectRecord} from "./functions/recordSelect.js";
import Preview from "../newPreview/Preview.svelte";
const channel = getContext("channel"); const channel = getContext("channel");
export let schema;
export let records; export let records;
export let isWritable; export let isWritable;
export let selected = []; export let selected = [];
@@ -17,42 +16,42 @@
</script> </script>
<div class="row" style="max-width:1000px"> <div class="row" style="max-width:1000px">
{#each records as record (record.id)} {#each records as queryRecord (queryRecord.record.id)}
<div class="col-6 col-md-4"> <div class="col-6 col-md-4">
<div <div
class="file-wrapper rounded p-2 mb-4 bg-light" class="file-wrapper rounded p-2 mb-4 bg-light"
class:selected={selected.includes(record)} class:selected={selected.includes(queryRecord)}
> >
{#if isWritable} {#if isWritable}
<div class="form-check"> <div class="form-check">
<input <input
on:change={() => select(record)} on:change={() => select(queryRecord.record)}
class="form-check-input " class="form-check-input "
type="checkbox" type="checkbox"
checked={selected.find( checked={selected.find(
(r) => r.id === record.id (r) => r.id === queryRecord.record.id
)} )}
value={record} value={queryRecord}
/> />
</div> </div>
{/if} {/if}
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<Preview {record} size="medium"/> <Preview record={queryRecord.record} />
</div> </div>
<a <a
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/records/{queryRecord.record.id}"
title={record._file.path} title={queryRecord.record._file.path}
class="d-block text-center overflow-hidden text-nowrap my-2 " class="d-block text-center overflow-hidden text-nowrap my-2 "
style=" style="
text-overflow: ellipsis; text-overflow: ellipsis;
font-size: 13px; font-size: 13px;
color: #333; color: #333;
">{record._file.path}</a ">{queryRecord.record._file.path}</a
> >
<span <span
class="lx-small-text text-muted d-block text-center" class="lx-small-text text-muted d-block text-center"
>{record._file.mime}</span >{queryRecord.record._file.mime}</span
> >
</div> </div>
</div> </div>
+14 -7
View File
@@ -1,15 +1,16 @@
<script> <script>
import Tools from "./tools/Tools.svelte"; // import Tools from "./tools/Tools.svelte";
import Pagination from "./pagination/Pagination.svelte"; import Pagination from "./pagination/Pagination.svelte";
import ActionsOnSelected from "./ActionsOnSelected.svelte"; import ActionsOnSelected from "./ActionsOnSelected.svelte";
import Table from "./Table.svelte"; import Table from "./Table.svelte";
import {getContext} from "svelte"; import {getContext} from "svelte";
import Grid from "./Grid.svelte";
import Tools from "./tools/Tools.svelte";
const axios = getContext("axios"); const axios = getContext("axios");
export let schema; export let schema;
export let users; export let users;
export let records; export let records;
export let graph;
// export let visibleFields; // export let visibleFields;
export let systemFields; export let systemFields;
export let sortParam; export let sortParam;
@@ -38,7 +39,6 @@
limit = response.data.limit; limit = response.data.limit;
total = response.data.total; total = response.data.total;
modalUrl = response.data.modalUrl; modalUrl = response.data.modalUrl;
document.querySelector("dialog h3").scrollIntoView();
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
@@ -46,8 +46,8 @@
} }
</script> </script>
<div class=""> <div class="wrapper-large transparent ">
<div class="{inModal ? 'mt-0' : 'mt-5'}"> <div class="lx-card mb-4 mt-0">
<h3 class="header-normal mb-5 "> <h3 class="header-normal mb-5 ">
{schema.label} {schema.label}
</h3> </h3>
@@ -62,7 +62,6 @@
{sortField} {sortField}
{operators} {operators}
{filter} {filter}
{graph}
{inModal} {inModal}
{modalUrl} {modalUrl}
{isWritable} {isWritable}
@@ -70,9 +69,9 @@
/> />
{/if} {/if}
{#if schema.type === "collection"}
<Table <Table
{records} {records}
{graph}
{schema} {schema}
{sortParam} {sortParam}
{sortField} {sortField}
@@ -82,7 +81,15 @@
{isWritable} {isWritable}
bind:selected bind:selected
/> />
{:else}
<Grid
{records}
{schema}
{isWritable}
bind:selected
/>
{/if}
</div> </div>
<Pagination <Pagination
+12 -13
View File
@@ -7,8 +7,7 @@
export let schema; export let schema;
export let users; export let users;
export let graph; export let queryRecord;
export let record;
export let sortParam; export let sortParam;
export let sortField; export let sortField;
export let visibleColumns; export let visibleColumns;
@@ -20,40 +19,40 @@
class="field-ui-{field.info.name}" class="field-ui-{field.info.name}"
class:is-sort={field.name === sortField.name} class:is-sort={field.name === sortField.name}
> >
<RenderField {record} {schema} {graph} {field}/> <RenderField {queryRecord} {field}/>
</td> </td>
{/each} {/each}
{#if schema.visible?.includes("status")} {#if schema.visible.includes("status")}
<td <td
class="text-center" class="text-center"
class:is-sort={"-status" == sortParam || "status" == sortParam} class:is-sort={"-status" == sortParam || "status" == sortParam}
> >
<Status status={record.status}/> <Status status={queryRecord.record.status}/>
</td> </td>
{/if} {/if}
{#if schema.visible?.includes("_sys.createdBy")} {#if schema.visible.includes("_sys.createdBy")}
<td <td
class="text-center" class="text-center"
class:is-sort={"-_sys.createdBy" == sortParam || "_sys.createdBy" == sortParam} class:is-sort={"-_sys.createdBy" == sortParam || "_sys.createdBy" == sortParam}
> >
<Avatar name={usernameById(users, record._sys.createdBy)} side={24}/> <Avatar name={usernameById(users, queryRecord.record._sys.createdBy)} side={24}/>
</td> </td>
{/if} {/if}
{#if schema.visible?.includes("_sys.updatedBy")} {#if schema.visible.includes("_sys.updatedBy")}
<td <td
class="text-center" class="text-center"
class:is-sort={"-_sys.updatedBy" == sortParam || "_sys.updatedBy" == sortParam} class:is-sort={"-_sys.updatedBy" == sortParam || "_sys.updatedBy" == sortParam}
> >
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/> <Avatar name={usernameById(users, queryRecord.record._sys.updatedBy)} side={24}/>
</td> </td>
{/if} {/if}
{#if schema.visible?.includes("_sys.createdAt")} {#if schema.visible.includes("_sys.createdAt")}
<td class:is-sort={"-_sys.createdAt" == sortParam || "_sys.createdAt" == sortParam}> <td class:is-sort={"-_sys.createdAt" == sortParam || "_sys.createdAt" == sortParam}>
{friendlyDate(record._sys.createdAt)} {friendlyDate(queryRecord.record._sys.createdAt)}
</td> </td>
{/if} {/if}
{#if schema.visible?.includes("_sys.updatedAt")} {#if schema.visible.includes("_sys.updatedAt")}
<td class:is-sort={"-_sys.updatedAt" == sortParam || "_sys.updatedAt" == sortParam}> <td class:is-sort={"-_sys.updatedAt" == sortParam || "_sys.updatedAt" == sortParam}>
{friendlyDate(record._sys.updatedAt)} {friendlyDate(queryRecord.record._sys.updatedAt)}
</td> </td>
{/if} {/if}
+3 -7
View File
@@ -29,17 +29,13 @@
file: File, file: File,
}; };
export let field; export let field;
export let schema; export let queryRecord;
export let record;
export let graph;
</script> </script>
<svelte:component <svelte:component
this={renderElements[field.info.name]} this={renderElements[field.info.name]}
value={record.data[field.name]} value={queryRecord.record.data[field.name]}
{record} {queryRecord}
{graph}
{schema}
{field} {field}
/> />
+52 -78
View File
@@ -5,16 +5,12 @@
import {getContext} from "svelte"; import {getContext} from "svelte";
import Avatar from "../account/Avatar.svelte"; import Avatar from "../account/Avatar.svelte";
import {selectRecord, toggleAll} from "./functions/recordSelect.js"; import {selectRecord, toggleAll} from "./functions/recordSelect.js";
import Checkbox from "../common/Checkbox.svelte";
import Preview from "../files/Preview.svelte";
import {fileurl} from "../files/imageserver.js";
const channel = getContext("channel"); const channel = getContext("channel");
export let schema; export let schema;
export let users; export let users;
export let records; export let records;
export let graph;
export let systemFields; export let systemFields;
export let sortParam; export let sortParam;
export let sortField; export let sortField;
@@ -23,29 +19,31 @@
export let selected = []; export let selected = [];
function eventToggleAll(e) { function eventToggleAll(e) {
selected = toggleAll(e, records, selected) selected = toggleAll(e,records,selected)
} }
function select(record) { function select(record) {
selected = selectRecord(record, selected) selected = selectRecord(record, selected)
} }
$: visibleColumns = schema.fields.filter(c => schema.visible?.includes(c.name) ?? []) $: visibleColumns = schema.fields.filter(c => schema.visible.includes(c.name))
</script> </script>
<div class="table mt-5 "> <div class="lx-table rounded">
<table> <table class="">
<thead> <thead class="table-light">
<tr> <tr>
{#if isWritable} {#if isWritable}
<th> <th>
<Checkbox <input
value="" on:change|preventDefault={eventToggleAll}
on:change={eventToggleAll} indeterminate={selected.length > 0 &&
indeterminate={selected.length > 0 && selected.length < records.length} selected.length < records.length}
checked={selected.length === records.length} checked={selected.length == records.length}
> class="form-check-input"
</Checkbox> type="checkbox"
/>
</th> </th>
{/if} {/if}
@@ -55,92 +53,68 @@
class:is-sort={field.name === sortField.name} class:is-sort={field.name === sortField.name}
scope="col" scope="col"
title={field.help} title={field.help}
>{field.label}</th data-bs-toggle="tooltip"
data-bs-placement="top">{field.label}</th
> >
{/each} {/each}
{#each systemFields.filter(c => schema.visible?.includes(c.name)) as sysField} {#each systemFields.filter(c => schema.visible.includes(c.name)) as sysField}
<th class:is-sort={sysField.name === sortField.name}>{sysField.label}</th> <th>{sysField.label}</th>
{/each} {/each}
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each records as record (record.id)} {#each records as queryRecord (queryRecord.record.id)}
<tr> <tr>
<td class="title-td"> <td class="title-td">
<div <div
class="title-td-contents" class="title-td-contents d-inline-flex justify-content-between w-100 align-items-center"
> >
<div class="d-flex align-items-center ">
{#if isWritable} {#if isWritable}
<Checkbox <div class="form-check">
on:change={() => select(record)} <input
checked={selected.find((r) => r.id === record.id)} on:change={() => select(queryRecord.record)}
value={record} class="form-check-input "
> type="checkbox"
</Checkbox> checked={selected.find(
(r) => r.id === queryRecord.record.id
)}
value={queryRecord}
/>
</div>
{/if} {/if}
{#if record._file?.path}
<div class="file-table-row">
<Preview record={record} size={record._file?.width > 0 ? "medium" : "small"}/>
<a
class="me-2 text-decoration-none text-dark fs-6"
href="{channel.lucentUrl}/records/{queryRecord.record.id}"
target={inModal ? "_blank" : "_self"}
title={previewTitle(queryRecord.record)}
data-bs-toggle="tooltip" data-bs-placement="left"
>
{previewTitle(queryRecord.record)}
</a>
</div>
<div> <div>
{#if record.status === "draft"} <Avatar
<span style="text-transform: uppercase;font-size:10px">{record.status}</span> name={usernameById(
{/if} users,
<a queryRecord.record._sys.updatedBy
href="{channel.lucentUrl}/records/{record.id}" )}
target={inModal ? "_blank" : "_self"} side={24}
> />
{previewTitle(channel.schemas, record, graph)}
</a>
<span>{(record._file.size / 1024).toFixed(1)}kB</span>
{#if record._file.width > 0}
<span>{record._file.width + "x" + record._file.height}</span>
{/if}
<a
href="{fileurl(channel,record)}"
target="_blank"
>
Download
</a>
</div> </div>
</div>
{:else}
<a
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}
</div> </div>
</td> </td>
<RecordRow <RecordRow
{record} {queryRecord}
{graph}
{schema} {schema}
{visibleColumns} {visibleColumns}
{sortParam} {sortParam}
{sortField} {sortField}
{users} {users}
/> />
<td>
<Avatar
name={usernameById(
users,
record._sys.updatedBy
)}
side={24}
/>
</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
+5 -7
View File
@@ -1,18 +1,16 @@
<script> <script>
import Preview from "../../files/Preview.svelte"; import Preview from "../../files/Preview.svelte";
export let record; export let queryRecord;
export let field; export let field;
export let graph;
let filePreviews = queryRecord?._children[field.name];
let filePreviews = graph.edges?.filter((ed) => ed.field === field.name && ed.source === record.id)
.map((ed) => graph.records.find((r) => r.id === ed.target));
// if (edges[0]) {
// firstRecord = record._children.find((r) => r.data.id === edges[0].to);
// }
</script> </script>
<!-- {#if firstRecord}
<Preview record={firstRecord} size="tiny" />
{/if} -->
<div class="d-flex me-1"> <div class="d-flex me-1">
{#each filePreviews as file} {#each filePreviews as file}
<div class="me-1"> <div class="me-1">
@@ -1,25 +1,16 @@
<script> <script>
import PreviewCardSmall from "../../records/PreviewCardSmall.svelte"; import PreviewCardSmall from "../../records/PreviewCardSmall.svelte";
export let record; export let queryRecord;
export let field; export let field;
export let schemas; $: recordEdges = queryRecord?._children[field.name];
export let graph;
$: recordEdges =
graph.edges
?.filter((ed) => ed.field === field.name && ed.source === record.id)
.map((edge) => {
return graph.records.find((r) => r.id === edge.target);
})
.filter((record) => (!record ? false : true)) ?? [];
</script> </script>
<div class="references"> <div class="references">
{#each recordEdges as recordEdge} {#each recordEdges as recordEdge}
<span class="reference"> <span class="mr-3">
<PreviewCardSmall {schemas} {graph} record={recordEdge}/> <PreviewCardSmall record={recordEdge}/>
</span> </span>
{/each} {/each}
</div> </div>
@@ -28,8 +28,8 @@
</script> </script>
{#each pages as i} {#each pages as i}
<li class="page-item" class:active={currentPage === i}> <li class="page-item">
{#if currentPage === i} {#if currentPage == i}
<span class="page-link active">{i}</span> <span class="page-link active">{i}</span>
{:else} {:else}
<a class="page-link" on:click={(e) => goto(e, i)} href={url(i)} <a class="page-link" on:click={(e) => goto(e, i)} href={url(i)}
@@ -43,7 +43,7 @@
</script> </script>
<nav> <nav>
<ul class="pagination"> <ul class="pagination justify-content-center">
{#if totalPages > 1} {#if totalPages > 1}
<li class="page-item disabled" class:disabled={currentPage === 1}> <li class="page-item disabled" class:disabled={currentPage === 1}>
<a on:click={first} href="/" class="page-link"> First </a> <a on:click={first} href="/" class="page-link"> First </a>
@@ -69,7 +69,7 @@
{/if} {/if}
</ul> </ul>
</nav> </nav>
<p style="display: flex;justify-content: center; gap: 4px"> <p class="text-muted text-center">
Showing Showing
<span class="font-medium">{+skip + 1}</span> <span class="font-medium">{+skip + 1}</span>
to to
@@ -1,7 +1,6 @@
<script> <script>
import {createEventDispatcher, getContext} from "svelte"; import {createEventDispatcher, getContext} from "svelte";
import {previewTitle} from "../../records/Preview"; import {previewTitle} from "../../records/Preview";
import Icon from "../../common/Icon.svelte";
const channel = getContext("channel"); const channel = getContext("channel");
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -11,7 +10,8 @@
export let value; export let value;
export let inModal; export let inModal;
export let modalUrl; export let modalUrl;
export let graph; export let records
let filter = { let filter = {
label: "", label: "",
operator: "", operator: "",
@@ -24,9 +24,6 @@
extractLabel(schema, key), extractLabel(schema, key),
].reduce((mem, fn) => fn(mem), filter); ].reduce((mem, fn) => fn(mem), filter);
function extractOperator(key) { function extractOperator(key) {
return (filter) => { return (filter) => {
if (filter.isReference) { if (filter.isReference) {
@@ -54,14 +51,13 @@
} }
const filterRecord = extractFilterRecord(graph, value); const filterRecord = extractFilterRecord(records, value);
function extractFilterRecord(graph, value) {
function extractFilterRecord(records, value) {
if (!filter.isReference) { if (!filter.isReference) {
return null; return null;
} }
return graph.records.find(r => r.id === value); return records.find(r => r.id === value);
} }
function removeFilter(k) { function removeFilter(k) {
@@ -77,20 +73,30 @@
} }
</script> </script>
<span class="applied-filter"> <span class="applied-filter d-inline-block border border-primary rounded lx-small-text me-1 px-2 py-1">
<div class="d-flex align-items-center justify-content-center">
{#if filter.isReference && filterRecord} {#if filter.isReference && filterRecord}
{filter.label} is {previewTitle(channel.schemas, filterRecord)} {filter.label} is {previewTitle(filterRecord)}
{:else} {:else}
{filter.label} {operators.find((o) => o.name === filter.operator)?.symbol ?? ""} {operators.find((o) => o.name === filter.operator)?.hasValue ? value : ""} {filter.label} {operators.find((o) => o.name === filter.operator)?.symbol ?? ""} {value}
{/if} {/if}
<button <button
on:click|preventDefault={() => removeFilter(key)} on:click|preventDefault={() => removeFilter(key)}
type="button" type="button"
class="button-text" class="btn-close btn-close ms-1"
aria-label="Close" aria-label="Close"
><Icon width={12} height={12} icon="close"></Icon></button> />
</div>
</span> </span>
<style>
.applied-filter {
background-color: #fff;
line-height: 22px;
}
.applied-filter:hover {
opacity: .8;
background-color: #eee;
}
</style>
@@ -1,35 +0,0 @@
<script>
import {createEventDispatcher, getContext} from "svelte";
import Icon from "../../common/Icon.svelte";
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);
url.searchParams.set("skip", "0");
url.searchParams.delete("notlinked");
if (inModal) {
dispatch("refresh", url);
} else {
window.location.replace(url);
}
}
</script>
{#if url.searchParams.get("notlinked")}
<span class="applied-filter">
Not linked
<button
on:click|preventDefault={() => removeFilter()}
type="button"
class="button-text"
aria-label="Close"
><Icon width={12} height={12} icon="close"></Icon></button>
</span>
{/if}
+70 -162
View File
@@ -1,8 +1,8 @@
<script> <script>
import Icon from "../../common/Icon.svelte"; import Icon from "../../common/Icon.svelte";
import {createEventDispatcher} from "svelte"; import {createEventDispatcher} from "svelte";
import Dropdown from "../../common/Dropdown.svelte";
import FilterReferenceInput from "./FilterReferenceInput.svelte"; import FilterReferenceInput from "./FilterReferenceInput.svelte";
import Dropdown from "../../common/Dropdown.svelte";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let schema; export let schema;
@@ -14,10 +14,50 @@
let dropdown; let dropdown;
let search = ""; let search = "";
let systemFieldsFiltered = systemFields; let systemFieldsFiltered = systemFields;
if (schema.type === "collection") { if (schema.type == "collection") {
systemFieldsFiltered = systemFields.filter((f) => f.files === false); systemFieldsFiltered = systemFields.filter((f) => f.files === false);
} }
let filterableFields = [...schema.fields, ...systemFieldsFiltered].filter(
(f) => !["file", "json"].includes(f.info?.name ?? f.ui)
);
let selectedField;
let selectedInput = "";
$: operatorsFiltered = operators.filter(
(o) => o.uis.includes(selectedField?.info?.name) || o.uis[0] == "*"
);
$: selectedOperator = operatorsFiltered[0];
function addFilter(e) {
e.preventDefault();
let filterPrefix = "";
let filterKey;
if (schema.fields.find(f => f.name === selectedField.name)) {
if (selectedField.info.name == "reference" && selectedOperator.name == "eq") {
filterPrefix = "children." + selectedField.name + ".id";
filterKey = `filter[${filterPrefix}]`;
} else {
filterPrefix = "data.";
filterKey = `filter[${filterPrefix + selectedField.name}_${selectedOperator.name}]`;
}
}
const url = new URL(modalUrl ?? window.location.href);
url.searchParams.set("skip", "0");
url.searchParams.set(filterKey, selectedInput);
if (inModal) {
dispatch("refresh", url);
dropdown.hide()
} else {
window.location = url;
}
}
function submitSearch(e) { function submitSearch(e) {
e.preventDefault(); e.preventDefault();
let filterKeyValue = search.split("=")[0] ?? ""; let filterKeyValue = search.split("=")[0] ?? "";
@@ -39,195 +79,63 @@
} else { } else {
window.location.replace(url); window.location.replace(url);
} }
resetFilters();
} }
// New Start
let selectedInput = null;
let activeField = null;
let activeReference = null;
let activeOperator = null;
let activeMenu = "main";
let activeOperators = null;
let dataFields = [...schema.fields, ...systemFieldsFiltered].filter(
(f) => !["file", "json", "reference"].includes(f.info?.name ?? f.ui)
);
let referenceFields = [...schema.fields].filter(
(f) => ["reference"].includes(f.info?.name ?? f.ui)
);
function selectField(e, field) {
activeField = field;
activeOperators = operators.filter(
(o) => o.uis.includes(activeField?.info?.name) || o.uis[0] === "*"
);
}
function selectReference(e, field) {
activeReference = field;
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 = "";
let filterKey;
let selectedField = activeField ?? activeReference;
if (schema.fields.find(f => f.name === selectedField.name)) {
if (selectedField.info.name === "reference" && activeOperator.name === "eq") {
filterPrefix = "children." + selectedField.name + ".id";
filterKey = `filter[${filterPrefix}]`;
} else {
filterPrefix = "data.";
filterKey = `filter[${filterPrefix + selectedField.name}_${activeOperator.name}]`;
}
}
const url = new URL(modalUrl ?? window.location.href);
url.searchParams.set("skip", "0");
url.searchParams.set(filterKey, selectedInput);
if (inModal) {
dispatch("refresh", url);
dropdown.close()
} else {
window.location.href = url.toString();
}
resetFilters();
}
function resetFilters() {
activeField = null;
activeOperator = null;
activeMenu = "main";
activeReference = null;
}
</script> </script>
<div class="mx-2 d-flex align-items-center">
<div> <Dropdown bind:this={dropdown} width="300">
<Dropdown bind:this={dropdown}>
<div slot="button"> <div slot="button">
<Icon icon="filter"/> <Icon icon="filter"/>
<span class="ms-1">Filter</span> <span class="ms-1">Filter</span>
</div> </div>
<div class:hide={activeMenu !== "main"}>
<button class="dropdown-item button" on:click={e => activeMenu = "byField" }> <div class="px-3 py-1 d-flex align-items-center">
Filter by field <select bind:value={selectedField} class="form-select">
</button> {#each filterableFields as field}
<button class="dropdown-item button" on:click={e => activeMenu = "byReference" }> <option value={field}>{field.label}</option>
Filter by Reference {/each}
</button> </select>
<button class="dropdown-item button" on:click={e => activeMenu = "advanced" }>
Advanced filter
</button>
</div> </div>
<div class:hide={activeMenu !== "byField"}> <div class="px-3 py-1 d-flex align-items-center">
{#if !activeField} <select class="form-select" bind:value={selectedOperator}>
<button class="dropdown-item button" on:click={e => activeMenu = "main" }> {#each operatorsFiltered as operator}
<Icon icon="arrow-left"></Icon> <option value={operator}>{operator.label}</option>
Back
</button>
{#each dataFields as field}
<button class="dropdown-item button" on:click={e => selectField(e,field)}>
{field.label}
</button>
{/each} {/each}
{/if} </select>
</div>
{#if activeField && !activeOperator} <div class="px-3 py-1 d-flex align-items-center">
<button class="dropdown-item button" on:click={e => activeField = null }> {#if selectedField?.info?.name === "reference" && selectedOperator.name === "eq"}
<Icon icon="arrow-left"></Icon> <FilterReferenceInput field={selectedField} bind:value={selectedInput} on:addFilter={addFilter}/>
Back {:else}
</button>
<div class="selected-filter">field: {activeField.label}</div>
{#each activeOperators as operator}
<button class="dropdown-item button" on:click={e => selectOperator(e,operator)}>
{operator.label}
</button>
{/each}
{/if}
{#if activeField && activeOperator}
<button class="dropdown-item button" on:click={e => activeOperator = null }>
<Icon icon="arrow-left"></Icon>
Back
</button>
<div class="selected-filter">field: {activeField.label} operator: {activeOperator.label}</div>
<div class="filter-input">
<input <input
type="text" type="text"
class="form-control" class="form-control"
bind:value={selectedInput} bind:value={selectedInput}
/> />
{/if}
</div> </div>
<div class="px-3 py-1 d-flex align-items-center">
<button <button
on:click={applyFilter} on:click={addFilter}
class="button applied-filter" class="btn btn-outline-primary"
type="button" type="button"
> >
Add filter Add filter
</button> </button>
{/if}
</div> </div>
<div class:hide={activeMenu !== "byReference"}>
{#if !activeReference}
<button class="dropdown-item button" on:click={e => activeMenu = "main" }>
<Icon icon="arrow-left"></Icon>
Back
</button>
{#each referenceFields as field}
<button class="dropdown-item button" on:click={e => selectReference(e,field)}>
{field.label}
</button>
{/each}
{/if}
{#if activeReference}
<button class="dropdown-item button" on:click={e => activeReference = null }>
<Icon icon="arrow-left"></Icon>
Back
</button>
<div class="selected-filter">field: {activeReference.label}</div>
<div class="mt-2">
<FilterReferenceInput field={activeReference} bind:value={selectedInput}
on:addFilter={applyFilter}/>
</div>
{/if}
<hr/>
</div> <div><h6 class="dropdown-header">Advanced filters</h6></div>
<div class:hide={activeMenu !== "advanced"}>
<button class="dropdown-item button" on:click={e => activeMenu = "main" }>
<Icon icon="arrow-left"></Icon>
Back
</button>
<form on:submit={submitSearch}> <form on:submit={submitSearch}>
<div class="px-3 py-1 d-flex align-items-center">
<input <input
bind:value={search} bind:value={search}
type="search" type="search"
class="mb-2 mt-2" class="form-control"
placeholder="Advanced filters" placeholder="Advanced filters"
required required
/> />
<button class="button applied-filter">
Submit
</button>
</form>
</div> </div>
</form>
</Dropdown> </Dropdown>
</div> </div>
@@ -42,37 +42,35 @@
} }
</script> </script>
<div class="reference-tags">
<input <input
type="search" type="search"
on:keyup={updateResults} on:keyup={updateResults}
class="form-control dropdown-toggle"
bind:value={search} bind:value={search}
placeholder={"Search for "+field.label} placeholder={"Search for "+field.label}
data-bs-toggle="dropdown"
autocomplete="off" autocomplete="off"
/> />
<div class="reference-tags-results"> <div class="dropdown-menu w-100">
{#if searchOptions} {#if searchOptions}
{#each searchOptions as option (option.id)} {#each searchOptions as option (option.id)}
<div <div
class="reference-tags-option"
role="button"
tabindex="0"
on:click={(e) => apply(e, option)} on:click={(e) => apply(e, option)}
on:keypress={(e) => apply(e, option)} on:keypress={(e) => apply(e, option)}
> >
{previewTitle(channel.schemas, option)} <span class="dropdown-item">
{previewTitle( option)}
</span>
</div> </div>
{:else} {:else}
<div
class="start-typing">
Start typing... Start typing...
</div>
{/each} {/each}
{/if} {/if}
</div>
</div> </div>
+38 -29
View File
@@ -1,7 +1,6 @@
<script> <script>
import Icon from "../../common/Icon.svelte"; import Icon from "../../common/Icon.svelte";
import {createEventDispatcher} from "svelte"; import {createEventDispatcher} from "svelte";
import Dropdown from "../../common/Dropdown.svelte";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let schema; export let schema;
@@ -43,80 +42,90 @@
} }
</script> </script>
<div class=" ">
<Dropdown> <button
<div slot="button"> class="btn btn-sm btn-outline-primary dropdown-toggle d-flex align-items-center"
type="button"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false"
>
{#if sortParam.startsWith("-")} {#if sortParam.startsWith("-")}
<Icon icon="arrow-down-wide-short"/> <Icon icon="arrow-down-wide-short"/>
{:else} {:else}
<Icon icon="arrow-up-short-wide"/> <Icon icon="arrow-up-short-wide"/>
{/if} {/if}
<span class="ms-1">{sortField.label}</span> <span class="ms-1">{sortField.label}</span>
</div> </button>
<div> <div class="dropdown-menu" style="width:auto;max-width:800px;">
<div class="row">
{#each sortableFields as field} {#each sortableFields as field}
<div class="dropdown-item"> <div class="col-4 px-3 py-1 d-flex align-items-center">
<div class="btn-group w-100">
<button <button
on:click={(e) => sortAsc(e, field)} on:click={(e) => sortAsc(e, field)}
title="Sort Ascending" title="Sort Ascending"
class="button button-icon {field.name == sortField.name && !sortParam.startsWith("-") class="btn btn-sm {field.name == sortField.name && !sortParam.startsWith("-")
? 'active' ? 'btn-primary'
: ''} " : 'btn-outline-primary'} "
> >
<Icon icon="arrow-up-short-wide"/> <Icon icon="arrow-up-short-wide"/>
</button> </button>
<button <button
on:click={(e) => sortDesc(e, field)} on:click={(e) => sortDesc(e, field)}
title="Sort Descending" title="Sort Descending"
class="button button-icon {field.name == sortField.name && sortParam.startsWith("-") class="btn btn-sm {field.name == sortField.name && sortParam.startsWith("-")
? 'active' ? 'btn-primary'
: ''} " : 'btn-outline-primary'} "
> >
<Icon icon="arrow-down-wide-short"/> <Icon icon="arrow-down-wide-short"/>
</button> </button>
<button <button
title="Sort Ascending" title="Sort Ascending"
on:click={(e) => sortAsc(e, field)} on:click={(e) => sortAsc(e, field)}
class="button" class="btn btn-sm btn-outline-primary w-100 text-nowrap"
style="overflow: hidden;"
> >
{field.label} {field.label}
</button> </button>
</div> </div>
</div>
{/each} {/each}
<h6 class="dropdown-header">System</h6> </div>
<h6 class="dropdown-header px-0">System</h6>
<div class="row">
{#each systemFieldsFiltered as field} {#each systemFieldsFiltered as field}
<div class="dropdown-item"> <div class="col-4 px-3 py-1 d-flex align-items-center">
<div class="btn-group w-100">
<button <button
on:click={(e) => sortAsc(e, field)} on:click={(e) => sortAsc(e, field)}
title="Sort Ascending" title="Sort Ascending"
class="button button-icon {field.name == sortParam class="btn btn-sm {field.name == sortParam
? 'active' ? 'btn-primary'
: ''} " : 'btn-outline-primary'} "
> >
<Icon icon="arrow-up-short-wide"/> <Icon icon="arrow-up-short-wide"/>
</button> </button>
<button <button
on:click={(e) => sortDesc(e, field)} on:click={(e) => sortDesc(e, field)}
title="Sort Descending" title="Sort Descending"
class="button button-icon {'-' + field.name == sortParam class="btn btn-sm {'-' + field.name == sortParam
? 'active' ? 'btn-primary'
: ''} " : 'btn-outline-primary'} "
> >
<Icon icon="arrow-down-wide-short"/> <Icon icon="arrow-down-wide-short"/>
</button> </button>
<button <button
title="Sort Ascending" title="Sort Ascending"
on:click={(e) => sortAsc(e, field)} on:click={(e) => sortAsc(e, field)}
class="button" class="btn btn-sm btn-outline-primary w-100 text-nowrap"
style="overflow: hidden;"
> >
{field.label} {field.label}
</button> </button>
</div> </div>
</div>
{/each} {/each}
</div> </div>
</Dropdown> </div>
</div>
+31 -31
View File
@@ -4,9 +4,7 @@
import Icon from "../../common/Icon.svelte"; import Icon from "../../common/Icon.svelte";
import SortFields from "./SortFields.svelte"; import SortFields from "./SortFields.svelte";
import AppliedFilter from "./AppliedFilter.svelte"; import AppliedFilter from "./AppliedFilter.svelte";
import {createEventDispatcher, getContext} from "svelte"; import {getContext, createEventDispatcher} from "svelte";
import Dropdown from "../../common/Dropdown.svelte";
import AppliedFilterNotLinked from "./AppliedFilterNotLinked.svelte";
const channel = getContext("channel"); const channel = getContext("channel");
@@ -20,7 +18,6 @@
export let modalUrl; export let modalUrl;
export let isWritable; export let isWritable;
export let records; export let records;
export let graph;
export let systemFields = []; export let systemFields = [];
// export let visibleFields = []; // export let visibleFields = [];
@@ -39,7 +36,7 @@
if (inModal) { if (inModal) {
dispatch("refresh", url); dispatch("refresh", url);
} else { } else {
window.location = url; window.location.href = url;
} }
} }
@@ -49,8 +46,8 @@
} }
</script> </script>
<div class="toolbar"> <div class="mb-3 d-flex align-items-center justify-content-between">
<div class="toolbar-filters"> <div class=" d-flex align-items-center">
<SortFields <SortFields
{schema} {schema}
@@ -75,73 +72,77 @@
<form method="GET" on:submit={search}> <form method="GET" on:submit={search}>
<input type="search" name="filter[search_regex]" placeholder="Search" <input type="search" name="filter[search_regex]" placeholder="Search"
class="search" required> class="form-control" required>
</form> </form>
</div> </div>
<div style="display:flex;align-items: center;gap:4px"> <div class="d-flex align-items-center ">
{#if schema.type === "collection"} {#if schema.type === "collection"}
{#if !inModal && isWritable} {#if !inModal && isWritable}
<a <a
href="{channel.lucentUrl}/records/new?schema={schema.name}" href="{channel.lucentUrl}/records/new?schema={schema.name}"
class="button" class="btn btn-sm btn-primary"
> >
New Record New Record
</a> </a>
{/if} {/if}
{:else } {:else }
<div> <div class="d-inline-block ms-1">
<Uploader {schema} on:uploadComplete={uploadComplete}/> <Uploader {schema} on:uploadComplete={uploadComplete}/>
</div> </div>
{/if} {/if}
{#if !inModal} {#if !inModal}
<Dropdown orientation="right"> <div class="dropdown d-inline-block">
<div slot="button"> <button
class="btn btn-link btn-sm"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Icon icon="ellipsis-vertical"/> <Icon icon="ellipsis-vertical"/>
</div> </button>
<ul class="dropdown-menu">
{#if filter["status_in"] === "trashed"} {#if filter["status_in"] === "trashed"}
{#if isWritable} {#if isWritable}
<li>
<a <a
class="dropdown-item" class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash" href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
> >
Empty trash Empty trash
</a> </a>
</li>
{/if} {/if}
{:else} {:else}
<li>
<a <a
class="dropdown-item" class="dropdown-item"
href={csvUrl} href={csvUrl}
>Export to CSV</a >Export to CSV</a
> >
</li>
<li>
<a <a
class="dropdown-item" class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed" href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
>View trashed records</a >View trashed records</a
> >
<a </li>
class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}?notlinked=*"
>View unlinked records</a
>
{/if} {/if}
</Dropdown>
</ul>
</div>
{/if} {/if}
</div> </div>
</div> </div>
<div class="applied-filters"> {#if Object.entries(filter).length > 0}
<AppliedFilterNotLinked <div class=" d-flex mb-3">
{inModal}
{modalUrl}
on:refresh
></AppliedFilterNotLinked>
{#if Object.entries(filter).length > 0}
{#each Object.entries(filter) as [k, v]} {#each Object.entries(filter) as [k, v]}
<AppliedFilter <AppliedFilter
{schema} {schema}
@@ -150,11 +151,10 @@
value={v} value={v}
{inModal} {inModal}
{modalUrl} {modalUrl}
{graph} {records}
on:refresh on:refresh
/> />
{/each} {/each}
{/if} </div>
</div> {/if}
-101
View File
@@ -1,101 +0,0 @@
<script>
import {createEventDispatcher, getContext} from "svelte";
import Icon from "../common/Icon.svelte";
import Index from "../content/Index.svelte";
import axios from "axios";
let dialogEl;
const dispatch = createEventDispatcher();
const channel = getContext("channel");
$: data = {};
let selectedRecords = [];
// onMount(() => {
// load();
// });
export function close(e) {
if(e){
e.preventDefault();
}
dialogEl.close()
selectedRecords = [];
}
function load(schema) {
axios
.get(channel.lucentUrl + "/content/" + schema)
.then((response) => {
data = response.data;
})
.catch((error) => console.log(error));
}
function insert(e) {
e.preventDefault();
dispatch("insert", {
records: selectedRecords,
action: "insert",
schema: data.schema.name,
});
}
function replace(e) {
e.preventDefault();
dispatch("insert", {
records: selectedRecords,
action: "replace",
});
}
export function open(schema) {
dialogEl.showModal()
load(schema);
}
</script>
<dialog bind:this={dialogEl}>
{#if data.schema}
<div class="dialog-header">
<button
type="button"
class="button"
on:click={insert}
disabled={selectedRecords.length === 0}
>
Insert
</button>
<button
type="button"
class="button"
on:click={replace}
disabled={selectedRecords.length === 0}
>
Replace
</button>
{#if selectedRecords.length > 0}
<span class="">
{selectedRecords.length} records selected
</span>
{/if}
<button
on:click|preventDefault={close}
type="button"
class="button close"
aria-label="Close"
>
<Icon icon="close"></Icon>
</button>
</div>
<div class="dialog-body">
<Index {...data} bind:selected={selectedRecords}></Index>
</div>
{/if}
</dialog>
@@ -1,38 +0,0 @@
<script>
import Icon from "../common/Icon.svelte";
let dialogEl;
$: data = {};
export function close(e) {
if (e) {
e.preventDefault();
}
dialogEl.close()
}
export function open() {
dialogEl.showModal()
}
</script>
<dialog bind:this={dialogEl}>
<div class="dialog-header">
<button
on:click|preventDefault={close}
type="button"
class="button close"
aria-label="Close"
>
<Icon icon="close"></Icon>
</button>
</div>
<div class="dialog-body" style="min-width: 900px">
<slot/>
</div>
</dialog>
+54
View File
@@ -0,0 +1,54 @@
<script>
import {createEventDispatcher, getContext} from "svelte";
const dispatch = createEventDispatcher();
const channel = getContext("channel");
let isOpen = false;
export function open() {
isOpen = true;
}
</script>
<div
class="modal fade show"
tabindex="-1"
class:d-block={isOpen}
aria-modal="true"
role="dialog"
style="background: rgba(100,100,100,.6);"
>
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<button
on:click|preventDefault={(e) => (isOpen = false)}
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
/>
</div>
<div class="modal-body">
</div>
</div>
</div>
</div>
<style>
.modal-dialog {
width: auto;
max-width: 100%;
}
.modal-content {
margin: 40px auto;
width: auto;
height: 100%;
}
</style>
+4 -8
View File
@@ -1,13 +1,9 @@
export function sortByField(from, to, queryRecords, fieldName) {
export function sortByField(from, to, edges, fieldName, references) {
if (from === to) { if (from === to) {
return edges; return queryRecords;
} }
let referenceIds = references.map(r => r.id); let edgesTosort = queryRecords?.filter((qr) => qr.edge.field === fieldName && qr.edge.depth === 1 ) ?? [];
let edgesTosort = edges?.filter((ed) => ed.field === fieldName && ed.depth === 1 && referenceIds.includes(ed.target)) ?? []; let remainingEdge = queryRecords?.filter((qr) => !(qr.edge.field === fieldName && qr.edge.depth === 1)) ?? [];
let remainingEdge = edges?.filter((ed) => !(ed.field === fieldName && ed.depth === 1)) ?? [];
edgesTosort = array_move(edgesTosort,from, to); edgesTosort = array_move(edgesTosort,from, to);
return [...remainingEdge, ...edgesTosort]; return [...remainingEdge, ...edgesTosort];
+12 -21
View File
@@ -1,6 +1,6 @@
<script> <script>
import Icon from "../common/Icon.svelte"; import Icon from "../common/Icon.svelte";
import {imgurl} from "./imageserver.js"; import {imgurl} from "../files/imageserver";
import {getContext} from "svelte"; import {getContext} from "svelte";
export let record; export let record;
@@ -28,19 +28,19 @@
fontSize = "13"; fontSize = "13";
} }
</script> </script>
<div style="display: flex;align-items: center;gap: 5px;">
{#if record}
{#if record}
{#if record._file.mime.startsWith("image")} {#if record._file.mime.startsWith("image")}
<!-- href={imgurl(record)} --> <!-- href={imgurl(record)} -->
<a <a
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/records/{record.id}"
title={record._file.originalName} title={record._file.path}
class="d-flex align-items-center justify-content-center "
style="width:{imageSide}px;height:{imageSide}px" style="width:{imageSide}px;height:{imageSide}px"
> >
<img <img
class="rounded w-100" class="rounded w-100"
src={imgurl(channel,record)} src={imgurl(record)}
alt={record._file.path} alt={record._file.path}
/> />
</a> </a>
@@ -48,30 +48,21 @@
<a <a
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path} title={record._file.path}
class="file-preview-small" class="btn btn-outline-primary btn-sm d-flex align-items-center justify-content-center"
style="width:{imageSide}px;height:{imageSide}px" style="width:{imageSide}px;height:{imageSide}px"
> >
<Icon icon="file" width={fileSide} height={fileSide}/> <Icon icon="file" width={fileSide} height={fileSide}/>
<span class="ms-2" <span class="ms-2" style="font-size:{fontSize}px"
>.{record._file.path.split(".").pop().toLowerCase()}</span >.{record._file.path.split(".").pop()}</span
> >
</a> </a>
{/if} {/if}
{/if} {/if}
{#if showFilename} {#if showFilename}
<a <a
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path} title={record._file.path}
class="preview-file-filename lx-small-text text-decoration-none" class="preview-file-filename lx-small-text text-decoration-none"
>{record._file.path} </a >{record._file.path}</a
> >
{/if} {/if}
</div>
<style>
img{
border-radius: 12px;
padding: 4px;
}
</style>
+6 -5
View File
@@ -43,15 +43,16 @@
} }
</script> </script>
<fieldset class="upload-button" disabled={isLoading}> <fieldset disabled={isLoading}>
<label class="button primary btn-spinner "> <label class="btn btn-primary btn-sm btn-spinner ">
Upload file
<span <span
class="spinner-border spinner-border-sm" class="spinner-border spinner-border-sm"
role="status" role="status"
aria-hidden="true" aria-hidden="true"
/> >
Upload file <span class="visually-hidden">Loading...</span>
</span>
<input <input
on:input={upload} on:input={upload}
class="form-control" class="form-control"
+11 -26
View File
@@ -1,30 +1,15 @@
export function imgurl(channel, record) { import {getContext} from "svelte";
if (record._file.mime === "image/svg+xml") {
return fileurl(channel, record); export function imgurl(record) {
if(record._file.mime === "image/svg+xml"){
return fileurl(record);
} }
return channel.disks[record._file.disk] + `/thumbs/${record._file.path}`; const channel = getContext("channel")
return channel.filesUrl + `/thumbs/${record._file.path}`;
} }
export function fileurl(channel, record) { export function fileurl(record) {
return channel.disks[record._file.disk] + `/${record._file.path}`; const channel = getContext("channel")
} return channel.filesUrl + `/${record._file.path}`;
export function htmlurl(channel, record, preset) {
let html = "";
let url = fileurl(channel, record)
if (record._file.width > 0) {
let presetUrl = url;
if (preset) {
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}"/>`
} else {
html = `<a href="${url}">${record._file.originalName}</a>`
}
return html;
} }
+26
View File
@@ -0,0 +1,26 @@
<script>
import { uniqueId } from "lodash";
export let label;
export let name;
export let group;
export let value;
export let help;
let id = uniqueId();
</script>
<div class="form-check">
<input
class="form-check-input"
type="radio"
{value}
{name}
bind:group
{id}
/>
<label class="form-check-label" for={id}>
{label}
</label>
{#if help}
<span class="text-muted">{help}</span>
{/if}
</div>
+8 -5
View File
@@ -21,11 +21,12 @@
}); });
</script> </script>
<div class="wrapper-normal transparent">
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3> <h3 class="header-small mb-4 ">Latest Content changes</h3>
{#if records.length > 0} {#if records.length > 0}
<div class="lx-card mb-4">
<div class="table"> <div class="lx-table p-0">
<table class=""> <table class="">
<tbody> <tbody>
{#each records as record (record.id)} {#each records as record (record.id)}
@@ -36,6 +37,8 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
{/if} {/if}
</div>
+9 -9
View File
@@ -1,6 +1,7 @@
<script> <script>
import {formatDistanceToNow, parseJSON} from "date-fns"; import {formatDistanceToNow, parseJSON} from "date-fns";
import Avatar from "../account/Avatar.svelte"; import Avatar from "../account/Avatar.svelte";
import Status from "../records/Status.svelte";
import {previewTitle} from "../records/Preview"; import {previewTitle} from "../records/Preview";
import Preview from "../files/Preview.svelte"; import Preview from "../files/Preview.svelte";
import {usernameById} from "../account/users"; import {usernameById} from "../account/users";
@@ -18,30 +19,29 @@
</script> </script>
<td> <td>
<div class="row-name">
{#if record.status === "draft"}
<span class="status">DRAFT</span>
{/if}
{#if schema.type === "files"} {#if schema.type === "files"}
<Preview {record} size="tiny" showFilename={true}/> <Preview {record} size="tiny"/>
{:else} {:else}
<a <a
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/records/{record.id}"
class="text-decoration-none text-dark d-block"
> >
{previewTitle(channel.schemas, record, graph)} {previewTitle(channel.schemas, record, graph)}
</a> </a>
{/if} {/if}
</div>
</td> </td>
<td><a <td><a
class="text-decoration-none lx-small-text"
href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a
> >
</td> </td>
<td class="text-center">
<Status status={record.status}/>
</td>
<td> <td>
<div style="display: flex;gap: 14px"> <div class="d-flex">
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/> <Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
<div class="ms-2"> <div class="ms-2">
{frieldlyUpdatedAt} {frieldlyUpdatedAt}
-34
View File
@@ -1,34 +0,0 @@
<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.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">-->
<!-- <input type="search" name="filter[search_regex]" placeholder="Search"-->
<!-- class="form-control" required/>-->
<!-- </form>-->
<!-- </div>-->
<a href="{channel.lucentUrl}/profile">
<Avatar side="28" name={user.name}/>
</a>
</div>
-39
View File
@@ -1,39 +0,0 @@
<script>
import NavbarMenu from "./NavbarMenu.svelte";
import {getContext} from "svelte";
export let schema;
const channel = getContext("channel");
const readableSchemas = getContext("readableSchemas");
const fileSchemas = readableSchemas.filter((sc) => sc.type === "files");
const otherSchemas = readableSchemas.filter((sc) => !sc.isEntry && sc.type === "collection");
</script>
<div class="sidebar-top">
<a class="logo" href="{channel.lucentUrl}">{channel.name}</a>
<a class="nav-item" href="{channel.lucentUrl}/profile">
</a>
</div>
<div class="sidebar">
<NavbarMenu
title="Content"
schemas={ readableSchemas.filter((sc) => sc.isEntry)}
schema={schema}
expanded={true}
/>
<NavbarMenu
title="Files"
schemas={ fileSchemas}
schema={schema}
/>
<NavbarMenu
title="Other"
schemas={ otherSchemas}
schema={schema}
/>
</div>
-34
View File
@@ -1,34 +0,0 @@
<script>
import {getContext} from "svelte";
import Icon from "../common/Icon.svelte";
const channel = getContext("channel");
export let schemas;
export let title;
export let schema;
export let expanded = false;
if(schemas.find(s => s.name === schema?.name)){
expanded = true;
}
function toggleExpand(){
expanded = !expanded;
}
</script>
<button class="sidebar-header" tabindex="0" on:click={toggleExpand}>
{title}
{#if expanded}
<Icon icon="circle-chevron-up"></Icon>
{:else}
<Icon icon="circle-chevron-down"></Icon>
{/if}
</button>
{#if expanded}
{#each schemas as aschema}
<a class="sidebar-item" class:active={aschema.name === schema?.name}
aria-current="page"
href="{channel.lucentUrl}/content/{aschema.name}">{aschema.label}</a>
{/each}
{/if}
+4 -26
View File
@@ -1,10 +1,10 @@
<script> <script>
// https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387 // https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387
import {onDestroy, onMount} from "svelte"; import {onMount, onDestroy} from "svelte";
import {basicSetup, EditorView} from "codemirror"; import {basicSetup, EditorView} from "codemirror";
import {autocompletion, completionKeymap} from "@codemirror/autocomplete"; import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
import {Compartment, EditorState} from "@codemirror/state"; import {EditorState, Compartment} from "@codemirror/state";
import {keymap} from "@codemirror/view"; import {keymap} from "@codemirror/view";
import {indentWithTab} from "@codemirror/commands"; import {indentWithTab} from "@codemirror/commands";
import {markdown} from "@codemirror/lang-markdown"; import {markdown} from "@codemirror/lang-markdown";
@@ -15,29 +15,6 @@
export let value; export let value;
export let editable = true; export let editable = true;
export function insertMedia(info) {
let insertText = "";
if (info.record._file.width > 0) {
insertText = `![${info.record._file.originalName}](${info.url})`;
} else {
insertText = `[${info.record._file.originalName}](${info.originalUrl})`;
}
const cursor = codeMirrorView.state.selection.main.head;
const transaction = codeMirrorView.state.update({
changes: {
from: cursor,
insert: insertText,
},
// the next 2 lines will set the appropriate cursor position after inserting the new text.
selection: {anchor: cursor + 1},
scrollIntoView: true,
});
if (transaction) {
codeMirrorView.dispatch(transaction);
}
}
onMount(() => { onMount(() => {
let language = new Compartment(); let language = new Compartment();
let tabSize = new Compartment(); let tabSize = new Compartment();
@@ -74,6 +51,7 @@
}); });
}); });
onDestroy(() => { onDestroy(() => {
+31 -11
View File
@@ -1,39 +1,59 @@
<script> <script>
import Sortable from "sortablejs"; import Sortable from "sortablejs";
import {createEventDispatcher, onMount} from "svelte"; import { onMount, createEventDispatcher } from "svelte";
export let sortableClass = ""; export let sortableClass = "";
// export let handle; // export let handle;
export let isTable = false; export let isTable = false;
export let sortableInstance; export let sortableInstance = null;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let sortableContainer; let sortableContainer;
onMount(() => { onMount(() => {
let options = { let options = {
// handle: ".sortable-handle",
// draggable: ".quote-line-wrapper",
// filter: ".not-draggable", // Selectors that do not lead to dragging (String or Function)
// preventOnFilter: true,
animation: 150, // ms, animation speed moving items when sorting, `0` — without animation animation: 150, // ms, animation speed moving items when sorting, `0` — without animation
easing: "cubic-bezier(1, 0, 0, 1)", easing: "cubic-bezier(1, 0, 0, 1)",
direction: 'vertical',
onUpdate: function (/**Event*/ evt) { onUpdate: function (/**Event*/ evt) {
// reorder(evt.oldIndex,evt.newIndex);
// console.log(evt)
dispatch("update", { dispatch("update", {
source: evt.oldIndex, source: evt.oldIndex,
target: evt.newIndex, target: evt.newIndex,
}); });
} },
onMove(event) {
// if (event.related.className.indexOf("not-draggable") > -1) {
// return false;
// }
},
}; };
// if (handle) {
// options.handle = handle;
// }
sortableInstance = Sortable.create(sortableContainer, options); sortableInstance = Sortable.create(sortableContainer, options);
}); });
// function reorder(from, to) {
// let newList = JSON.parse(JSON.stringify(value));
// let fromElem = newList[from];
// newList.splice(from, 1);
// newList.splice(to, 0, fromElem);
// value = newList;
// dispatch("reordered", value);
// }
</script> </script>
{#if isTable} {#if isTable}
<tbody class="sortable-container {sortableClass}" bind:this={sortableContainer}> <tbody class="sortable-container {sortableClass}" bind:this={sortableContainer}>
<slot/> <slot />
</tbody> </tbody>
{:else} {:else}
<div class="sortable-container {sortableClass}" bind:this={sortableContainer}> <div class="sortable-container {sortableClass}" bind:this={sortableContainer}>
<slot/> <slot />
</div> </div>
{/if} {/if}
+4 -7
View File
@@ -6,8 +6,7 @@
import "tinymce/icons/default"; import "tinymce/icons/default";
import "tinymce/themes/silver"; import "tinymce/themes/silver";
import "tinymce/skins/ui/oxide/skin.css"; import "tinymce/skins/ui/oxide/skin.css";
import contentUiSkinCss from "tinymce/skins/ui/oxide/content.css?inline"; import contentUiSkinCss from "tinymce/skins/ui/oxide/content.css";
import customcss from "./tinymce.css?inline";
import "tinymce/plugins/link"; import "tinymce/plugins/link";
import "tinymce/plugins/code"; import "tinymce/plugins/code";
@@ -53,7 +52,7 @@
toolbar_sticky: true, toolbar_sticky: true,
skin: false, skin: false,
content_css: false, content_css: false,
content_style: contentUiSkinCss.toString() + customcss.toString(), content_style: contentUiSkinCss.toString(),
branding: false, branding: false,
inline: false, inline: false,
plugins: plugins, plugins: plugins,
@@ -68,7 +67,8 @@
browser_spellcheck: true, browser_spellcheck: true,
max_height: 600, max_height: 600,
// media_poster: false, // media_poster: false,
content_style:
"img {max-width: 100%;height: auto;",
setup: function (editor) { setup: function (editor) {
activeEditor = editor; activeEditor = editor;
@@ -100,9 +100,6 @@
tinymce.init({...config, ...additionalConfig}); tinymce.init({...config, ...additionalConfig});
}); });
export function insertMedia(info){
activeEditor.execCommand('InsertHTML', false, info.html);
}
</script> </script>
-174
View File
@@ -1,174 +0,0 @@
<script>
import {onDestroy, onMount} from 'svelte';
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';
import History from '@tiptap/extension-history';
import Italic from '@tiptap/extension-italic';
import ListItem from '@tiptap/extension-list-item';
import OrderedList from '@tiptap/extension-ordered-list';
import Strike from '@tiptap/extension-strike';
import Table from '@tiptap/extension-table';
import TableRow from '@tiptap/extension-table-row';
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;
export let value = "";
onMount(() => {
editor = new Editor({
element: element,
extensions: [
Document,
Paragraph,
Text,
Bold,
ListItem,
BulletList,
Code,
CodeBlock,
History,
Italic,
HardBreak,
OrderedList,
Strike,
Table,
TableRow,
TableCell,
TableHeader,
Underline,
Dropcursor,
Image,
Heading.configure({
levels: [1, 2, 3],
}),
Blockquote
],
content: value,
editable: true,
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor;
},
onUpdate: ({editor}) => {
value = editor.getHTML()
},
});
});
onDestroy(() => {
if (editor) {
editor.destroy();
}
});
export function insertMedia(info){
editor.chain().focus().setImage({ src: info.url }).run()
}
</script>
{#if editor}
<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"/>
-68
View File
@@ -1,68 +0,0 @@
<script>
import {onDestroy, onMount} from "svelte";
import Trix from "trix"
import "trix/dist/trix.css"
export let value = "";
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
Trix.config.blockAttributes.heading3 = {
tagName: 'h3',
terminal: true,
breakOnReturn: true,
group: false
}
// console.log(Trix.config)
</script>
<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>
-37
View File
@@ -1,37 +0,0 @@
.mce-content-body .img {
max-width: 100%;
height: auto;
}
.mce-content-body{
font-size: 16px;
line-height: 20px;
}
.mce-content-body p{
margin-bottom: 14px;
&:last-child{
margin-bottom: 0;
}
}
.mce-content-body ul {
padding: 0 0 0 16px;
list-style: none outside none;
}
.mce-content-body li::before {
content: "—";
opacity: .5;
font-size: 12px;
padding-right: 6px;
vertical-align: 10%;
}
.mce-content-body li {
list-style: none;
padding: 0;
}
@@ -2,7 +2,6 @@
import Avatar from "../account/Avatar.svelte"; import Avatar from "../account/Avatar.svelte";
import {fly} from "svelte/transition"; import {fly} from "svelte/transition";
import {createEventDispatcher} from "svelte"; import {createEventDispatcher} from "svelte";
import Dropdown from "../common/Dropdown.svelte";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let member; export let member;
@@ -36,41 +35,50 @@
<div <div
transition:fly={{ duration: 200 }} transition:fly={{ duration: 200 }}
class="member-item" class="d-flex justify-content-between align-items-center mb-3 "
> >
<div class="member-name status-{member.roles.includes('removed') ? 'removed' : 'active'}"> <div class="d-flex align-items-center status-{member.roles.includes('removed') ? 'removed' : 'active'}">
<Avatar name={member.name ?? "" } side={32}/> <Avatar name={member.name ?? "" } side={32}/>
<div class="ms-3 ">
<div> <div>
<div> <span class="fs-5">
{member.name} {member.name}
</span>
</div> </div>
<div>
{member.email} {member.email}
</div> </div>
</div> </div>
</div> <div>
<Dropdown orientation="right"> <div class="dropdown dropdown-center">
<div slot="button"> <button
class=" dropdown-toggle btn btn-light"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Roles Roles
</div> </button>
<div class="dropdown-menu">
<h6 class="dropdown-header">Remove role</h6> <h6 class="dropdown-header">Remove role</h6>
{#each roles as role} {#each roles as role}
{#if member.roles.includes(role)} {#if member.roles.includes(role)}
<button <button
class="dropdown-item button" class="dropdown-item text-capitalize"
on:click={(e) => removeFrom(e,role)} on:click={(e) => removeFrom(e,role)}
> >
{role} {role}
</button> </button>
{/if} {/if}
{/each} {/each}
<div>
<hr class="dropdown-divider">
</div>
<h6 class="dropdown-header">Add role</h6> <h6 class="dropdown-header">Add role</h6>
{#each roles as role} {#each roles as role}
{#if !member.roles.includes(role)} {#if !member.roles.includes(role)}
<button <button
class="dropdown-item button" class="dropdown-item text-capitalize"
on:click={(e) => addTo(e,role)} on:click={(e) => addTo(e,role)}
> >
{role} {role}
@@ -78,8 +86,10 @@
{/if} {/if}
{/each} {/each}
</Dropdown> </div>
</div>
</div>
</div> </div>
<style> <style>
.status-removed { .status-removed {
+11 -9
View File
@@ -2,9 +2,9 @@
import ErrorAlert from "../common/ErrorAlert.svelte"; import ErrorAlert from "../common/ErrorAlert.svelte";
import SuccessAlert from "../common/SuccessAlert.svelte"; import SuccessAlert from "../common/SuccessAlert.svelte";
import SpinnerButton from "../common/SpinnerButton.svelte"; import SpinnerButton from "../common/SpinnerButton.svelte";
import Radio from "../forms/Radio.svelte";
import MemberSettingsCard from "./MemberSettingsCard.svelte"; import MemberSettingsCard from "./MemberSettingsCard.svelte";
import {getContext} from "svelte"; import {getContext} from "svelte";
import axios from "axios";
const channel = getContext("channel"); const channel = getContext("channel");
@@ -60,7 +60,7 @@
} }
</script> </script>
<div class="common-wrapper"> <div class="wrapper-tiny transparent mb-5">
<div class="lx-card mt-5"> <div class="lx-card mt-5">
<h3 class="header-small mb-5">Invite people</h3> <h3 class="header-small mb-5">Invite people</h3>
<ErrorAlert message={errorMessage}/> <ErrorAlert message={errorMessage}/>
@@ -95,14 +95,14 @@
</div> </div>
<div class="me-3"> <div class="me-3">
<select bind:value={role}>
{#each channel.roles.filter((r) => r !== "removed") as arole} {#each channel.roles.filter((r) => r !== "removed") as arole}
<option <Radio
bind:group={role}
value={arole} value={arole}
name="role"
>{arole}</option> label={arole}
/>
{/each} {/each}
</select>
</div> </div>
<div class="mt-5 d-block text-center"> <div class="mt-5 d-block text-center">
@@ -111,8 +111,8 @@
</form> </form>
</div> </div>
<div class="member-list"> <div class="lx-card mt-3">
<h3 class="header-small mb-5 mt-5">Members</h3> <h3 class="header-small mb-5">Members</h3>
{#each users as user} {#each users as user}
<MemberSettingsCard <MemberSettingsCard
member={user} member={user}
@@ -121,5 +121,7 @@
on:reinvite={(e) => invite(e.detail.email, e.detail.role)} on:reinvite={(e) => invite(e.detail.email, e.detail.role)}
/> />
{/each} {/each}
</div> </div>
</div> </div>
+65
View File
@@ -0,0 +1,65 @@
<script>
import File from "./includes/File.svelte";
import {previewTitle} from "../records/Preview.js";
import {getContext} from "svelte";
import Status from "../records/Status.svelte";
const channel = getContext("channel");
export let record;
export let edge = null;
let schema = channel.schemas.find(s => s.name === record.schema);
let types = ["inline", "card"];
export let type = "inline";
if (!types.includes(type)) {
console.error("unknown preview type")
}
export let editable = false;
</script>
<div class="preview-card">
{#if edge?.data}
<div class="preview-card-edge">Edge Data</div>
{/if}
<div class="d-flex column-gap-3">
{#if record._file}
<div>
<File {record}/>
</div>
{/if}
<div class="d-flex flex-md-column " style="line-height: 22px">
<a class="text-decoration-none" target="_blank" href="{channel.lucentUrl}/records/{record.id}">{previewTitle(record)}</a>
<span class="d-flex gap-1 text-muted">
{#if record.status === "draft"}
<Status status={record.status}/>
{/if}
{schema.label}
</span>
</div>
</div>
</div>
<style>
.preview-card {
position: relative
}
.preview-card-edge {
position: absolute;
top: -28px;
background: #fff;
padding: 0px 5px;
border: 1px solid #ccc;
border-radius: 7px;
font-size: 14px;
}
</style>
<!--{#if record._file && type === "inline"}-->
<!--&lt;!&ndash; <FilePreviewInline {record} {edge} {editable}/>&ndash;&gt;-->
<!--{:else if record._file && type === "card"}-->
<!-- <FilePreviewCard {record} {edge} {editable}/>-->
<!--{:else if type === "inline"}-->
<!--&lt;!&ndash; <DocPreviewCard {record} {edge} {editable}/>&ndash;&gt;-->
<!--{:else if type === "card"}-->
<!--&lt;!&ndash; <DocPreviewCard {record} {edge} {editable}/>&ndash;&gt;-->
<!--{/if}-->
@@ -0,0 +1,60 @@
<script>
import {imgurl} from "../../files/imageserver.js";
import {getContext} from "svelte";
import Icon from "../../common/Icon.svelte";
const channel = getContext("channel");
export let record;
export let size = "tiny";
let imageSide;
let fileSide;
let fontSize;
if (size === "large") {
imageSide = 256;
fileSide = 32;
fontSize = "20";
} else if (size === "medium") {
imageSide = 128;
fileSide = 12;
fontSize = "17";
} else if (size === "small") {
imageSide = 64;
fileSide = 12;
fontSize = "15";
} else if (size === "tiny") {
imageSide = 42;
fileSide = 12;
fontSize = "13";
}
</script>
{#if record}
{#if record._file.mime.startsWith("image")}
<!-- href={imgurl(record)} -->
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path}
class="d-flex align-items-center justify-content-center "
style="width:{imageSide}px;height:{imageSide}px"
>
<img
class="rounded w-100"
src={imgurl(record)}
alt={record._file.path}
/>
</a>
{:else}
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path}
class="btn btn-outline-primary btn-sm d-flex align-items-center justify-content-center"
style="width:{imageSide}px;height:{imageSide}px"
>
<Icon icon="file" width={fileSide} height={fileSide}/>
<span class="ms-2" style="font-size:{fontSize}px"
>.{record._file.path.split(".").pop()}</span
>
</a>
{/if}
{/if}
@@ -1,7 +1,5 @@
<script> <script>
export let schema; export let schema;
export let isCreateMode;
export let active = ""; export let active = "";
let tabs = schema.groups?.map((group) => { let tabs = schema.groups?.map((group) => {
@@ -11,38 +9,22 @@
label: "Main", label: "Main",
name: "", name: "",
}; };
let graphTab = {
label: "Backlinks",
name: "_graph",
};
if (isCreateMode) {
tabs = [mainTab, ...tabs];
} else {
tabs = [mainTab, ...tabs, graphTab];
}
function showGraph(e) { tabs = [mainTab, ...tabs];
e.preventDefault();
active = "_graph";
}
function changeTab(e, tabName) { function changeTab(e, tabName) {
e.preventDefault(); e.preventDefault();
if (tabName == "_graph") {
showGraph(e);
} else {
active = tabName; active = tabName;
} }
}
</script> </script>
{#if tabs.length > 1} {#if tabs.length > 1}
<ul class="tabs"> <ul class="nav nav-pills mb-4 justify-content-center">
{#each tabs as tab} {#each tabs as tab}
<li class="tab"> <li class="nav-item">
<button <button
on:click={(e) => changeTab(e, tab.name)} on:click={(e) => changeTab(e, tab.name)}
class="button" class="nav-link"
class:active={active === tab.name} class:active={active === tab.name}
aria-current="page" aria-current="page"
> >
+40 -157
View File
@@ -1,131 +1,60 @@
<script> <script>
import {afterUpdate, getContext, onMount} from "svelte"; import {getContext} from "svelte";
import {isEqual} from "lodash"; import Manager from "./Manager.svelte";
import axios from "axios";
import EditHeader from "./header/EditHeader.svelte"
import FilePreview from "./FilePreview.svelte" import FilePreview from "./FilePreview.svelte"
import ContentTabs from "./header/ContentTabs.svelte" import Form from "./form/Form.svelte";
import FormField from "./FormField.svelte" import axios from "axios";
import Graph from "./Graph.svelte"
import Info from "./Info.svelte"
import ErrorAlert from "../common/ErrorAlert.svelte"
import Title from "./header/Title.svelte";
const channel = getContext("channel"); const channel = getContext("channel");
export let schema; export let schema;
export let record; export let record;
export let graph = { export let graph = [];
records: [], export let recordHistory;
edges: []
};
// export let recordHistory;
export let isCreateMode; export let isCreateMode;
// export let isWritable = false; // export let isWritable = false;
export let users; // export let users;
let originalContent;
let activeContentTab = "";
$: hasUnsavedData = false;
$: validationErrors = null; $: validationErrors = null;
$: errorMessage = validationErrors $: errorMessage = null;
? `Record submission failed. ${
Object.entries(validationErrors).length
} error(s)`
: null;
let activeFields = schema.fields.filter( let form;
(f) => f.name !== "id"
);
onMount(() => {
setOriginalContent();
});
function setOriginalContent() {
originalContent = {
data: JSON.parse(JSON.stringify(record.data)),
schema: record.schema,
status: record.status,
_sys: JSON.parse(JSON.stringify(record._sys)),
_file: JSON.parse(JSON.stringify(record._file)),
edges: JSON.parse(JSON.stringify(graph.edges)),
};
}
afterUpdate(() => {
hasUnsavedData = checkUnsavedData();
});
function beforeUnload(e) {
// Cancel the event as stated by the standard.
// e.preventDefault();
// console.log(hasUnsavedData);
if (hasUnsavedData) {
return (e.returnValue =
"You have unsaved changes. Are you sure you want to exit?");
}
// Chrome requires returnValue to be set.
// e.returnValue = "";
delete e["returnValue"];
// more compatibility
// return true;
return "...";
}
function checkUnsavedData() {
if (isCreateMode) {
return false;
}
return !isEqual(originalContent, {
data: record.data,
schema: record.schema,
status: record.status,
_sys: record._sys,
_file: record._file,
edges: graph.edges,
});
}
function save(e) { function save(e) {
e.preventDefault(); e.preventDefault();
let status = e.detail.status
console.log("SAVE: Attempt"); console.log("SAVE: Attempt");
validationErrors = null; validationErrors = null;
errorMessage = "";
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
if (!hasUnsavedData && !isCreateMode) {
resolve(null);
return;
}
if (!record) {
resolve(null);
return;
}
// remove trashed edges // remove trashed edges
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id); let replaceEdges = graph
.map((queryRecord) => queryRecord.edge)
.filter((edge) => !edge._isTrashed && edge.source === record.id);
axios axios
.post(channel.lucentUrl + "/records", { .post(channel.lucentUrl + "/records", {
record: record, schemaName: record.schema,
edges: graph.edges, updateEdges: true,
id: record.id,
data: record.data,
edges: replaceEdges,
status: status,
isCreateMode: isCreateMode, isCreateMode: isCreateMode,
}) })
.then(function (response) { .then(function (response) {
console.log("SAVE: SAVED"); console.log("SAVE: SAVED");
if (isCreateMode) { if (isCreateMode) {
window.location = channel.lucentUrl + "/records/" + record.id; window.location.href = channel.lucentUrl + "/records/" + record.id;
} else { } else {
record = response.data.records[0] ?? null; record = response.data.record ?? null;
if (!record) { if (!record) {
// means trashed // means trashed
hasUnsavedData = false;
window.location = channel.lucentUrl; window.location = channel.lucentUrl;
return; return;
} }
graph = response.data; graph = [...response.data.graph];
setOriginalContent(); form.setOriginalData();
} }
resolve(null); resolve(null);
@@ -137,82 +66,36 @@
errorMessage = error.response.data.error; errorMessage = error.response.data.error;
} else { } else {
validationErrors = error.response.data.error; validationErrors = error.response.data.error;
console.log(validationErrors) // console.log(validationErrors)
} }
} }
resolve(null); resolve(null);
// msgSuccess = null;
// msgError = error.response.data.error;
// submitted = false;
}); });
}); });
} }
</script> </script>
<svelte:window on:beforeunload={beforeUnload}/>
<div class="record-edit"> <div class="wrapper-normal transparent">
<div class="tools-header"> <Manager managerRecords={recordHistory} {graph}/>
<!-- <Manager managerRecords={recordHistory} {graph}/>-->
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab/>
{#if isCreateMode}
<button
class="button primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Create
</button>
{:else if hasUnsavedData}
<button
type="button"
class="button primary ms-2 btn btn-primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Save
</button>
{/if}
</div>
<Title {schema} {record} {isCreateMode}/>
<ErrorAlert message={errorMessage}/>
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
<ContentTabs
{schema}
{isCreateMode}
bind:active={activeContentTab}
/>
{#if !["_graph", "_info"].includes(activeContentTab)}
<FilePreview {record} {schema}/> <FilePreview {record} {schema}/>
{#each activeFields as field (field.name)}
{#if activeContentTab === field.group} <div class=" mt-4" style="margin-bottom:150px">
<FormField <Form
bind:data={record.data} bind:this={form}
bind:graph={graph} data={record.data}
{field} status={record.status}
bind:graph
{schema} {schema}
{record} {record}
{validationErrors}
{isCreateMode} {isCreateMode}
{errorMessage}
{validationErrors}
on:save={save}
/> />
{/if} <!-- <Graph {graph} {record}/>-->
{/each} <!-- <Info {record} {graph} {users} {schema}/>-->
{:else if activeContentTab === "_graph"}
<Graph {graph} {record}/>
{:else if activeContentTab === "_info"}
<Info {record} {graph} {users} {schema}/>
{/if}
</div> </div>
</div> </div>
+77
View File
@@ -0,0 +1,77 @@
<script>
import {getContext} from "svelte";
import Icon from "../common/Icon.svelte";
import {previewTitle} from "./Preview";
import axios from "axios";
const channel = getContext("channel");
export let schema;
export let record;
export let title;
export let isCreateMode;
function clone(e) {
e.preventDefault();
axios.post(channel.lucentUrl + "/records/clone/" + record.id).then(response => {
window.location.href = channel.lucentUrl + "/records/" + response.data.id;
}).catch(error => {
});
}
</script>
<h3 class="header-normal mb-0">
<a
class="text-muted d-block text-decoration-none fs-6 mb-1"
href="{channel.lucentUrl}/content/{schema.name}"
>{schema.label.toUpperCase()}</a
>
<span class="text-dark d-block">
{#if !isCreateMode}
{#if record}
{previewTitle(record)}
{:else}
{ title}
{/if}
{:else}
New Record
{/if}
</span>
{#if !isCreateMode && !!record}
<div class="dropdown d-inline-block">
<button
class="btn btn-link btn-sm"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Icon icon="ellipsis"/>
</button>
<div class="dropdown-menu">
<h6 class="dropdown-header">Record Actions</h6>
<a
class="dropdown-item"
href="{channel.lucentUrl}/records/new?schema={schema.name}"
>Create new</a
>
{#if !isCreateMode}
<a
class="dropdown-item"
on:click={clone}
href={channel.lucentUrl}
>
Clone
</a>
{/if}
<!-- <a-->
<!-- on:click|preventDefault={(e) =>-->
<!-- (activeContentTab = "_info")}-->
<!-- class="dropdown-item"-->
<!-- href="{channel.lucentUrl}">Revisions</a-->
<!-- >-->
</div>
</div>
{/if}
</h3>
+26 -21
View File
@@ -1,50 +1,55 @@
<script> <script>
import Preview from "../files/Preview.svelte"; import Preview from "../files/Preview.svelte";
import {fileurl} from "../files/imageserver" import {fileurl} from "../files/imageserver"
import {getContext} from "svelte"
const channel = getContext("channel");
export let record; export let record;
export let schema; export let schema;
</script> </script>
{#if schema.type === "files"} {#if schema.type === "files"}
<div class="record-edit-file-preview"> <div class="row mb-4">
<div> <div class="col" style="max-width:276px">
<Preview {record} size="large"/> <Preview {record} size="large"/>
</div> </div>
<div class="file-details"> <div class="col">
<ul class="list-group ">
<div class="file-details-item"> <li class="list-group-item border-primary">
<span class="text-muted">Filename</span> <span class="text-muted">Filename</span>
<span>{record._file.path}</span> <span>{record._file.path}</span>
</div> </li>
<div class="file-details-item"> <li class="list-group-item border-primary">
<span class="text-muted">Original name</span> <span class="text-muted">Original name</span>
<span>{record._file.originalName}</span> <span>{record._file.originalName}</span>
</div> </li>
<div class="file-details-item"> <li class="list-group-item border-primary">
<span class="text-muted">Mime type</span> <span class="text-muted">Mime type</span>
<span>{record._file.mime}</span> <span>{record._file.mime}</span>
</div> </li>
{#if record._file.width} {#if record._file.width}
<div class="file-details-item"> <li class="list-group-item border-primary">
<span class="text-muted">Dimensions</span> <span class="text-muted">Dimensions</span>
<span>{record._file.width}x{record._file.height}</span> <span>{record._file.width}x{record._file.height}</span>
</div> </li>
{/if} {/if}
<div class="file-details-item"> <li class="list-group-item border-primary">
<span class="text-muted">File size</span> <span class="text-muted">File size</span>
<span>{(record._file.size / 1024).toFixed(1)}kB</span> <span>{(record._file.size / 1024).toFixed(1)}kB</span>
</div> </li>
<div class="file-details-item"> <li class="list-group-item border-primary">
<span class="text-muted">Checksum</span> <span class="text-muted">Checksum</span>
<span>{record._file.checksum}</span> <span>{record._file.checksum}</span>
</div> </li>
<div class="file-details-item"> <li class="list-group-item border-primary">
<a class="button primary" target="_blank" style="display: inline-flex" href="{fileurl(channel,record)}">Download</a> <span class="text-muted">Download</span>
</div> <a href="{fileurl(record)}">{record._file.path}</a>
</li>
</ul>
</div> </div>
</div> </div>
{/if} {/if}
<style>
.list-group {
font-size: 14px;
}
</style>
+41 -44
View File
@@ -1,21 +1,23 @@
<script> <script>
import Text from "./elements/Text.svelte"; import Text from "./form/fields/Text.svelte";
import Slug from "./elements/Slug.svelte"; import Slug from "./form/fields/Slug.svelte";
import Reference from "./elements/Reference.svelte"; import Reference from "./elements/Reference.svelte";
import Color from "./elements/Color.svelte"; import ReferenceInline from "./elements/ReferenceInline.svelte";
import Checkbox from "./elements/Checkbox.svelte"; import Block from "./block/Block.svelte";
import Number from "./elements/Number.svelte"; import Color from "./form/fields/Color.svelte";
import Url from "./elements/Url.svelte"; import Checkbox from "./form/fields/Checkbox.svelte";
import Date from "./elements/Date.svelte"; import Number from "./form/fields/Number.svelte";
import UUID from "./elements/UUID.svelte"; import Date from "./form/fields/Date.svelte";
import File from "./elements/File.svelte"; import UUID from "./form/fields/UUID.svelte";
import Textarea from "./elements/Textarea.svelte"; import File from "./form/references/File.svelte";
import Datetime from "./elements/Datetime.svelte"; import Textarea from "./form/fields/Textarea.svelte";
import RichEditor from "./elements/RichEditor.svelte"; import Datetime from "./form/fields/Datetime.svelte";
import Json from "./elements/JSON.svelte"; import RichEditor from "./form/fields/RichEditor.svelte";
import Markdown from "./elements/Markdown.svelte"; import Json from "./form/fields/JSON.svelte";
import FieldHeader from "./elements/FieldHeader.svelte"; import Markdown from "./form/fields/Markdown.svelte";
import ReferenceTags from "./elements/ReferenceTags.svelte"; import FieldHeader from "./form/FieldHeader.svelte";
import ReferenceTable from "./elements/ReferenceTable.svelte";
import ReferenceTags from "./form/references/ReferenceTags.svelte";
const formElements = { const formElements = {
text: Text, text: Text,
@@ -25,7 +27,6 @@
color: Color, color: Color,
checkbox: Checkbox, checkbox: Checkbox,
number: Number, number: Number,
url: Url,
date: Date, date: Date,
datetime: Datetime, datetime: Datetime,
uuid: UUID, uuid: UUID,
@@ -44,9 +45,24 @@
const id = `field-${field.name}-${record.id}`; const id = `field-${field.name}-${record.id}`;
</script> </script>
<div class="editor-field"> <div class="card editor-field">
<FieldHeader {field} {id}/> <FieldHeader {field} {id}/>
{#if field.info.name === "reference" && field.layout === "tags"} {#if field.info.name === "reference" && field.layout === "inline"}
<ReferenceInline
bind:graph
{record}
{field}
{validationErrors}
/>
{:else if field.info.name === "reference" && field.layout === "table"}
<ReferenceTable
bind:graph
{id}
{record}
{field}
{validationErrors}
/>
{:else if field.info.name === "reference" && field.layout === "tags"}
<ReferenceTags <ReferenceTags
bind:graph bind:graph
{id} {id}
@@ -54,16 +70,17 @@
{field} {field}
{validationErrors} {validationErrors}
/> />
{:else if field.info.name === "reference"} {:else if ["reference","file"].includes(field.info.name)}
<Reference <File bind:graph {record} {field} />
{:else if field.info.name === "block"}
<Block
bind:graph bind:graph
{id} bind:value={data[field.name]}
{record} {record}
{id}
{field} {field}
{validationErrors} {validationErrors}
/> />
{:else if field.info.name === "file"}
<File bind:graph {record} {field} {validationErrors}/>
{:else if field.info.name === "text"} {:else if field.info.name === "text"}
<Text <Text
bind:value={data[field.name]} bind:value={data[field.name]}
@@ -88,26 +105,6 @@
{isCreateMode} {isCreateMode}
{id} {id}
/> />
{:else if field.info.name === "rich"}
<RichEditor
bind:value={data[field.name]}
{schema}
{field}
{validationErrors}
{isCreateMode}
bind:graph
{record}
/>
{:else if field.info.name === "markdown"}
<Markdown
bind:value={data[field.name]}
{schema}
{field}
{validationErrors}
{isCreateMode}
bind:graph
{record}
/>
{:else} {:else}
<svelte:component <svelte:component
this={formElement} this={formElement}
+119 -27
View File
@@ -1,9 +1,15 @@
<script> <script>
import PreviewCardSmall from "./PreviewCardSmall.svelte";
import PreviewCard from "./PreviewCard.svelte";
import Icon from "../common/Icon.svelte";
import Preview from "../files/Preview.svelte";
import {getContext} from "svelte"; import {getContext} from "svelte";
import PreviewReference from "./previews/PreviewReference.svelte"; import {uniqBy} from "lodash";
const channel = getContext("channel"); const channel = getContext("channel");
export let graph; export let graph;
export let record;
function findEdgeField(schema, edgeField){ function findEdgeField(schema, edgeField){
if(edgeField.includes(":")){ if(edgeField.includes(":")){
let edgeFieldAr = edgeField.split(":"); let edgeFieldAr = edgeField.split(":");
@@ -12,35 +18,121 @@
return schema.fields.find((f) => f.name === edgeField); return schema.fields.find((f) => f.name === edgeField);
} }
let backlinks = graph.parentEdges.map(edge => { let parentEdgesByField = graph.parentEdges
.filter((edge) => edge.source !== record.id && edge.depth === 1)
.reduce((carry, edge) => {
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema); let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
let edgeField = findEdgeField(schema,edge.field); let edgeField = findEdgeField(schema,edge.field);
if(!edgeField){ let schemaField = edge.sourceSchema + 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}
<div style="margin: 0 0 15px;position: relative;">
<span style="
font-size: 14px;
margin-bottom: 5px;
display: block;
" let arecord = graph.records.find((n) => {
>In <i>{backlink.field}</i> of</span> return n.id === edge.source;
<PreviewReference });
record={backlink.record} if (!carry[schemaField]) {
hasDelete={false} carry[schemaField] = {
{graph} field: edgeField,
schema: schema,
nodes: [],
};
}
if (arecord) {
carry[schemaField].nodes.push(arecord);
carry[schemaField].nodes = uniqBy(carry[schemaField].nodes,"id");
}
return carry;
}, {});
let childrenEdgesByField = graph.edges
.filter((edge) => edge.source === record.id && edge.depth === 1)
.reduce((carry, edge) => {
let schema = channel.schemas.find((s) => s.name === record.schema);
let edgeField = findEdgeField(schema,edge.field);
// let schemaField = edge.targetSchema + edgeField;
let schemaField = edgeField.name + edge.targetSchema;
if (!carry[schemaField]) {
carry[schemaField] = {
field: edgeField,
nodes: [],
};
}
let arecord = graph.records.find((n) => {
return n.id === edge.target;
});
if (arecord) {
carry[schemaField].nodes.push(arecord);
carry[schemaField].nodes = uniqBy(carry[schemaField].nodes,"id");
}
return carry;
}, {});
</script>
{#each Object.entries(parentEdgesByField) as [fieldName, fieldData]}
<div class="lx-card mt-3">
<div class="text-center mb-3 d-flex justify-content-center align-items-center text-uppercase ">
<span>{fieldData.schema.label}</span>
<Icon icon="angle-right" width="12" height="12"/>
<span>{fieldData.field.label}</span>
</div>
<div class="d-flex justify-content-center text-center flex-wrap">
{#each fieldData.nodes as node}
{#if node._file?.path}
<div class="ms-2 mb-2" style="max-height:64px;">
<Preview record={node} size="small"/>
</div>
{:else}
<div class="ms-2 mb-2">
<PreviewCardSmall {graph} record={node}/>
</div>
{/if}
{/each}
</div>
<!-- <div class="text-center mt-3 d-block">{fieldData.field.label}</div>-->
</div>
{/each}
{#if Object.entries(parentEdgesByField).length > 0}
<div class="text-center my-4">
<Icon icon="angles-down" width="32" height="32"/>
</div>
{/if}
<div style="max-width:400px;margin:0 auto;">
<PreviewCard {graph} record={record}/>
</div>
{#if Object.entries(childrenEdgesByField).length > 0}
<div class="text-center my-4">
<Icon icon="angles-down" width="32" height="32"/>
</div>
{/if}
{#each Object.entries(childrenEdgesByField) as [fieldName, fieldData]}
<div class="lx-card mt-3">
<div class="text-center mb-5 d-block">{fieldData.field.label}</div>
<div class="d-flex justify-content-center text-center flex-wrap">
{#each fieldData.nodes as node}
{#if fieldData.field.info.ui === "file"}
<div
class="ms-2 mb-2"
style="max-width:64px;overflow:hidden;white-space: nowrap;text-overflow: ellipsis;"
>
<Preview
record={node}
size="small"
showFilename={true}
/> />
</div> </div>
{:else} {:else}
Nothing links to this record <div class="ms-2 mb-2">
<PreviewCardSmall {graph} record={node}/>
</div>
{/if}
{/each}
</div>
</div>
{/each} {/each}
</div>
<style>
</style>
+38 -23
View File
@@ -2,7 +2,7 @@
import {friendlyDate} from "../../helpers"; import {friendlyDate} from "../../helpers";
import Avatar from "../account/Avatar.svelte"; import Avatar from "../account/Avatar.svelte";
import {usernameById} from "../account/users"; import {usernameById} from "../account/users";
import {isEqual} from "lodash"; import {isEqual, sortBy} from "lodash";
import Icon from "../common/Icon.svelte"; import Icon from "../common/Icon.svelte";
import RevisionCell from "./revisions/RevisionCell.svelte"; import RevisionCell from "./revisions/RevisionCell.svelte";
import {getContext} from "svelte"; import {getContext} from "svelte";
@@ -123,19 +123,19 @@
</div> </div>
</div> </div>
</div> </div>
<div class="revisions"> <div class="lx-card mt-4">
{#if schema.revisions > 0} {#if schema.revisions > 0}
<div class="header-small mb-3">Revisions</div> <div class="header-small mb-3">Revisions</div>
{#each revisions as revision} {#each revisions as revision}
{#if revision._sys.version !== record._sys.version} {#if revision._sys.version != record._sys.version}
<div <div
class="revision" class="row p-2 rounded"
class:active={revision._sys.version === class:active={revision._sys.version ===
selectedRevision?._sys.version} selectedRevision?._sys.version}
> >
<div class="version"> <div class="col-2">version {revision._sys.version}</div>
<span>version {revision._sys.version}</span> <div class="col-5">
<Avatar <Avatar
name={usernameById(users, revision._sys.updatedBy)} name={usernameById(users, revision._sys.updatedBy)}
side={24} side={24}
@@ -147,7 +147,7 @@
<button <button
disabled={revision._sys.version === disabled={revision._sys.version ===
selectedRevision?._sys.version} selectedRevision?._sys.version}
class="button" class="btn btn-sm btn-outline-primary"
on:click={(e) => compare(e, revision)} on:click={(e) => compare(e, revision)}
>Compare >Compare
</button </button
@@ -164,14 +164,14 @@
</div> </div>
<div bind:this={revisionSection}> <div bind:this={revisionSection}>
{#if selectedRevision} {#if selectedRevision}
<div class="selected-revision"> <div class="mt-4">
{#if fieldsWithDiff.length > 0} {#if fieldsWithDiff.length > 0}
<p class="text-center fw-bold mb-3 mt-5"> <p class="text-center fw-bold mb-3 mt-5">
If you choose to rollback to this revision If you choose to rollback to this revision
</p> </p>
<button <button
on:click={rollback} on:click={rollback}
class="button" class="btn btn-primary mb-5 d-block mx-auto"
> >
Rollback to version {selectedRevision._sys.version} Rollback to version {selectedRevision._sys.version}
</button> </button>
@@ -189,17 +189,20 @@
{field.label} {field.label}
</div> --> </div> -->
<div <div
class="revision-field" class="lx-card row p-4 mb-4 w-100"
style="overflow:hidden" style="overflow:hidden"
> >
<div class="compare-left"> <div class="col-5">
<RevisionCell <RevisionCell
{field} {field}
side={record.data[field.name]} side={record.data[field.name]}
colorClass="text-danger" colorClass="text-danger"
/> />
</div> </div>
<div class="compare-center"> <div class="col-2">
<div
class="h-100 d-flex align-items-center justify-content-center text-secondary"
>
<span class="me-1">{field.label}</span> <span class="me-1">{field.label}</span>
<Icon <Icon
icon="angle-right" icon="angle-right"
@@ -207,7 +210,8 @@
height="12" height="12"
/> />
</div> </div>
<div class="compare-right"> </div>
<div class="col-5">
<RevisionCell <RevisionCell
edges={selectedRevision._edges} edges={selectedRevision._edges}
{field} {field}
@@ -231,25 +235,22 @@
</p> </p>
{#each Object.entries(edgeFieldsDiff) as [field, edges]} {#each Object.entries(edgeFieldsDiff) as [field, edges]}
<div <div
class="revision-references" class="lx-card row p-4 mb-4 w-100"
style="overflow:hidden" style="overflow:hidden"
> >
<div class="reference-field"> <div class="col-4">
{field}: {field}:
</div> </div>
<div class="reference-compare"> <div class="col-8">
<p class="mb-2 text-danger">Record</p>
<p class="">Record</p>
{#each edges.record as edge} {#each edges.record as edge}
<RevisionEdgeRow {edge}/> <RevisionEdgeRow {edge} />
{:else} {:else}
<p>No references</p> <p>No references</p>
{/each} {/each}
</div> <p class="mt-4 mb-2 text-success">Revision</p>
<div class="reference-compare">
<p class="text-success">Revision</p>
{#each edges.revision as edge} {#each edges.revision as edge}
<RevisionEdgeRow {edge}/> <RevisionEdgeRow {edge} />
{:else} {:else}
<p>No references</p> <p>No references</p>
{/each} {/each}
@@ -262,3 +263,17 @@
{/if} {/if}
</div> </div>
<style>
.label {
width: 180px;
margin-right: 10px;
margin-bottom: 4px;
display: inline-block;
}
.active {
background-color: #eee;
border: 1px solid #ccc;
}
</style>
+53 -38
View File
@@ -1,14 +1,12 @@
<script> <script>
import {afterUpdate, createEventDispatcher, getContext, onMount} from "svelte"; import {afterUpdate, createEventDispatcher, onMount,getContext} from "svelte";
import {isEqual} from "lodash"; import {isEqual} from "lodash";
import FormField from "./FormField.svelte"; import FormField from "./FormField.svelte";
import FilePreview from "./FilePreview.svelte"; import FilePreview from "./FilePreview.svelte";
import ContentTabs from "./header/ContentTabs.svelte"; import ContentTabs from "./ContentTabs.svelte";
import StatusSelect from "./form/StatusSelect.svelte";
import ErrorAlert from "../common/ErrorAlert.svelte"; import ErrorAlert from "../common/ErrorAlert.svelte";
import EditHeader from "./header/EditHeader.svelte";
import axios from "axios";
import Title from "./header/Title.svelte";
const channel = getContext("channel"); const channel = getContext("channel");
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -137,6 +135,7 @@
resolve(null); resolve(null);
}) })
.catch(function (error) { .catch(function (error) {
// setOriginalContent();
if (error.response) { if (error.response) {
if (typeof error.response.data.error === "string") { if (typeof error.response.data.error === "string") {
errorMessage = error.response.data.error; errorMessage = error.response.data.error;
@@ -145,6 +144,9 @@
} }
} }
resolve(null); resolve(null);
// msgSuccess = null;
// msgError = error.response.data.error;
// submitted = false;
}); });
}); });
} }
@@ -152,45 +154,15 @@
<svelte:window on:beforeunload={beforeUnload}/> <svelte:window on:beforeunload={beforeUnload}/>
<div class="inline-edit record-edit"> <div class="inline-edit my-4">
<div class="tools-header">
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab/>
{#if isCreateMode}
<button
class="button primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Create
</button>
{:else if hasUnsavedData}
<button
type="button"
class="button primary ms-2 btn btn-primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Save
</button>
{/if}
</div>
<Title {schema} {record} {isCreateMode}/>
<ErrorAlert message={errorMessage}/> <ErrorAlert message={errorMessage}/>
<div class=" mt-4" style="margin-bottom:150px;position:relative;"> <div class=" mt-1">
<ContentTabs <ContentTabs
{schema} {schema}
{isCreateMode} {isCreateMode}
bind:active={activeContentTab} bind:active={activeContentTab}
{record}
/> />
<FilePreview {record} {schema}/> <FilePreview {record} {schema}/>
<!-- <fieldset disabled="disabled"> --> <!-- <fieldset disabled="disabled"> -->
@@ -209,5 +181,48 @@
{/each} {/each}
<!-- </fieldset> --> <!-- </fieldset> -->
</div> </div>
<div>
<div class="d-flex mt-3 align-items-center justify-content-center">
{#if schema.hasDrafts}
<StatusSelect bind:status={record.status} {schema}/>
{/if}
{#if isCreateMode}
<button
class="ms-2 btn btn-primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Add
</button>
{:else}
<button
disabled={!hasUnsavedData}
class="ms-2 btn btn-primary btn-spinner"
on:click={save}
>
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Save
</button>
{/if}
<button class="ms-2 btn btn-link" on:click={cancel}>
cancel
</button>
</div>
</div>
</div> </div>
<style>
.inline-edit {
padding: 44px;
background-color: #eee;
border-radius: 32px;
}
</style>
+34
View File
@@ -0,0 +1,34 @@
<script>
import Icon from "../common/Icon.svelte";
import PreviewCardSmall from "./PreviewCardSmall.svelte";
export let managerRecords;
export let graph;
</script>
{#if managerRecords.length > 0}
<div
class="record-history d-flex justify-content-center align-items-center w-100 mb-4 mt-4"
>
{#each managerRecords.reverse() as arecord, i}
{#if i !== 0}
<Icon icon="angle-right"/>
{/if}
<div class="mx-3 p-0 my-0">
<PreviewCardSmall record={arecord} {graph}/>
</div>
{/each}
</div>
{/if}
<style>
.record-history {
/* background-color: #fff; */
padding: 15px 10px;
border-radius: 32px;
line-height: 12px;
}
</style>
+31 -8
View File
@@ -1,14 +1,18 @@
import Mustache from "mustache"; import Mustache from "mustache";
import {stripHtml} from "../../helpers"; import {stripHtml} from "../../helpers";
import {getContext} from "svelte";
export function previewTitle(schemas, record, graph) { export function previewTitle(record) {
let schema = schemas.find((aSchema) => aSchema.name === record?.schema); const channel = getContext("channel");
if (!schema?.cardTitle) { let schema = channel.schemas.find((aSchema) => aSchema.name === record?.schema);
if (!schema?.titleTemplate) {
return noTemplate(schema, record); return noTemplate(schema, record);
} }
let recordData = record.data; let template = Mustache.parse(schema.titleTemplate);
let render = Mustache.render(schema.cardTitle, recordData); let render = Mustache.render(schema.titleTemplate, record.data);
if (!render || render === "") { if (!render || render === "") {
return noTemplate(schema, record); return noTemplate(schema, record);
} }
@@ -16,6 +20,26 @@ export function previewTitle(schemas, record, graph) {
return stripHtml(render.slice(0, 300)); return stripHtml(render.slice(0, 300));
} }
export function previewEdgeTitle(edge) {
const channel = getContext("channel");
let edgeSchemaName = channel.schemas
.find((aSchema) => aSchema.name === edge?.sourceSchema)
.fields.find(f => f.name === edge.field).data;
let schema = channel.schemas.find((aSchema) => aSchema.name === edgeSchemaName);
if (!schema?.titleTemplate) {
return noTemplate(schema, edge);
}
let template = Mustache.parse(schema.titleTemplate);
let render = Mustache.render(schema.titleTemplate, edge.data);
if (!render || render === "") {
return noTemplate(schema, edge);
}
return stripHtml(render.slice(0, 300));
}
function noTemplate(schema, record) { function noTemplate(schema, record) {
if (schema?.type === "files") { if (schema?.type === "files") {
return record._file.path; return record._file.path;
@@ -24,9 +48,8 @@ function noTemplate(schema, record) {
let title = stripHtml( let title = stripHtml(
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name] record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name]
).slice(0, 300); ).slice(0, 300);
if (title === "") {
if(title.trim() === ""){ return "Untitled";
return "~Untitled~";
} }
return title; return title;
@@ -0,0 +1,96 @@
<script>
import Icon from "../common/Icon.svelte";
import {getContext, createEventDispatcher} from "svelte";
import {previewEdgeTitle, previewTitle} from "./Preview";
import Status from "./Status.svelte";
import Preview from "../newPreview/Preview.svelte";
import EdgeData from "./form/references/EdgeData.svelte";
const dispatch = createEventDispatcher();
const channel = getContext("channel");
export let record;
export let field;
export let edge = null;
export let editable = false;
export let classes = "";
export let hasDelete = false;
let edgeData;
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
let cardTitle = previewTitle(record);
function remove(e) {
e.preventDefault();
dispatch("remove", record.id);
}
function edit(e) {
e.preventDefault();
edgeData.openEdit();
}
</script>
<div class="d-flex gap-2">
{#if editable}
<div
class="card mb-2 bg-light w-50 "
style="border-color:{schema.color ?? '#ccc'}; border-width: 1px;"
>
<div class="card-body">
<span class="text-muted d-block">Relation Data</span>
{previewEdgeTitle(edge)}
<div class="position-absolute d-flex end-0" style="top:5px">
<button
class="trash-button text-dark btn btn-sm btn-link"
on:click={edit}
>
<Icon icon="pencil"/>
</button>
</div>
</div>
</div>
<EdgeData bind:this={edgeData} {record} {field} bind:edge/>
{/if}
<div
class="card mb-2 bg-light w-100 {classes}"
style="border-color:{schema.color ?? '#ccc'}; border-width: 1px;"
>
<div class="card-body">
<Preview {record} type="card"/>
</div>
{#if hasDelete}
<div class="position-absolute d-flex end-0" style="top:5px">
<button
class="trash-button text-dark btn btn-sm btn-link"
on:click={remove}
>
<Icon icon="trash-can"/>
</button>
</div>
{/if}
</div>
</div>
<style>
.card .trash-button {
display: none;
}
.card:hover .trash-button {
display: block;
}
.title-link {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>
@@ -0,0 +1,230 @@
<script>
import Icon from "../common/Icon.svelte";
import {createEventDispatcher, onMount, getContext} from "svelte";
import Preview from "../files/Preview.svelte";
import InlineEdit from "./InlineEdit.svelte";
import Reference from "../content/elements/Reference.svelte";
import File from "../content/elements/File.svelte";
const channel = getContext("channel");
const dispatch = createEventDispatcher();
export let isFirst;
export let isLast;
export let toDelete = false;
export let record;
let editRecord;
let editGraph;
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
$: editMode = false;
$: expanded = false;
function editInline(e) {
e.preventDefault();
axios
.get(channel.lucentUrl + "/records/editInline/" + record.id)
.then((response) => {
record = response.data;
editRecord = response.data.record;
editGraph = response.data.graph;
editMode = true;
})
.catch((error) => {
console.log(error);
});
}
function moveup(e) {
e.preventDefault();
dispatch("moveup");
}
function movedn(e) {
e.preventDefault();
dispatch("movedn");
}
function handleInlinesaved(e) {
e.preventDefault();
dispatch("inlinesaved", e.detail);
editMode = false;
}
function remove(e) {
e.preventDefault();
dispatch("remove", record.id);
}
function trash(e) {
e.preventDefault();
dispatch("trash", record.id);
}
function undo(e) {
e.preventDefault();
dispatch("undoremove", record.id);
}
function cancel(e) {
e.preventDefault();
editMode = false;
}
onMount(() => {
editMode = false;
});
function deleteFromChannel(e) {
e.preventDefault();
axios
.post(channel.lucentUrl +"/records/status/trashed", [record])
.then((response) => {
dispatch("remove", record.id);
})
.catch((error) => {
console.log(error);
});
}
</script>
<div>
{#if toDelete}
<div class="lx-card bg-danger bg-opacity-10 text-center">
<p>Item was removed from the current record.</p>
<p>
<button
class="btn btn-sm btn-outline border border-1 border-dark"
on:click={undo}>Undo
</button
>
<button
class="btn btn-sm btn-danger "
on:click={deleteFromChannel}
>Delete completely from channel
</button
>
</p>
<button class="btn btn-sm btn-link" on:click={remove}
>Dismiss Message
</button
>
</div>
{:else if editMode === true}
<InlineEdit
{schema}
record={editRecord}
graph={editGraph}
isCreateMode={false}
on:cancel={cancel}
on:inlinesaved={handleInlinesaved}
/>
{:else}
<div class="lx-card mt-4 bg-primary bg-opacity-10">
<div class="actions">
<small class="text-muted">{schema.label}</small>
<button
class="btn btn-sm btn-link"
on:click|preventDefault={editInline}
>
<Icon icon="pencil" width={12} height={12}/>
</button>
<button
class="btn btn-sm btn-link"
on:click={(e) => (expanded = !expanded)}
>
{#if expanded}
<Icon icon="compress" width={12} height={12}/>
{:else}
<Icon icon="expand" width={12} height={12}/>
{/if}
</button>
<div class="dropdown d-inline-block">
<button
class="btn btn-link btn-sm"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Icon icon="ellipsis"/>
</button>
<div class="dropdown-menu">
<a
class="dropdown-item"
href="/records/{record.id}"
target="_blank"
>Edit in new tab
</a>
<button class="dropdown-item" on:click={trash}>
Remove
</button>
<div class="text-center mt-3">
<!-- <a class="dropdown-item" href="#">Clone</a> -->
{#if !isFirst}
<button
class="btn btn-sm btn-outline-primary border-0"
on:click|preventDefault={moveup}
>
<Icon icon="circle-chevron-up"/>
</button>
{/if}
{#if !isLast}
<button
class="btn btn-sm btn-outline-primary border-0"
on:click|preventDefault={movedn}
>
<Icon icon="circle-chevron-down"/>
</button>
{/if}
</div>
</div>
</div>
</div>
<div class="inline-preview" class:expanded>
{#if schema.type === "files"}
<Preview {record} size="small"/>
{/if}
{#each schema.fields.filter((f) => !(f.trashed || ["tab"].includes(f.ui) || ["id"].includes(f.name))) as field}
<span class="text-muted d-block mt-2" style="font-size:13px"
>{field.label}</span
>
{#if field.ui === "reference"}
<Reference {record} {field}/>
{:else if field.ui === "file"}
<File {record} {field}/>
{:else}
{@html record.data[field.name]}
{/if}
{/each}
</div>
</div>
{/if}
</div>
<style>
.lx-card {
position: relative;
}
.lx-card .inline-preview {
max-height: 120px;
overflow: hidden;
}
.lx-card .inline-preview.expanded {
max-height: none;
}
.lx-card .actions {
top: 10px;
right: 44px;
position: absolute;
/* visibility: hidden; */
}
/* .lx-card:hover .actions {
visibility: visible;
} */
</style>
@@ -4,17 +4,19 @@
const channel = getContext("channel"); const channel = getContext("channel");
export let record; export let record;
export let graph;
$: schema = channel.schemas.find((aschema) => aschema.name === record.schema); $: schema = channel.schemas.find((aschema) => aschema.name === record.schema);
$: title = previewTitle(channel.schemas, record, graph); $: title = previewTitle( record);
</script> </script>
{#if record?.data} {#if record?.data}
<a <a
href="{channel.lucentUrl}/records/{record.id}" href="{channel.lucentUrl}/records/{record.id}"
class="text-decoration-none rounded py-1 px-2 d-inline-block"
{title} {title}
class="reference" style="border:2px solid {!schema.color
? '#999'
: schema.color}!important;white-space: nowrap;"
> >
{title} {title}
</a> </a>
@@ -0,0 +1,37 @@
<script>
import BlockButtons from "./BlockButtons.svelte";
import BlockElements from "./BlockElements.svelte";
import {flip} from "svelte/animate";
import {quintOut} from 'svelte/easing';
import {getContext} from "svelte";
const channel = getContext("channel");
export let record;
export let field;
export let value = [];
export let graph;
let blockSchema = channel.schemas.find((s) => s.name === field.schema);
</script>
<div class=" ">
<div class="inline-card-wrapper">
<BlockButtons
bind:blockData={value}
{blockSchema}
/>
</div>
{#each value as blockItemData (blockItemData.id)}
<div class="block-field-wrapper" animate:flip="{{delay: 250, duration: 250, easing: quintOut}}">
<BlockElements
bind:block={blockItemData}
bind:blockData={value}
{record}
{field}
bind:graph
/>
</div>
{/each}
</div>
@@ -0,0 +1,61 @@
<script>
import Icon from "../../common/Icon.svelte";
import {insertBlock} from "./block";
export let blockId = "";
export let blockData;
export let blockSchema;
$: showOptions = false;
function createBlock(e, ui) {
e.preventDefault();
blockData = insertBlock(blockData,ui);
showOptions = false;
}
</script>
<div class="d-flex justify-content-left mb-2 ">
<button
type="button"
class:is-first={!blockId}
class=" btn btn-lg btn-link text-decoration-none block-buttons"
on:click|preventDefault={(e) => (showOptions = !showOptions)}
>
<Icon width={24} height={24} icon="circle-plus"/>
</button>
{#if showOptions}
<div class="d-flex ">
{#each blockSchema.fields as validUi}
<div class="ms-2">
<button
class="btn btn-sm btn-primary"
on:click={(e) => createBlock(e, validUi)}
>{validUi.label}
</button>
</div>
{/each}
</div>
{/if}
</div>
<style>
:global(.block-field-wrapper) {
display: flex;
flex-direction: column;
}
:global(.block-field-wrapper .block-buttons) {
visibility: hidden;
}
:global(.block-field-wrapper:hover .block-buttons) {
visibility: visible;
}
.block-buttons {
padding: 0px;
z-index: 1;
margin: 0px ;
}
</style>
@@ -0,0 +1,158 @@
<script>
import Heading from "./elements/Heading.svelte";
import Textarea from "./elements/Textarea.svelte";
import Rich from "./elements/Rich.svelte";
import Markdown from "./elements/Markdown.svelte";
import Reference from "./elements/Reference.svelte";
import Icon from "../../common/Icon.svelte";
import {insertBlock} from "./block";
import {getContext} from "svelte";
import {findIndex} from "lodash";
import File from "./elements/File.svelte";
const channel = getContext("channel");
export let record;
export let blockData;
export let field;
export let graph;
export let block;
let blockSchema = channel.schemas.find((s) => s.name === field.schema);
function createBlock(e, ui, blockId) {
e.preventDefault();
blockData = insertBlock(blockData, ui, blockId);
}
function deleteBlock(e, blockId) {
e.preventDefault();
blockData = blockData.filter(b => b.id !== blockId)
}
function upBlock(e, blockId) {
e.preventDefault();
let blockIndex = findIndex(blockData, (b) => b.id === blockId);
let tempBlock = blockData[blockIndex];
blockData[blockIndex] = blockData[blockIndex - 1];
blockData[blockIndex - 1] = tempBlock;
}
function downBlock(e, blockId) {
e.preventDefault();
let blockIndex = findIndex(blockData, (b) => b.id === blockId);
let tempBlock = blockData[blockIndex];
blockData[blockIndex] = blockData[blockIndex + 1];
blockData[blockIndex + 1] = tempBlock;
}
function blockIsFirst(blockId) {
return findIndex(blockData, (b) => b.id === blockId) === 0;
}
function blockIsLast(blockId) {
return findIndex(blockData, (b) => b.id === blockId) === blockData.length - 1;
}
</script>
<div class="card block-editor-field d-flex">
<div class="d-flex justify-content-between">
<span class="text-muted d-block fs-6 mb-1">{block.meta.label}</span>
<div class="dropdown d-inline-block">
<button
class="btn btn-link btn-sm"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Icon icon="ellipsis"/>
</button>
<div class="dropdown-menu">
<h6 class="dropdown-header">
Block id: <input class="form-control-plaintext" readonly value={block.id}/>
Block name: <input class="form-control-plaintext" readonly value={block.meta.name}/>
</h6>
<div>
<hr class="dropdown-divider">
</div>
<h6 class="dropdown-header">Actions</h6>
<button
class="dropdown-item"
class:d-none={blockIsFirst(block.id)}
on:click={(e) => upBlock(e, block.id)}
>Move up
</button>
<button
class="dropdown-item"
class:d-none={blockIsLast(block.id)}
on:click={(e) => downBlock(e, block.id)}
>Move down
</button>
<button
class="dropdown-item text-danger"
on:click={(e) => deleteBlock(e, block.id)}
>Delete
</button
>
<h6 class="dropdown-header">Insert after</h6>
{#each blockSchema.fields as blockField}
<button
class="dropdown-item"
on:click={(e) => createBlock(e, blockField, block.id)}
>{blockField.label}
</button
>
{/each}
</div>
</div>
</div>
{#if block.meta.info.name === "heading"}
<Heading
bind:block={block}
/>
{:else if block.meta.info.name === "textarea"}
<Textarea
bind:block={block}
/>
{:else if block.meta.info.name === "rich"}
<Rich
bind:block={block}
/>
{:else if block.meta.info.name === "markdown"}
<Markdown
bind:block={block}
/>
{:else if block.meta.info.name === "file"}
<File
{record}
{field}
bind:graph
bind:block={block}
/>
{:else if block.meta.info.name === "reference"}
<Reference
{record}
{field}
bind:graph
bind:block={block}
/>
{/if}
</div>
<style>
.block-editor-field{
margin: 10px 0;
border-color: transparent;
}
</style>
+25
View File
@@ -0,0 +1,25 @@
import {randomId} from "../../../helpers.js";
export function insertBlock(blockData, blockField, afterBlockId = null) {
if (!afterBlockId) {
return [{
meta: blockField,
id: randomId(),
value: null
}, ...blockData];
}
return blockData.reduce((carry, block) => {
carry.push(block)
if (block.id === afterBlockId) {
carry.push({
meta: blockField,
id: randomId(),
value: null
});
}
return carry;
}, []);
}
@@ -0,0 +1,105 @@
<script>
import {getContext} from "svelte";
import PreviewCard from "../../PreviewCard.svelte";
import {sortByField} from "../../../edges/sortEdges";
import ReferenceInlineButtons from "../../elements/ReferenceInlineButtons.svelte"
import Sortable from "../../../libs/Sortable.svelte";
import {insertEdges} from "../../form/references/reference.js";
import BrowseModal from "../../form/references/BrowseModal.svelte";
const channel = getContext("channel");
export let block;
export let record;
export let field;
export let graph;
let browseModal;
let blockFieldName = field.name + ":" + block.id;
$: references = graph.edges
.filter((edge) => edge.field === blockFieldName)
.map((edge) => {
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
block.meta.collections.includes(aschema.name)
);
function removeReference(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === blockFieldName)
);
block.value = graph.edges.filter(edge => edge.field === blockFieldName) ?? [];
}
function openBrowseModal(e, schema) {
e.preventDefault();
browseModal.open(schema);
}
function reorder(e) {
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, blockFieldName);
}
function insert(e) {
e.preventDefault();
browseModal.close();
graph = insertEdges(graph,record,e.detail.records,blockFieldName,e.detail.action);
}
</script>
<div class="mb-0">
{#if block.meta.collections.length === 1}
<button
class="btn btn-outline-primary"
on:click={(e) => openBrowseModal(e, collections[0].name)}
>
Browse
</button>
{:else}
<div class="dropdown d-inline-block">
<button
class="btn btn-outline-primary btn-sm"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Browse
</button>
<ul class="dropdown-menu">
{#each collections as collection}
<li>
<!-- {`${channelurl}/content/${collection.name}?parent=${record.id}&parentfield=${field.name}`} -->
<a
class="dropdown-item"
on:click={(e) =>
openBrowseModal(e, collection.name)}
href="/">{collection.label}</a
>
</li>
{/each}
</ul>
</div>
{/if}
</div>
{#if references.length > 0}
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
{#each references as reference (reference.id)}
<div class="col mb-3">
<PreviewCard
classes="h-100"
record={reference}
hasDelete={true}
on:remove={removeReference}
/>
</div>
{/each}
</Sortable>
{/if}
<BrowseModal bind:this={browseModal} on:insert={insert}/>
@@ -0,0 +1,13 @@
<script>
export let block;
</script>
<div class="mb-0">
<input
type="text"
id={block.id}
class="form-control"
bind:value={block.value}
autocomplete="off"
/>
</div>
@@ -0,0 +1,15 @@
<script>
import Codemirror from "../../../libs/CodemirrorMarkdown.svelte";
export let block;
// export let id;
</script>
<div class="mb-3">
<Codemirror bind:value={block.value} />
</div>
@@ -0,0 +1,71 @@
<script>
import {getContext} from "svelte";
import PreviewCard from "../../PreviewCard.svelte";
import {sortByField} from "../../../edges/sortEdges";
import ReferenceInlineButtons from "../../elements/ReferenceInlineButtons.svelte"
import Sortable from "../../../libs/Sortable.svelte";
import {insertEdges} from "../../form/references/reference.js";
const channel = getContext("channel");
export let block;
export let record;
export let field;
export let graph;
let blockFieldName = field.name + ":" + block.id;
$: references = graph.edges
.filter((edge) => edge.field === blockFieldName)
.map((edge) => {
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
block.meta.collections.includes(aschema.name)
);
function removeReference(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === blockFieldName)
);
block.value = graph.edges.filter(edge => edge.field === blockFieldName) ?? [];
}
function reorder(e) {
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, blockFieldName);
}
function insert(e) {
e.preventDefault();
graph = insertEdges(graph,record,e.detail.records,blockFieldName,e.detail.action);
}
</script>
<div class="inline-card-wrapper">
<ReferenceInlineButtons
buttonClass="mt-2"
recordId={null}
schemas={collections}
on:insert={insert}
on:save={insert}
/>
</div>
{#if references.length > 0}
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
{#each references as reference (reference.id)}
<div class="col mb-3">
<PreviewCard
classes="h-100"
record={reference}
hasDelete={true}
on:remove={removeReference}
/>
</div>
{/each}
</Sortable>
{/if}
@@ -0,0 +1,10 @@
<script>
import Tinymce from "../../../libs/Tinymce.svelte";
export let block;
let additionalConfig = {};
</script>
<div class="mb-0">
<Tinymce bind:value={block.value} {additionalConfig}/>
</div>
@@ -0,0 +1,39 @@
<script>
import {onMount} from "svelte";
export let block;
let thisEl;
function resize(e) {
let el;
if (e.target) {
el = e.target;
} else {
el = e;
}
el.style.overflow = "hidden";
el.style.height = "1px";
el.style.height = +el.scrollHeight + "px";
}
onMount(() => {
resize(thisEl);
});
</script>
<div class="mb-0">
<textarea
bind:value={block.value}
bind:this={thisEl}
on:input={resize}
id={block.id}
class="form-control"
autocomplete="off"></textarea>
</div>
<style>
textarea {
resize: none;
}
</style>
+34
View File
@@ -0,0 +1,34 @@
import { deepEqual } from 'fast-equals';
export function isEqual(obj1, obj2) {
return deepEqual(obj1, obj2);
// if (obj1 === obj2) return true;
//
// if (Array.isArray(obj1) && Array.isArray(obj2)) {
//
// if(obj1.length !== obj2.length) return false;
//
// return obj1.every((elem, index) => {
// return isEqual(elem, obj2[index]);
// })
//
//
// }
//
// if(typeof obj1 === "object" && typeof obj2 === "object" && obj1 !== null && obj2 !== null) {
// if(Array.isArray(obj1) || Array.isArray(obj2)) return false;
//
// const keys1 = Object.keys(obj1)
// const keys2 = Object.keys(obj2)
//
// if(keys1.length !== keys2.length || !keys1.every(key => keys2.includes(key))) return false;
//
// for(let key in obj1) {
// if (!isEqual(obj1[key], obj2[key])) { return false; }
// }
//
// return true;
//
// }
//
// return false;
}
@@ -1,36 +0,0 @@
<script>
import {getErrorMessage} from "./errorMessage";
export let field;
export let value;
export let isCreateMode;
export let validationErrors;
export let id;
$: errorMessage = getErrorMessage(validationErrors, field.name);
</script>
<div class="mb-0">
<div style="display: flex; align-items: center;gap: 10px">
<input
type="color"
{id}
style="border: none;background: transparent;padding: 0;width:64px;"
disabled={field.readonly && !isCreateMode}
bind:value
/>
<input
type="text"
class:is-invalid={errorMessage}
{id}
class="form-control"
bind:value
readonly={field.readonly && !isCreateMode}
/>
</div>
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -1,57 +0,0 @@
<script>
import {onMount} from "svelte";
import flatpickr from "flatpickr";
import "flatpickr/dist/flatpickr.css";
import "flatpickr/dist/themes/light.css";
import {getErrorMessage} from "./errorMessage";
export let field;
export let value;
export let id;
export let isCreateMode;
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
let pickerInput;
let pickerInstance;
let flatpickrOptions = {
enableTime: false,
allowInput: true,
dateFormat: "Y-m-d",
};
if (field.min) {
flatpickrOptions.minDate = field.min;
}
if (field.max) {
flatpickrOptions.maxDate = field.max;
}
onMount(() => {
if (!field.readonly || isCreateMode) {
pickerInstance = flatpickr(pickerInput, flatpickrOptions);
}
});
</script>
<div class="mb-0">
<input
type="text"
{id}
class="form-control"
class:is-invalid={errorMessage}
bind:value
bind:this={pickerInput}
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -1,68 +0,0 @@
<script>
import {onMount} from "svelte";
import flatpickr from "flatpickr";
import "flatpickr/dist/flatpickr.css";
import "flatpickr/dist/themes/light.css";
import {getErrorMessage} from "./errorMessage";
export let field;
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",
dateFormat: "Z",
enableTime: true,
time_24hr: true,
enableSeconds: true,
};
if (field.min) {
flatpickrOptions.minDate = field.min;
}
if (field.max) {
flatpickrOptions.maxDate = field.max;
}
onMount(() => {
if (!field.readonly || isCreateMode) {
pickerInstance = flatpickr(pickerInput, flatpickrOptions);
}
});
</script>
<div class="mb-0" bind:this={wrapperDiv}>
<input
type="text"
{id}
class="form-control"
class:is-invalid={errorMessage}
bind:value
bind:this={pickerInput}
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
<span class="system-help-text"
>Dates are displayed according to your timezone: {timezone}</span
>
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -1,84 +0,0 @@
<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";
import {insertEdges} from "./reference.js";
import {getContext} from "svelte";
const channel = getContext("channel");
export let field;
export let record;
export let graph
let browseModal;
$: references = graph?.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
);
function removeReference(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === field.name)
);
}
function openBrowseModal(e, schema) {
e.preventDefault();
browseModal.open(schema);
}
async function reorder(e) {
graph.edges = await sortByField(e.detail.source, e.detail.target, graph.edges, field.name, references);
}
function insert(e) {
e.preventDefault();
browseModal.close();
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action);
}
</script>
<div class="mb-0">
{#if field.collections.length === 1}
<button
class="button"
on:click={(e) => openBrowseModal(e, collections[0].name)}
>
Browse
</button>
{:else}
<Dropdown>
<div slot="button">
Browse
</div>
{#each collections as collection}
<!-- {`${channelurl}/content/${collection.name}?parent=${record.id}&parentfield=${field.name}`} -->
<a
class="dropdown-item"
on:click={(e) => openBrowseModal(e, collection.name)}
href="/">{collection.label}</a
>
{/each}
</Dropdown>
{/if}
</div>
{#if references.length > 0}
<Sortable sortableClass="mt-3" on:update={reorder}>
{#each references as reference (reference.id)}
<!--This div helps the sorting thing-->
<div>
<PreviewFile record={reference} hasDelete={true} on:remove={removeReference}></PreviewFile>
</div>
{/each}
</Sortable>
{/if}
<Dialog bind:this={browseModal} on:insert={insert}></Dialog>
@@ -1,40 +0,0 @@
<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: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}
</div>
{/if}
</div>
@@ -1,43 +0,0 @@
<script>
import {getErrorMessage} from "./errorMessage";
export let field;
export let value;
export let validationErrors;
export let isCreateMode;
export let id;
$: errorMessage = getErrorMessage(validationErrors, field.name);
let list;
function fixDecimals(e) {
const number = e.currentTarget.value;
const formattedNumber = formatNumber(number);
value = isNaN(formattedNumber) ? null : formattedNumber;
}
function formatNumber(number) {
return parseFloat(number).toFixed(field.decimals);
}
</script>
<div class="mb-0">
<input
type="number"
{id}
class="form-control"
class:is-invalid={errorMessage}
on:change={fixDecimals}
bind:value
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
</div>
{/if}
</div>
@@ -1,12 +1,11 @@
<script> <script>
import {getContext} from "svelte"; import {getContext} from "svelte";
import {insertEdges} from "./reference"; import {insertEdges} from "../form/references/reference.js";
import {getErrorMessage} from "./errorMessage"; import PreviewCard from "../PreviewCard.svelte";
import {getErrorMessage} from "../form/errorMessage.js";
import {sortByField} from "../../edges/sortEdges"; import {sortByField} from "../../edges/sortEdges";
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte"; import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
import Sortable from "../../libs/Sortable.svelte"; import Sortable from "../../libs/Sortable.svelte";
import PreviewReference from "../previews/PreviewReference.svelte";
import axios from "axios";
const channel = getContext("channel"); const channel = getContext("channel");
export let record; export let record;
@@ -15,12 +14,7 @@
export let validationErrors; export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name); $: errorMessage = getErrorMessage(validationErrors, field.name);
$: currentReferences = graph._children[field.name] ?? [];
$: references = graph.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) => let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name) field.collections.includes(aschema.name)
@@ -34,22 +28,12 @@
} }
function reorder(e) { function reorder(e) {
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, field.name);
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, field.name, references);
} }
function insert(e) { function insert(e) {
e.preventDefault(); e.preventDefault();
// axios.post(channel.lucentUrl + "/edges/insert-many", { graph = insertEdges(graph,record,e.detail.records,field.name,e.detail.action);
// source: record.id,
// sourceSchema: record.schema,
// targetSchema: e.detail.schema,
// field: field.name,
// targets: e.detail.records.map(r => r.id),
// }).then(function (response) {
// graph = response.data.graph;
// })
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action);
} }
</script> </script>
@@ -61,18 +45,19 @@
{/if} {/if}
<div class="inline-card-wrapper"> <div class="inline-card-wrapper">
<ReferenceInlineButtons <ReferenceInlineButtons
buttonClass="mt-2"
recordId={null} recordId={null}
schemas={collections} schemas={collections}
on:insert={insert} on:insert={insert}
on:save={insert} on:save={insert}
/> />
</div> </div>
{#if references.length > 0} {#if currentReferences.length > 0}
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}> <Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
{#each references as reference (reference.id)} {#each currentReferences as reference (reference.id)}
<div> <div class="col mb-3">
<PreviewReference <PreviewCard
{graph} classes="h-100"
record={reference} record={reference}
hasDelete={true} hasDelete={true}
on:remove={removeReference} on:remove={removeReference}

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