Compare commits

...

72 Commits

Author SHA1 Message Date
lexx e910ae9878 login 2025-05-16 13:53:41 +03:00
lexx 362c649d36 Merge branch 'dev' of ssh://code.radical-elements.com:2727/lucent/lucent-laravel into dev 2025-05-16 13:27:48 +03:00
lexx 852c4d608d thumbnail limit 2025-05-16 13:27:25 +03:00
lexx aa59e55a41 meta htmx 2025-05-15 19:18:03 +03:00
lexx 348bad80e0 Merge pull request 'Fix' (#23) from Catching-thumbnail-rebuild-error into dev
Reviewed-on: #23
2025-05-06 10:46:58 +00:00
arvanitakis f0d4686141 Fix 2025-05-06 13:46:00 +03:00
lexx a482ab3c7e login urls 2025-03-20 21:06:40 +02:00
lexx c580882ec0 fix url 2025-03-20 20:57:47 +02:00
lexx 2cf8379cbe urls update 2025-03-20 20:53:51 +02:00
lexx c39ec469df files bug 2025-01-22 20:03:12 +02:00
lexx 232fcc8845 csv render title 2024-12-18 13:02:09 +02:00
lexx 9d5d4dd930 csv relations 2024-12-14 18:56:04 +02:00
lexx c507dc6031 upload fix 2024-10-23 19:34:41 +03:00
lexx 843f560710 new build 2024-09-27 17:42:49 +03:00
lexx 7574d67d80 some styling in tables 2024-09-27 16:48:05 +03:00
lexx 19931cb4d1 update files script 2024-09-27 16:27:37 +03:00
lexx 6458c1e71d rebuilding thumbnails command 2024-09-27 15:32:35 +03:00
lexx 63232585ab storage and image model 2024-09-27 14:28:20 +03:00
lexx 6d15591601 wip stograge 2024-09-20 13:39:45 +03:00
lexx 32c8378020 refactor files 2024-09-19 23:36:43 +03:00
lexx d0cd8228cc fix replacing config 2024-09-13 18:13:15 +03:00
lexx c45a3847f8 fix rich editor embed image original 2024-09-13 18:11:57 +03:00
lexx c0b3878674 file route for template generation 2024-09-13 17:16:04 +03:00
lexx f868219981 fixes and stuff 2024-09-11 16:21:51 +03:00
lexx 8ac0567e66 fix graph ignoring missing fields 2024-09-07 15:57:31 +03:00
lexx 02f8f5970a codemirror insert 2024-09-07 15:31:56 +03:00
lexx 0cd4e08716 fixing database connections 2024-09-07 13:22:58 +03:00
lexx cf3d621587 helper commands 2024-09-07 00:03:11 +03:00
lexx 6fc0a65b6f setup complete 2024-09-06 23:30:12 +03:00
lexx a73ee21568 wip setup guide 2024-09-06 21:00:15 +03:00
lexx ff54bcc2ef wip setup guide 2024-09-06 20:59:56 +03:00
lexx ab1517cc8f tabs and date in modal fix 2024-08-30 13:38:34 +03:00
lexx 9f724a3243 better report 2024-08-27 17:59:12 +03:00
lexx ae65ca47f6 commands and logs to the database 2024-08-27 17:42:06 +03:00
lexx 74d2fcc4fa build assets 2024-08-27 12:25:42 +03:00
lexx 82174afdea fixing multiple references 2024-08-27 12:24:51 +03:00
lexx ffc39f078d trix 2024-08-25 14:45:49 +03:00
lexx 7c4e19afbc tip tap and trix 2024-08-25 14:23:20 +03:00
lexx 7b10bfca1d cleanup 2024-08-24 19:57:17 +03:00
lexx 0e5ac08641 actions 2024-08-24 19:35:07 +03:00
lexx 1505aaa909 multiple commands 2024-08-24 18:51:36 +03:00
lexx d9e2c4954a refactoring of filters 2024-08-24 17:22:40 +03:00
lexx 97ad9de3d2 readme 2024-08-24 01:30:44 +03:00
lexx 9e140be0ec updated readme 2024-08-23 21:06:53 +03:00
lexx a737c2d571 configurable disks 2024-08-23 20:58:45 +03:00
lexx c43c29eb14 modal save button 2024-08-23 19:37:20 +03:00
lexx 0c00f76657 fix tools and layhout 2024-08-23 18:15:18 +03:00
lexx 4165bfb95d build controller 2024-08-23 17:29:08 +03:00
lexx 570dbf747e channel 2024-08-23 17:23:43 +03:00
lexx 14cbd0a845 manifest fix 2024-08-23 17:15:40 +03:00
lexx c99634bb46 query 2024-08-23 16:54:33 +03:00
lexx 246696f331 assets build 2024-08-22 17:59:06 +03:00
lexx 0643578d15 record update 2024-08-22 17:58:30 +03:00
lexx 3aa9191cba file upload 2024-08-19 17:59:08 +03:00
lexx c97be8666e records and edgs 2024-08-19 17:48:10 +03:00
lexx 509d7c13f2 cleanup 2024-08-18 19:20:53 +03:00
lexx 50c8af7bda theming 2024-08-18 19:04:32 +03:00
lexx 5d6869c118 rich editor files 2024-08-18 17:23:18 +03:00
lexx ec15f21e67 here and there 2024-08-17 21:10:01 +03:00
lexx 36165444cf file previews 2024-08-17 20:31:04 +03:00
lexx 322962403d colors and filters 2024-08-17 19:23:19 +03:00
lexx db37653748 fixed zindex thing - brightness was the problem 2024-08-16 21:11:01 +03:00
lexx 5a13ddb2ec backlinks 2024-08-16 17:38:26 +03:00
lexx 9bbd53b586 references 2024-08-16 16:00:48 +03:00
lexx a04e338ce2 updated dependencies 2024-08-16 14:34:39 +03:00
lexx 2429d4acb5 dialog wip 2024-08-15 22:11:26 +03:00
lexx 113533408d content and edit record 2024-08-15 18:52:53 +03:00
lexx f9806f60c9 filters and sidebar 2024-08-15 14:44:53 +03:00
lexx 1f3ebafe69 transition 2024-08-14 22:04:34 +03:00
lexx 1ab3f678b7 untitled fix 2024-07-22 20:09:16 +03:00
lexx a54200c5e5 build 2024-05-22 17:05:56 +03:00
lexx 069ae72705 fix sort 2024-05-22 17:05:25 +03:00
319 changed files with 15778 additions and 7078 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "/phpactor.schema.json",
"language_server_phpstan.enabled": false
}
+25 -3
View File
@@ -9,8 +9,8 @@ include_toc: true
### Requirements
- PHP 8.2
- Laravel 10
- PHP 8.3
- Laravel 11
- Postgres or Sqlite database
- ImageMagick
@@ -82,7 +82,9 @@ return [
### Database
The recommended database for small website is sqlite. But you can also use postresql
Make sure to delete the existing migration scripts in your database/migrations folder.
> [!CAUTION]
> Make sure to delete the existing migration scripts in your database/migrations folder.
Then run:
@@ -90,6 +92,26 @@ Then run:
php artisan migrate
```
### File Storage
You can use your local filesystem or s3 compatible storage. Lucent expects you to have a valid configuration inside ``config/filesystems.php``
example:
```php
return [
'disks' => [
'lucent' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => true,
],
],
];
```
### First user
To create your first user, head to your localhost:8000/lucent
+32
View File
@@ -0,0 +1,32 @@
# Upgrade from 1.1.* to 1.2.0
## lucent.php config file
There is now an array of commands, accepting more than one.
from
```php
"generateCommand" => env("LUCENT_GENERATE_COMMAND", "generate:static"),
```
to
```php
"commands" => [
"generate:static" => "Build Website",
],
```
## config/filesystems.php
Lucent has its own filesystem.
You should now add:
```
'lucent' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
],
```
+7 -5
View File
@@ -8,16 +8,19 @@
"ext-zip": "*",
"ext-sqlite3": "*",
"ext-imagick": "*",
"php": "^8.2",
"ext-pdo": "*",
"php": "^8.3",
"guzzlehttp/guzzle": "^7.2",
"intervention/image": "^2.7",
"phpoption/phpoption": "^1.9",
"spatie/image-optimizer": "^1.6",
"staudenmeir/laravel-cte": "^1.0",
"ext-pdo": "*"
"mustache/mustache": "^2.14"
},
"require-dev": {
"phpstan/phpstan": "^1.8"
"phpstan/phpstan": "^1.8",
"laravel/framework": "^10.10"
},
"autoload": {
"psr-4": {
@@ -25,8 +28,7 @@
},
"files": [
"src/Response.php",
"src/macros.php",
"src/File/Uploader.php"
"src/macros.php"
]
},
"extra": {
Generated
+3982 -514
View File
File diff suppressed because it is too large Load Diff
+5 -11
View File
@@ -24,7 +24,8 @@ There are 3 types of schemas
- **fields**: The list of your fields. Look the field reference for more
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar. Default: false _optional_
- **sortBy**: The default sorting in the content browser _optional_
- **titleTemplate**: Mustache code to customize the preview field _optional_
- **cardTitle**: Mustache code to customize the preview field _optional_
- **cardImage**: Field name of image you want to use as a preview image _optional_
- **revisions**: How many revisions are going to be kept for each record _optional_
- **read**: Array of user groups that have read permissions _optional_
- **write**: Array of user groups that have write permissions _optional_
@@ -40,20 +41,12 @@ There are 3 types of schemas
- **fields**: The list of your fields. Look the field reference for more
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar _optional_
- **sortBy**: The default sorting in the content browser _optional_
- **titleTemplate**: Mustache code to customize the preview field _optional_
- **cardTitle**: Mustache code to customize the preview field _optional_
- **revisions**: How many revisions are going to be kept for each record _optional_
- **read**: Array of user groups that have read permissions _optional_
- **write**: Array of user groups that have write permissions _optional_
## Block Reference
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
- **label**: The friendly name of the schema
- **type**: The type of the collection. Should be "block"
- **fields**: The list of your fields. Look the field reference for more
A full Collection example without the fields:
```json
@@ -74,7 +67,8 @@ A full Collection example without the fields:
"SEO"
],
"sortBy": "-_sys.createdAt",
"titleTemplate": "{{name}} {{slug}}",
"schemaTitle": "{{name}} {{slug}}",
"schemaImage": "cover",
"revisions": 15,
"read": [
"admin",
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
+3 -6
View File
@@ -1,14 +1,11 @@
{
"main.js": {
"file": "assets/main.7c3e8b7b.js",
"file": "assets/main-BJyanQ7P.js",
"name": "main",
"src": "main.js",
"isEntry": true,
"css": [
"assets/main.587d6006.css"
"assets/main-Dk7njt4m.css"
]
},
"main.css": {
"file": "assets/main.587d6006.css",
"src": "main.css"
}
}
+3
View File
@@ -5,6 +5,9 @@
*/
import axios from "axios";
import {loadHtmxFormsBehaviour} from "./htmx-form.js";
loadHtmxFormsBehaviour();
window.axios = axios;
export const axiosInstance = axios;
+20
View File
@@ -30,3 +30,23 @@ export function stripHtml(html = "") {
export function randomId(length = 10) {
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
@@ -0,0 +1,24 @@
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");
}
});
}
+1 -11
View File
@@ -2,22 +2,13 @@ import {axiosInstance} from "./bootstrap";
import "../sass/app.scss";
import Account from "./svelte/Account.svelte";
import Channel from "./svelte/Channel.svelte";
import * as bootstrap from "bootstrap";
import Mustache from "mustache";
import 'htmx.org';
Mustache.escape = function (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
const entryComponents = {
@@ -61,4 +52,3 @@ let loadSvelte = function () {
// document.addEventListener("turbo:load", loadSvelte);
document.addEventListener("DOMContentLoaded", loadSvelte);
document.addEventListener("DOMContentLoaded", enableTooltipsAnywhere);
+4 -2
View File
@@ -3,6 +3,7 @@
import Login from "./account/Login.svelte";
import Verify from "./account/Verify.svelte";
import Profile from "./account/Profile.svelte";
import SetupIndex from "./setup/Index.svelte";
import {setContext} from "svelte";
const components = {
@@ -10,6 +11,7 @@
login: Login,
verify: Verify,
profile: Profile,
setup: SetupIndex,
};
export let title;
@@ -21,8 +23,8 @@
setContext("channel", channel);
setContext("user", user);
</script>
<div class="text-center">
<h1><a class="text-decoration-none" href="{channel.lucentUrl}">{channel.name}</a></h1>
<div style="text-align: center;background: var(--p20);padding: 20px;color: var(--p90)">
<h1><a class="text-decoration-none" href="{channel.lucentUrl}">{channel.name ?? "Lucent Setup"}</a></h1>
</div>
<div>
<svelte:component this={components[view]} {title} {...data}/>
+13 -3
View File
@@ -4,9 +4,10 @@
import RecordEdit from "./records/Edit.svelte";
import ContentIndex from "./content/Index.svelte";
import {setContext} from "svelte";
import Navbar from "./Navbar.svelte";
import Navbar from "./layout/Navbar.svelte";
import HomeIndex from "./home/Index.svelte";
import BuildReport from "./build/Report.svelte";
import Header from "./layout/Header.svelte";
const components = {
members: Members,
@@ -35,7 +36,16 @@
</script>
<Navbar schema={data.schema}/>
<svelte:component this={components[view]} {title} {...data}/>
<div class="main-wrapper">
<div class="sidebar-content">
<Navbar schema={data.schema}/>
</div>
<div class="main-content">
<Header />
<svelte:component this={components[view]} {title} {...data}/>
</div>
</div>
-126
View File
@@ -1,126 +0,0 @@
<script>
import Avatar from "./account/Avatar.svelte";
import NavbarMenu from "./NavbarMenu.svelte";
import {getContext} from "svelte";
export let schema;
const channel = getContext("channel");
const readableSchemas = getContext("readableSchemas");
const user = getContext("user");
let contentIsOpen = false;
const fileSchemas = readableSchemas.filter((sc) => sc.type === "files");
const otherSchemas = readableSchemas.filter((sc) => !sc.isEntry && sc.type === "collection");
let filesIsActive = false;
let otherIsActive = false;
if(schema){
filesIsActive = fileSchemas.filter(s => s.name === schema.name).length > 0;
otherIsActive = otherSchemas.filter(s => s.name === schema.name).length > 0;
}
</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 ">
<a class="nav-item" href="{channel.lucentUrl}">{channel.name}</a>
<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}
<!-- <div>-->
<!-- <form method="GET">-->
<!-- <input type="search" name="filter[search_regex]" placeholder="Search"-->
<!-- class="form-control" required/>-->
<!-- </form>-->
<!-- </div>-->
</div>
<div>
<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-light-subtle" class:d-none={!contentIsOpen}
style="padding-top:36px " data-bs-scroll="true"
data-bs-backdrop="false"
tabindex="-1" aria-labelledby="offcanvasScrollingLabel">
<!-- <div class="offcanvas-header">-->
<!-- <h5 class="offcanvas-title" id="offcanvasScrollingLabel">Content</h5>-->
<!-- </div>-->
<div class="offcanvas-body">
<button on:click={(e) => contentIsOpen = false} class="btn btn-primary btn-sm d-xxl-none mb-4">« close</button>
<div class="accordion">
<div class="accordion-item">
<h2 class="accordion-header" id="panelsStayOpen-headingMain">
<button class="accordion-button" type="button" data-bs-toggle="collapse"
data-bs-target="#panelsStayOpen-collapseMain" aria-expanded="true"
aria-controls="panelsStayOpen-collapseMain">
Main
</button>
</h2>
<div id="panelsStayOpen-collapseMain" class="accordion-collapse collapse show"
aria-labelledby="panelsStayOpen-headingMain">
<div class="accordion-body">
<NavbarMenu
schemas={ readableSchemas.filter((sc) => sc.isEntry)}
schema={schema}
/>
</div>
</div>
</div>
{#if otherSchemas.length > 0}
<div class="accordion-item">
<h2 class="accordion-header" id="panelsStayOpen-headingOther">
<button class="accordion-button" class:collapsed={!otherIsActive} type="button" data-bs-toggle="collapse"
data-bs-target="#panelsStayOpen-collapseOther" aria-expanded={otherIsActive}
aria-controls="panelsStayOpen-collapseOther">
Other
</button>
</h2>
<div id="panelsStayOpen-collapseOther" class="accordion-collapse collapse"
class:show={otherIsActive}
aria-labelledby="panelsStayOpen-headingOther">
<div class="accordion-body">
<NavbarMenu
schemas={ otherSchemas}
schema={schema}
/>
</div>
</div>
</div>
{/if}
{#if fileSchemas.length > 0}
<div class="accordion-item">
<h2 class="accordion-header" id="panelsStayOpen-headingFS">
<button class="accordion-button " class:collapsed={!filesIsActive} type="button" data-bs-toggle="collapse"
data-bs-target="#panelsStayOpen-collapseFS" aria-expanded={filesIsActive}
aria-controls="panelsStayOpen-collapseFS">
Filesystem
</button>
</h2>
<div id="panelsStayOpen-collapseFS" class="accordion-collapse collapse" class:show={filesIsActive}
aria-labelledby="panelsStayOpen-headingFS">
<div class="accordion-body">
<NavbarMenu
schemas={ fileSchemas}
schema={schema}
/>
</div>
</div>
</div>
{/if}
</div>
</div>
</div>
-16
View File
@@ -1,16 +0,0 @@
<script>
import {getContext} from "svelte";
const channel = getContext("channel");
export let schemas;
export let schema;
</script>
<div class="list-group list-group-flush">
{#each schemas as aschema}
<a class="list-group-item list-group-item-action" class:active={aschema.name === schema?.name}
aria-current="page"
href="{channel.lucentUrl}/content/{aschema.name}">{aschema.label}</a>
{/each}
</div>
+6 -6
View File
@@ -55,28 +55,28 @@
<Avatar name={user.name}/>
</h3>
<form on:submit={saveName}>
<div class="input-group mb-3">
<div class="input-group mb-5">
<input
type="text"
bind:value={name}
class="form-control"
class="form-control mb-3"
placeholder="Name"
required
/>
<SpinnerButton label="Update"/>
<SpinnerButton label="Update Name"/>
</div>
</form>
<form on:submit={saveEmail}>
<div class="input-group mb-3">
<div class="input-group mb-5">
<input
type="email"
bind:value={email}
class="form-control"
class="form-control mb-3"
placeholder="Email"
required
/>
<SpinnerButton label="Update"/>
<SpinnerButton label="Update Email"/>
</div>
</form>
@@ -0,0 +1,52 @@
<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}
@@ -0,0 +1,58 @@
<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}
+23 -11
View File
@@ -1,22 +1,25 @@
<script>
import {getContext, onMount} from "svelte";
import axios from "axios";
const channel = getContext("channel");
export let title;
export let command;
$: date = "";
$: logs = "";
let anchorEl;
let inProgress = false;
function connect() {
const eventSource = new EventSource(channel.lucentUrl + "/build-report-source");
const eventSource = new EventSource(channel.lucentUrl + "/command-report-source/" + command.signature );
eventSource.onmessage = function (event) {
inProgress = true;
const data = JSON.parse(event.data);
date = data.date;
logs = data.logs;
anchorEl.scrollIntoView()
}
eventSource.onerror = (e) => {
console.log(e)
@@ -28,8 +31,7 @@
function buildWebsite(e) {
e.preventDefault();
inProgress = true;
axios.post(channel.lucentUrl + "/build").then(response => {
axios.post(channel.lucentUrl + "/command/" + command.signature).then(response => {
connect()
})
@@ -41,31 +43,41 @@
</script>
<div class="wrapper-tiny transparent mb-5">
<div class="common-wrapper">
<div class="lx-card mt-5">
<h3 class="header-small mb-5">{title}</h3>
<button on:click={buildWebsite} class="btn btn-outline-primary btn-sm mb-3" disabled={inProgress}>Start Build
<button on:click={buildWebsite} class="button primary mb-3" disabled={inProgress}>Start
</button>
<div class="mb-3">
{#if inProgress}
<span class="badge text-bg-warning">
Build in progress
Action in progress
</span>
{/if}
{#if !inProgress && logs}
<span class="badge text-bg-info">
Build completed
Action completed
</span>
{/if}
</div>
<pre>{logs}</pre>
<pre class="logs">{logs}
<div bind:this={anchorEl}>&nbsp;</div>
</pre>
</div>
</div>
<style>
.logs{
max-height: 70vh;
overflow: scroll;
background: var(--p90);
color: var(--p10);
padding: 10px;
}
</style>
+14
View File
@@ -0,0 +1,14 @@
<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>
+29 -19
View File
@@ -1,23 +1,33 @@
<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>
<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>
+3 -2
View File
@@ -3,7 +3,8 @@
</script>
{#if message}
<div class="alert alert-danger" role="alert">
{message}
<div class="notice notice-error" role="alert">
<div class="title">Submission Errors</div>
<div class="content"> {message}</div>
</div>
{/if}
+34 -10
View File
@@ -76,7 +76,7 @@
path: '<path d="M447.1 224c0-12.56-4.781-25.13-14.35-34.76l-174.9-174.9C249.1 4.786 236.5 0 223.1 0C211.4 0 198.9 4.786 189.2 14.35L14.35 189.2C4.783 198.9-.0011 211.4-.0011 223.1c0 12.56 4.785 25.17 14.35 34.8l174.9 174.9c9.625 9.562 22.19 14.35 34.75 14.35s25.13-4.783 34.75-14.35l174.9-174.9C443.2 249.1 447.1 236.6 447.1 224zM96 248c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1S120 210.8 120 224S109.3 248 96 248zM224 376c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1s23.1 10.75 23.1 23.1S237.3 376 224 376zM224 248c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1S248 210.8 248 224S237.3 248 224 248zM224 120c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1s23.1 10.75 23.1 23.1S237.3 120 224 120zM352 248c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1s23.1 10.75 23.1 23.1S365.3 248 352 248zM591.1 192l-118.7 0c4.418 10.27 6.604 21.25 6.604 32.23c0 20.7-7.865 41.38-23.63 57.14l-136.2 136.2v46.37C320 490.5 341.5 512 368 512h223.1c26.5 0 47.1-21.5 47.1-47.1V240C639.1 213.5 618.5 192 591.1 192zM479.1 376c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1s23.1 10.75 23.1 23.1S493.2 376 479.1 376z"/>',
viewBox: "0 0 640 512",
},
"triangle-exclamation": {
path: '<path d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/>',
viewBox: "0 0 512 512",
@@ -105,7 +105,29 @@
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",
},
"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 height = 16;
export let icon = "";
@@ -116,15 +138,16 @@
</script>
<svg
class="bi"
xmlns="http://www.w3.org/2000/svg"
{width}
{height}
viewBox={selectedIcon.viewBox}
aria-labelledby={icon}
role="presentation"
{stroke}
{fill}
class="bi"
xmlns="http://www.w3.org/2000/svg"
{width}
{height}
viewBox={selectedIcon.viewBox}
aria-labelledby={icon}
role="presentation"
{stroke}
{fill}
>
{@html selectedIcon.path}
</svg>
@@ -132,5 +155,6 @@
<style>
svg {
vertical-align: text-top;
}
</style>
+1 -1
View File
@@ -3,7 +3,7 @@
export let disabled = false;
</script>
<button type="submit" class="btn btn-primary btn-spinner" {disabled}>
<button type="submit" class="button secondary btn-spinner" {disabled}>
<span
class="spinner-border spinner-border-sm"
role="status"
+5 -16
View File
@@ -12,21 +12,10 @@
</script>
{#if isVisible}
<div
transition:fly={{ duration: 500 }}
class="lx-alert text-white bg-success border-1 border rounded px-3 py-0 text-center"
role="alert"
>
{message}
</div>
<div class="notice notice-success" transition:fly={{ duration: 500 }} role="alert">
<div class="title">Success</div>
<div class="content"> {message}</div>
</div>
{/if}
<style>
.lx-alert {
position: fixed;
left: 50%;
transform: translateX(-50%);
top: 45px;
margin: 0 auto;
}
</style>
+9
View File
@@ -0,0 +1,9 @@
<script>
export let value;
export let checked = false;
</script>
<input type="checkbox" {value} on:change
class="switch" {checked}/>
@@ -35,49 +35,47 @@
}
</script>
<div class="d-flex align-items-center mb-3">
<div style="display: flex;align-items: center; gap: 8px">
<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")}
type="button"
class="btn btn-sm btn-outline-primary">Publish
</button
>
<button
class="button">Publish
</button
>
<button
on:click|preventDefault={(e) => changeStatus(e, "draft")}
type="button"
class="btn btn-sm btn-outline-primary">Make Draft
</button
>
{#if filter["status_in"] === "trashed"}
<button
class="button">Make Draft
</button
>
{#if filter["status_in"] === "trashed"}
<button
on:click|preventDefault={(e) => changeStatus(e, "published")}
type="button"
class="btn btn-sm btn-outline-primary">Publish
</button
>
{#if schema.hasDrafts}
<button
class="button">Publish
</button
>
{#if schema.hasDrafts}
<button
on:click|preventDefault={(e) => changeStatus(e, "draft")}
type="button"
class="btn btn-sm btn-outline-primary">Make Draft
</button
>
{/if}
<button
on:click|preventDefault={deleteRecords}
type="button"
class="btn btn-sm btn-outline-primary">Delete forever
</button
>
{:else}
<button
type="button"
on:click|preventDefault={(e) => changeStatus(e, "trashed")}
class="btn btn-sm btn-outline-primary">Move to trash
class="button">Make Draft
</button
>
{/if}
</div>
<button
on:click|preventDefault={deleteRecords}
type="button"
class="button">Delete forever
</button
>
{:else}
<button
type="button"
on:click|preventDefault={(e) => changeStatus(e, "trashed")}
class="button">Move to trash
</button
>
{/if}
</div>
+15 -24
View File
@@ -4,7 +4,6 @@
import ActionsOnSelected from "./ActionsOnSelected.svelte";
import Table from "./Table.svelte";
import {getContext} from "svelte";
import Grid from "./Grid.svelte";
const axios = getContext("axios");
export let schema;
@@ -39,6 +38,7 @@
limit = response.data.limit;
total = response.data.total;
modalUrl = response.data.modalUrl;
document.querySelector("dialog h3").scrollIntoView();
})
.catch((error) => {
console.log(error);
@@ -46,8 +46,8 @@
}
</script>
<div class="wrapper-large transparent ">
<div class="lx-card mb-4 {inModal ? 'mt-0' : 'mt-5'}">
<div class="">
<div class="{inModal ? 'mt-0' : 'mt-5'}">
<h3 class="header-normal mb-5 ">
{schema.label}
</h3>
@@ -70,28 +70,19 @@
/>
{/if}
{#if schema.type === "collection"}
<Table
{records}
{graph}
{schema}
{sortParam}
{sortField}
{systemFields}
{inModal}
{users}
{isWritable}
bind:selected
/>
{:else}
<Grid
{records}
{schema}
{isWritable}
bind:selected
/>
<Table
{records}
{graph}
{schema}
{sortParam}
{sortField}
{systemFields}
{inModal}
{users}
{isWritable}
bind:selected
/>
{/if}
</div>
<Pagination
+5 -5
View File
@@ -23,7 +23,7 @@
<RenderField {record} {schema} {graph} {field}/>
</td>
{/each}
{#if schema.visible.includes("status")}
{#if schema.visible?.includes("status")}
<td
class="text-center"
class:is-sort={"-status" == sortParam || "status" == sortParam}
@@ -31,7 +31,7 @@
<Status status={record.status}/>
</td>
{/if}
{#if schema.visible.includes("_sys.createdBy")}
{#if schema.visible?.includes("_sys.createdBy")}
<td
class="text-center"
class:is-sort={"-_sys.createdBy" == sortParam || "_sys.createdBy" == sortParam}
@@ -39,7 +39,7 @@
<Avatar name={usernameById(users, record._sys.createdBy)} side={24}/>
</td>
{/if}
{#if schema.visible.includes("_sys.updatedBy")}
{#if schema.visible?.includes("_sys.updatedBy")}
<td
class="text-center"
class:is-sort={"-_sys.updatedBy" == sortParam || "_sys.updatedBy" == sortParam}
@@ -47,12 +47,12 @@
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
</td>
{/if}
{#if schema.visible.includes("_sys.createdAt")}
{#if schema.visible?.includes("_sys.createdAt")}
<td class:is-sort={"-_sys.createdAt" == sortParam || "_sys.createdAt" == sortParam}>
{friendlyDate(record._sys.createdAt)}
</td>
{/if}
{#if schema.visible.includes("_sys.updatedAt")}
{#if schema.visible?.includes("_sys.updatedAt")}
<td class:is-sort={"-_sys.updatedAt" == sortParam || "_sys.updatedAt" == sortParam}>
{friendlyDate(record._sys.updatedAt)}
</td>
+71 -47
View File
@@ -5,6 +5,9 @@
import {getContext} from "svelte";
import Avatar from "../account/Avatar.svelte";
import {selectRecord, toggleAll} from "./functions/recordSelect.js";
import Checkbox from "../common/Checkbox.svelte";
import Preview from "../files/Preview.svelte";
import {fileurl} from "../files/imageserver.js";
const channel = getContext("channel");
@@ -20,31 +23,29 @@
export let selected = [];
function eventToggleAll(e) {
selected = toggleAll(e,records,selected)
selected = toggleAll(e, records, selected)
}
function select(record) {
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>
<div class="lx-table rounded">
<table class="">
<thead class="table-light">
<div class="table mt-5 ">
<table>
<thead>
<tr>
{#if isWritable}
<th>
<input
on:change|preventDefault={eventToggleAll}
indeterminate={selected.length > 0 &&
selected.length < records.length}
checked={selected.length == records.length}
class="form-check-input"
type="checkbox"
/>
<Checkbox
value=""
on:change={eventToggleAll}
indeterminate={selected.length > 0 && selected.length < records.length}
checked={selected.length === records.length}
>
</Checkbox>
</th>
{/if}
@@ -54,13 +55,13 @@
class:is-sort={field.name === sortField.name}
scope="col"
title={field.help}
data-bs-toggle="tooltip"
data-bs-placement="top">{field.label}</th
>{field.label}</th
>
{/each}
{#each systemFields.filter(c => schema.visible.includes(c.name)) as sysField}
<th>{sysField.label}</th>
{#each systemFields.filter(c => schema.visible?.includes(c.name)) as sysField}
<th class:is-sort={sysField.name === sortField.name}>{sysField.label}</th>
{/each}
<th></th>
</tr>
</thead>
<tbody>
@@ -68,44 +69,58 @@
<tr>
<td class="title-td">
<div
class="title-td-contents d-inline-flex justify-content-between w-100 align-items-center"
class="title-td-contents"
>
<div class="d-flex align-items-center ">
{#if isWritable}
<div class="form-check">
<input
on:change={() => select(record)}
class="form-check-input "
type="checkbox"
checked={selected.find(
(r) => r.id === record.id
)}
value={record}
/>
{#if isWritable}
<Checkbox
on:change={() => select(record)}
checked={selected.find((r) => r.id === record.id)}
value={record}
>
</Checkbox>
{/if}
{#if record._file?.path}
<div class="file-table-row">
<Preview record={record} size={record._file?.width > 0 ? "medium" : "small"}/>
<div>
{#if record.status === "draft"}
<span style="text-transform: uppercase;font-size:10px">{record.status}</span>
{/if}
<a
href="{channel.lucentUrl}/records/{record.id}"
target={inModal ? "_blank" : "_self"}
>
{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>
{/if}
</div>
{:else}
<a
class="me-2 text-decoration-none text-dark fs-6"
href="{channel.lucentUrl}/records/{record.id}"
target={inModal ? "_blank" : "_self"}
title={previewTitle(channel.schemas, record, graph)}
data-bs-toggle="tooltip" data-bs-placement="left"
>
{#if record.status === "draft"}
<span style="text-transform: uppercase;font-size:10px">{record.status}</span>
{/if}
{previewTitle(channel.schemas, record, graph)}
</a>
</div>
<div>
<Avatar
name={usernameById(
users,
record._sys.updatedBy
)}
side={24}
/>
</div>
{/if}
</div>
</td>
<RecordRow
@@ -117,6 +132,15 @@
{sortField}
{users}
/>
<td>
<Avatar
name={usernameById(
users,
record._sys.updatedBy
)}
side={24}
/>
</td>
</tr>
{/each}
</tbody>
@@ -11,13 +11,8 @@
// if (edges[0]) {
// firstRecord = record._children.find((r) => r.data.id === edges[0].to);
// }
console.log(filePreviews)
</script>
<!-- {#if firstRecord}
<Preview record={firstRecord} size="tiny" />
{/if} -->
<div class="d-flex me-1">
{#each filePreviews as file}
<div class="me-1">
@@ -18,7 +18,7 @@
<div class="references">
{#each recordEdges as recordEdge}
<span class="mr-3">
<span class="reference">
<PreviewCardSmall {schemas} {graph} record={recordEdge}/>
</span>
{/each}
@@ -28,8 +28,8 @@
</script>
{#each pages as i}
<li class="page-item">
{#if currentPage == i}
<li class="page-item" class:active={currentPage === i}>
{#if currentPage === i}
<span class="page-link active">{i}</span>
{:else}
<a class="page-link" on:click={(e) => goto(e, i)} href={url(i)}
@@ -43,7 +43,7 @@
</script>
<nav>
<ul class="pagination justify-content-center">
<ul class="pagination">
{#if totalPages > 1}
<li class="page-item disabled" class:disabled={currentPage === 1}>
<a on:click={first} href="/" class="page-link"> First </a>
@@ -69,7 +69,7 @@
{/if}
</ul>
</nav>
<p class="text-muted text-center">
<p style="display: flex;justify-content: center; gap: 4px">
Showing
<span class="font-medium">{+skip + 1}</span>
to
@@ -1,6 +1,7 @@
<script>
import {createEventDispatcher, getContext} from "svelte";
import {previewTitle} from "../../records/Preview";
import Icon from "../../common/Icon.svelte";
const channel = getContext("channel");
const dispatch = createEventDispatcher();
@@ -11,7 +12,6 @@
export let inModal;
export let modalUrl;
export let graph;
let filter = {
label: "",
operator: "",
@@ -24,6 +24,9 @@
extractLabel(schema, key),
].reduce((mem, fn) => fn(mem), filter);
function extractOperator(key) {
return (filter) => {
if (filter.isReference) {
@@ -54,6 +57,7 @@
const filterRecord = extractFilterRecord(graph, value);
function extractFilterRecord(graph, value) {
if (!filter.isReference) {
return null;
}
@@ -73,30 +77,20 @@
}
</script>
<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">
<span class="applied-filter">
{#if filter.isReference && filterRecord}
{filter.label} is {previewTitle(channel.schemas, filterRecord)}
{:else}
{filter.label} {operators.find((o) => o.name === filter.operator)?.symbol ?? ""} {value}
{filter.label} {operators.find((o) => o.name === filter.operator)?.symbol ?? ""} {operators.find((o) => o.name === filter.operator)?.hasValue ? value : ""}
{/if}
<button
on:click|preventDefault={() => removeFilter(key)}
type="button"
class="btn-close btn-close ms-1"
class="button-text"
aria-label="Close"
/>
</div>
><Icon width={12} height={12} icon="close"></Icon></button>
</span>
<style>
.applied-filter {
background-color: #fff;
line-height: 22px;
}
.applied-filter:hover {
opacity: .8;
background-color: #eee;
}
</style>
@@ -0,0 +1,35 @@
<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}
+176 -84
View File
@@ -1,8 +1,8 @@
<script>
import Icon from "../../common/Icon.svelte";
import {createEventDispatcher} from "svelte";
import FilterReferenceInput from "./FilterReferenceInput.svelte";
import Dropdown from "../../common/Dropdown.svelte";
import FilterReferenceInput from "./FilterReferenceInput.svelte";
const dispatch = createEventDispatcher();
export let schema;
@@ -14,50 +14,10 @@
let dropdown;
let search = "";
let systemFieldsFiltered = systemFields;
if (schema.type == "collection") {
if (schema.type === "collection") {
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) {
e.preventDefault();
let filterKeyValue = search.split("=")[0] ?? "";
@@ -79,63 +39,195 @@
} else {
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>
<div class="mx-2 d-flex align-items-center">
<Dropdown bind:this={dropdown} width="300">
<div slot="button">
<Icon icon="filter"/>
<span class="ms-1">Filter</span>
</div>
<div class="px-3 py-1 d-flex align-items-center">
<select bind:value={selectedField} class="form-select">
{#each filterableFields as field}
<option value={field}>{field.label}</option>
{/each}
</select>
</div>
<div class="px-3 py-1 d-flex align-items-center">
<select class="form-select" bind:value={selectedOperator}>
{#each operatorsFiltered as operator}
<option value={operator}>{operator.label}</option>
{/each}
</select>
</div>
<div class="px-3 py-1 d-flex align-items-center">
{#if selectedField?.info?.name === "reference" && selectedOperator.name === "eq"}
<FilterReferenceInput field={selectedField} bind:value={selectedInput} on:addFilter={addFilter}/>
{:else}
<div>
<Dropdown bind:this={dropdown}>
<div slot="button">
<Icon icon="filter"/>
<span class="ms-1">Filter</span>
</div>
<div class:hide={activeMenu !== "main"}>
<button class="dropdown-item button" on:click={e => activeMenu = "byField" }>
Filter by field
</button>
<button class="dropdown-item button" on:click={e => activeMenu = "byReference" }>
Filter by Reference
</button>
<button class="dropdown-item button" on:click={e => activeMenu = "advanced" }>
Advanced filter
</button>
</div>
<div class:hide={activeMenu !== "byField"}>
{#if !activeField}
<button class="dropdown-item button" on:click={e => activeMenu = "main" }>
<Icon icon="arrow-left"></Icon>
Back
</button>
{#each dataFields as field}
<button class="dropdown-item button" on:click={e => selectField(e,field)}>
{field.label}
</button>
{/each}
{/if}
{#if activeField && !activeOperator}
<button class="dropdown-item button" on:click={e => activeField = null }>
<Icon icon="arrow-left"></Icon>
Back
</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
type="text"
class="form-control"
bind:value={selectedInput}
/>
{/if}
</div>
<div class="px-3 py-1 d-flex align-items-center">
</div>
<button
on:click={addFilter}
class="btn btn-outline-primary"
on:click={applyFilter}
class="button applied-filter"
type="button"
>
Add filter
</button>
</div>
{/if}
<hr/>
<div><h6 class="dropdown-header">Advanced filters</h6></div>
<form on:submit={submitSearch}>
<div class="px-3 py-1 d-flex align-items-center">
<input
bind:value={search}
type="search"
class="form-control"
placeholder="Advanced filters"
required
/>
</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}
</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}>
<input
bind:value={search}
type="search"
class="mb-2 mt-2"
placeholder="Advanced filters"
required
/>
<button class="button applied-filter">
Submit
</button>
</form>
</Dropdown>
</div>
</Dropdown>
</div>
@@ -42,35 +42,37 @@
}
</script>
<div class="reference-tags">
<input
type="search"
on:keyup={updateResults}
bind:value={search}
placeholder={"Search for "+field.label}
autocomplete="off"
/>
<input
type="search"
on:keyup={updateResults}
class="form-control dropdown-toggle"
bind:value={search}
placeholder={"Search for "+field.label}
data-bs-toggle="dropdown"
autocomplete="off"
/>
<div class="reference-tags-results">
<div class="dropdown-menu w-100">
{#if searchOptions}
{#each searchOptions as option (option.id)}
<div
class="reference-tags-option"
role="button"
tabindex="0"
on:click={(e) => apply(e, option)}
on:keypress={(e) => apply(e, option)}
>
{previewTitle(channel.schemas, option)}
</div>
{#if searchOptions}
{#each searchOptions as option (option.id)}
<div
on:click={(e) => apply(e, option)}
on:keypress={(e) => apply(e, option)}
>
<span class="dropdown-item">
{previewTitle(channel.schemas, option)}
</span>
</div>
{:else}
Start typing...
{/each}
{/if}
{:else}
<div
class="start-typing">
Start typing...
</div>
{/each}
{/if}
</div>
</div>
+73 -82
View File
@@ -1,6 +1,7 @@
<script>
import Icon from "../../common/Icon.svelte";
import {createEventDispatcher} from "svelte";
import Dropdown from "../../common/Dropdown.svelte";
const dispatch = createEventDispatcher();
export let schema;
@@ -31,101 +32,91 @@
function sortAsc(e, field) {
e.preventDefault();
let prefix = systemFields.map((el) => el.name).includes(field.name) ? "" : "data.";
let prefix = systemFields.map((el) => el.name).includes(field.name) ? "" : "data.";
return triggerSortField(prefix + field.name);
}
function sortDesc(e, field) {
e.preventDefault();
let prefix = systemFields.map((el) => el.name).includes(field.name) ? "" : "data.";
let prefix = systemFields.map((el) => el.name).includes(field.name) ? "" : "data.";
return triggerSortField("-" + prefix + field.name);
}
</script>
<div class=" ">
<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"
>
<Dropdown>
<div slot="button">
{#if sortParam.startsWith("-")}
<Icon icon="arrow-down-wide-short"/>
{:else}
<Icon icon="arrow-up-short-wide"/>
{/if}
<span class="ms-1">{sortField.label}</span>
</button>
<div class="dropdown-menu" style="width:auto;max-width:800px;">
<div class="row">
{#each sortableFields as field}
<div class="col-4 px-3 py-1 d-flex align-items-center">
<div class="btn-group w-100">
<button
on:click={(e) => sortAsc(e, field)}
title="Sort Ascending"
class="btn btn-sm {field.name == sortField.name && !sortParam.startsWith("-")
? 'btn-primary'
: 'btn-outline-primary'} "
>
<Icon icon="arrow-up-short-wide"/>
</button>
<button
on:click={(e) => sortDesc(e, field)}
title="Sort Descending"
class="btn btn-sm {field.name == sortField.name && sortParam.startsWith("-")
? 'btn-primary'
: 'btn-outline-primary'} "
>
<Icon icon="arrow-down-wide-short"/>
</button>
<button
title="Sort Ascending"
on:click={(e) => sortAsc(e, field)}
class="btn btn-sm btn-outline-primary w-100 text-nowrap"
style="overflow: hidden;"
>
{field.label}
</button>
</div>
</div>
{/each}
</div>
<h6 class="dropdown-header px-0">System</h6>
<div class="row">
{#each systemFieldsFiltered as field}
<div class="col-4 px-3 py-1 d-flex align-items-center">
<div class="btn-group w-100">
<button
on:click={(e) => sortAsc(e, field)}
title="Sort Ascending"
class="btn btn-sm {field.name == sortParam
? 'btn-primary'
: 'btn-outline-primary'} "
>
<Icon icon="arrow-up-short-wide"/>
</button>
<button
on:click={(e) => sortDesc(e, field)}
title="Sort Descending"
class="btn btn-sm {'-' + field.name == sortParam
? 'btn-primary'
: 'btn-outline-primary'} "
>
<Icon icon="arrow-down-wide-short"/>
</button>
<button
title="Sort Ascending"
on:click={(e) => sortAsc(e, field)}
class="btn btn-sm btn-outline-primary w-100 text-nowrap"
style="overflow: hidden;"
>
{field.label}
</button>
</div>
</div>
{/each}
</div>
</div>
</div>
<div>
{#each sortableFields as field}
<div class="dropdown-item">
<button
on:click={(e) => sortAsc(e, field)}
title="Sort Ascending"
class="button button-icon {field.name == sortField.name && !sortParam.startsWith("-")
? 'active'
: ''} "
>
<Icon icon="arrow-up-short-wide"/>
</button>
<button
on:click={(e) => sortDesc(e, field)}
title="Sort Descending"
class="button button-icon {field.name == sortField.name && sortParam.startsWith("-")
? 'active'
: ''} "
>
<Icon icon="arrow-down-wide-short"/>
</button>
<button
title="Sort Ascending"
on:click={(e) => sortAsc(e, field)}
class="button"
>
{field.label}
</button>
</div>
{/each}
<h6 class="dropdown-header">System</h6>
{#each systemFieldsFiltered as field}
<div class="dropdown-item">
<button
on:click={(e) => sortAsc(e, field)}
title="Sort Ascending"
class="button button-icon {field.name == sortParam
? 'active'
: ''} "
>
<Icon icon="arrow-up-short-wide"/>
</button>
<button
on:click={(e) => sortDesc(e, field)}
title="Sort Descending"
class="button button-icon {'-' + field.name == sortParam
? 'active'
: ''} "
>
<Icon icon="arrow-down-wide-short"/>
</button>
<button
title="Sort Ascending"
on:click={(e) => sortAsc(e, field)}
class="button"
>
{field.label}
</button>
</div>
{/each}
</div>
</Dropdown>
+50 -51
View File
@@ -4,7 +4,9 @@
import Icon from "../../common/Icon.svelte";
import SortFields from "./SortFields.svelte";
import AppliedFilter from "./AppliedFilter.svelte";
import {getContext, createEventDispatcher} from "svelte";
import {createEventDispatcher, getContext} from "svelte";
import Dropdown from "../../common/Dropdown.svelte";
import AppliedFilterNotLinked from "./AppliedFilterNotLinked.svelte";
const channel = getContext("channel");
@@ -47,8 +49,8 @@
}
</script>
<div class="mb-3 d-flex align-items-center justify-content-between">
<div class=" d-flex align-items-center">
<div class="toolbar">
<div class="toolbar-filters">
<SortFields
{schema}
@@ -72,78 +74,74 @@
/>
<form method="GET" on:submit={search}>
<input type="search" name="filter[search_regex]" placeholder="Search"
class="form-control" required>
<input type="search" name="filter[search_regex]" placeholder="Search"
class="search" required>
</form>
</div>
<div class="d-flex align-items-center ">
<div style="display:flex;align-items: center;gap:4px">
{#if schema.type === "collection"}
{#if !inModal && isWritable}
<a
href="{channel.lucentUrl}/records/new?schema={schema.name}"
class="btn btn-sm btn-primary"
class="button"
>
New Record
</a>
{/if}
{:else }
<div class="d-inline-block ms-1">
<div>
<Uploader {schema} on:uploadComplete={uploadComplete}/>
</div>
{/if}
{#if !inModal}
<div class="dropdown d-inline-block">
<button
class="btn btn-link btn-sm"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Dropdown orientation="right">
<div slot="button">
<Icon icon="ellipsis-vertical"/>
</button>
<ul class="dropdown-menu">
{#if filter["status_in"] === "trashed"}
{#if isWritable}
<li>
<a
class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
>
Empty trash
</a>
</li>
{/if}
{:else}
<li>
<a
class="dropdown-item"
href={csvUrl}
>Export to CSV</a
>
</li>
<li>
<a
class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
>View trashed records</a
>
</li>
</div>
{#if filter["status_in"] === "trashed"}
{#if isWritable}
<a
class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
>
Empty trash
</a>
{/if}
{:else}
<a
class="dropdown-item"
href={csvUrl}
>Export to CSV</a
>
<a
class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
>View trashed records</a
>
<a
class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}?notlinked=*"
>View unlinked records</a
>
{/if}
</Dropdown>
</ul>
</div>
{/if}
</div>
</div>
{#if Object.entries(filter).length > 0}
<div class=" d-flex mb-3">
<div class="applied-filters">
<AppliedFilterNotLinked
{inModal}
{modalUrl}
on:refresh
></AppliedFilterNotLinked>
{#if Object.entries(filter).length > 0}
{#each Object.entries(filter) as [k, v]}
<AppliedFilter
{schema}
@@ -156,6 +154,7 @@
on:refresh
/>
{/each}
</div>
{/if}
{/if}
</div>
+101
View File
@@ -0,0 +1,101 @@
<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>
@@ -0,0 +1,38 @@
<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>
+6 -2
View File
@@ -1,8 +1,12 @@
export function sortByField(from, to, edges, fieldName) {
export function sortByField(from, to, edges, fieldName, references) {
if (from === to) {
return edges;
}
let edgesTosort = edges?.filter((ed) => ed.field === fieldName && ed.depth === 1 ) ?? [];
let referenceIds = references.map(r => r.id);
let edgesTosort = edges?.filter((ed) => ed.field === fieldName && ed.depth === 1 && referenceIds.includes(ed.target)) ?? [];
let remainingEdge = edges?.filter((ed) => !(ed.field === fieldName && ed.depth === 1)) ?? [];
edgesTosort = array_move(edgesTosort,from, to);
+45 -36
View File
@@ -1,6 +1,6 @@
<script>
import Icon from "../common/Icon.svelte";
import {imgurl} from "../files/imageserver";
import {imgurl} from "./imageserver.js";
import {getContext} from "svelte";
export let record;
@@ -28,41 +28,50 @@
fontSize = "13";
}
</script>
<div style="display: flex;align-items: center;gap: 5px;">
{#if record}
{#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
{#if record._file.mime.startsWith("image")}
<!-- href={imgurl(record)} -->
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.originalName}
style="width:{imageSide}px;height:{imageSide}px"
>
</a>
<img
class="rounded w-100"
src={imgurl(channel,record)}
alt={record._file.path}
/>
</a>
{:else}
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path}
class="file-preview-small"
style="width:{imageSide}px;height:{imageSide}px"
>
<Icon icon="file" width={fileSide} height={fileSide}/>
<span class="ms-2"
>.{record._file.path.split(".").pop().toLowerCase()}</span
>
</a>
{/if}
{/if}
{/if}
{#if showFilename}
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path}
class="preview-file-filename lx-small-text text-decoration-none"
>{record._file.path}</a
>
{/if}
{#if showFilename}
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path}
class="preview-file-filename lx-small-text text-decoration-none"
>{record._file.path} </a
>
{/if}
</div>
<style>
img{
border-radius: 12px;
padding: 4px;
}
</style>
+8 -9
View File
@@ -43,16 +43,15 @@
}
</script>
<fieldset disabled={isLoading}>
<label class="btn btn-primary btn-sm btn-spinner ">
<fieldset class="upload-button" disabled={isLoading}>
<label class="button primary btn-spinner ">
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
/>
Upload file
<span
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
>
<span class="visually-hidden">Loading...</span>
</span>
<input
on:input={upload}
class="form-control"
+26 -11
View File
@@ -1,15 +1,30 @@
import {getContext} from "svelte";
export function imgurl(record) {
if(record._file.mime === "image/svg+xml"){
return fileurl(record);
export function imgurl(channel, record) {
if (record._file.mime === "image/svg+xml") {
return fileurl(channel, record);
}
const channel = getContext("channel")
return channel.filesUrl + `/thumbs/${record._file.path}`;
return channel.disks[record._file.disk] + `/thumbs/${record._file.path}`;
}
export function fileurl(record) {
const channel = getContext("channel")
return channel.filesUrl + `/${record._file.path}`;
export function fileurl(channel, record) {
return channel.disks[record._file.disk] + `/${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
@@ -1,26 +0,0 @@
<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>
+15 -18
View File
@@ -21,24 +21,21 @@
});
</script>
<div class="wrapper-normal transparent">
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
{#if records.length > 0}
<div class="lx-card mb-4">
<div class="lx-table p-0">
<table class="">
<tbody>
{#each records as record (record.id)}
<tr>
<RecordRow {graph} {record} {users}/>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
{#if records.length > 0}
{/if}
<div class="table">
<table class="">
<tbody>
{#each records as record (record.id)}
<tr>
<RecordRow {graph} {record} {users}/>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
+11 -11
View File
@@ -1,7 +1,6 @@
<script>
import {formatDistanceToNow, parseJSON} from "date-fns";
import Avatar from "../account/Avatar.svelte";
import Status from "../records/Status.svelte";
import {previewTitle} from "../records/Preview";
import Preview from "../files/Preview.svelte";
import {usernameById} from "../account/users";
@@ -19,29 +18,30 @@
</script>
<td>
<div class="row-name">
{#if record.status === "draft"}
<span class="status">DRAFT</span>
{/if}
{#if schema.type === "files"}
<Preview {record} size="tiny"/>
<Preview {record} size="tiny" showFilename={true}/>
{:else}
<a
href="{channel.lucentUrl}/records/{record.id}"
class="text-decoration-none text-dark d-block"
href="{channel.lucentUrl}/records/{record.id}"
>
{previewTitle(channel.schemas, record, graph)}
</a>
{/if}
</div>
</td>
<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 class="text-center">
<Status status={record.status}/>
</td>
<td>
<div class="d-flex">
<div style="display: flex;gap: 14px">
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
<div class="ms-2">
{frieldlyUpdatedAt}
+34
View File
@@ -0,0 +1,34 @@
<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
@@ -0,0 +1,39 @@
<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
@@ -0,0 +1,34 @@
<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}
+26 -4
View File
@@ -1,10 +1,10 @@
<script>
// https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387
import {onMount, onDestroy} from "svelte";
import {onDestroy, onMount} from "svelte";
import {basicSetup, EditorView} from "codemirror";
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
import {EditorState, Compartment} from "@codemirror/state";
import {autocompletion, completionKeymap} from "@codemirror/autocomplete";
import {Compartment, EditorState} from "@codemirror/state";
import {keymap} from "@codemirror/view";
import {indentWithTab} from "@codemirror/commands";
import {markdown} from "@codemirror/lang-markdown";
@@ -15,6 +15,29 @@
export let value;
export let editable = true;
export function insertMedia(info) {
let insertText = "";
if (info.record._file.width > 0) {
insertText = `![${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(() => {
let language = new Compartment();
let tabSize = new Compartment();
@@ -51,7 +74,6 @@
});
});
onDestroy(() => {
+10 -30
View File
@@ -1,6 +1,7 @@
<script>
import Sortable from "sortablejs";
import { onMount, createEventDispatcher } from "svelte";
import {createEventDispatcher, onMount} from "svelte";
export let sortableClass = "";
// export let handle;
export let isTable = false;
@@ -10,50 +11,29 @@
onMount(() => {
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
easing: "cubic-bezier(1, 0, 0, 1)",
direction: 'vertical',
onUpdate: function (/**Event*/ evt) {
// reorder(evt.oldIndex,evt.newIndex);
console.log(evt)
dispatch("update", {
source: evt.oldIndex,
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);
});
// 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>
{#if isTable}
<tbody class="sortable-container {sortableClass}" bind:this={sortableContainer}>
<slot />
</tbody>
<tbody class="sortable-container {sortableClass}" bind:this={sortableContainer}>
<slot/>
</tbody>
{:else}
<div class="sortable-container {sortableClass}" bind:this={sortableContainer}>
<slot />
</div>
<div class="sortable-container {sortableClass}" bind:this={sortableContainer}>
<slot/>
</div>
{/if}
+7 -4
View File
@@ -6,7 +6,8 @@
import "tinymce/icons/default";
import "tinymce/themes/silver";
import "tinymce/skins/ui/oxide/skin.css";
import contentUiSkinCss from "tinymce/skins/ui/oxide/content.css";
import contentUiSkinCss from "tinymce/skins/ui/oxide/content.css?inline";
import customcss from "./tinymce.css?inline";
import "tinymce/plugins/link";
import "tinymce/plugins/code";
@@ -52,7 +53,7 @@
toolbar_sticky: true,
skin: false,
content_css: false,
content_style: contentUiSkinCss.toString(),
content_style: contentUiSkinCss.toString() + customcss.toString(),
branding: false,
inline: false,
plugins: plugins,
@@ -67,8 +68,7 @@
browser_spellcheck: true,
max_height: 600,
// media_poster: false,
content_style:
"img {max-width: 100%;height: auto;",
setup: function (editor) {
activeEditor = editor;
@@ -100,6 +100,9 @@
tinymce.init({...config, ...additionalConfig});
});
export function insertMedia(info){
activeEditor.execCommand('InsertHTML', false, info.html);
}
</script>
+174
View File
@@ -0,0 +1,174 @@
<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
@@ -0,0 +1,68 @@
<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
@@ -0,0 +1,37 @@
.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,6 +2,7 @@
import Avatar from "../account/Avatar.svelte";
import {fly} from "svelte/transition";
import {createEventDispatcher} from "svelte";
import Dropdown from "../common/Dropdown.svelte";
const dispatch = createEventDispatcher();
export let member;
@@ -35,61 +36,50 @@
<div
transition:fly={{ duration: 200 }}
class="d-flex justify-content-between align-items-center mb-3 "
class="member-item"
>
<div class="d-flex align-items-center status-{member.roles.includes('removed') ? 'removed' : 'active'}">
<div class="member-name status-{member.roles.includes('removed') ? 'removed' : 'active'}">
<Avatar name={member.name ?? "" } side={32}/>
<div class="ms-3 ">
<div>
<div>
<span class="fs-5">
{member.name}
</span>
{member.name}
</div>
{member.email}
</div>
</div>
<div>
<div class="dropdown dropdown-center">
<button
class=" dropdown-toggle btn btn-light"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Roles
</button>
<div class="dropdown-menu">
<h6 class="dropdown-header">Remove role</h6>
{#each roles as role}
{#if member.roles.includes(role)}
<button
class="dropdown-item text-capitalize"
on:click={(e) => removeFrom(e,role)}
>
{role}
</button>
{/if}
{/each}
<div>
<hr class="dropdown-divider">
</div>
<h6 class="dropdown-header">Add role</h6>
{#each roles as role}
{#if !member.roles.includes(role)}
<button
class="dropdown-item text-capitalize"
on:click={(e) => addTo(e,role)}
>
{role}
</button>
{/if}
{/each}
<div>
{member.email}
</div>
</div>
</div>
<Dropdown orientation="right">
<div slot="button">
Roles
</div>
<h6 class="dropdown-header">Remove role</h6>
{#each roles as role}
{#if member.roles.includes(role)}
<button
class="dropdown-item button"
on:click={(e) => removeFrom(e,role)}
>
{role}
</button>
{/if}
{/each}
<h6 class="dropdown-header">Add role</h6>
{#each roles as role}
{#if !member.roles.includes(role)}
<button
class="dropdown-item button"
on:click={(e) => addTo(e,role)}
>
{role}
</button>
{/if}
{/each}
</Dropdown>
</div>
<style>
.status-removed {
+28 -30
View File
@@ -2,9 +2,9 @@
import ErrorAlert from "../common/ErrorAlert.svelte";
import SuccessAlert from "../common/SuccessAlert.svelte";
import SpinnerButton from "../common/SpinnerButton.svelte";
import Radio from "../forms/Radio.svelte";
import MemberSettingsCard from "./MemberSettingsCard.svelte";
import {getContext} from "svelte";
import axios from "axios";
const channel = getContext("channel");
@@ -60,7 +60,7 @@
}
</script>
<div class="wrapper-tiny transparent mb-5">
<div class="common-wrapper">
<div class="lx-card mt-5">
<h3 class="header-small mb-5">Invite people</h3>
<ErrorAlert message={errorMessage}/>
@@ -72,12 +72,12 @@
>Invitee Name</label
>
<input
type="text"
bind:value={name}
class="form-control"
id="inviteeName"
placeholder="Member name"
required
type="text"
bind:value={name}
class="form-control"
id="inviteeName"
placeholder="Member name"
required
/>
</div>
<div class="mb-3">
@@ -85,24 +85,24 @@
>Invitee Email Address</label
>
<input
type="email"
bind:value={email}
class="form-control"
id="inviteeEmail"
placeholder="Member email"
required
type="email"
bind:value={email}
class="form-control"
id="inviteeEmail"
placeholder="Member email"
required
/>
</div>
<div class="me-3">
{#each channel.roles.filter((r) => r !== "removed") as arole}
<Radio
bind:group={role}
value={arole}
name="role"
label={arole}
/>
{/each}
<select bind:value={role}>
{#each channel.roles.filter((r) => r !== "removed") as arole}
<option
value={arole}
>{arole}</option>
{/each}
</select>
</div>
<div class="mt-5 d-block text-center">
@@ -111,17 +111,15 @@
</form>
</div>
<div class="lx-card mt-3">
<h3 class="header-small mb-5">Members</h3>
<div class="member-list">
<h3 class="header-small mb-5 mt-5">Members</h3>
{#each users as user}
<MemberSettingsCard
member={user}
roles={channel.roles}
on:update={update}
on:reinvite={(e) => invite(e.detail.email, e.detail.role)}
member={user}
roles={channel.roles}
on:update={update}
on:reinvite={(e) => invite(e.detail.email, e.detail.role)}
/>
{/each}
</div>
</div>
+30 -39
View File
@@ -1,15 +1,15 @@
<script>
import {afterUpdate, getContext, onMount} from "svelte";
import {isEqual} from "lodash";
import Manager from "./Manager.svelte";
import EditHeader from "./EditHeader.svelte"
import StatusSelect from "./StatusSelect.svelte"
import axios from "axios";
import EditHeader from "./header/EditHeader.svelte"
import FilePreview from "./FilePreview.svelte"
import ContentTabs from "./ContentTabs.svelte"
import ContentTabs from "./header/ContentTabs.svelte"
import FormField from "./FormField.svelte"
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");
@@ -19,9 +19,9 @@
records: [],
edges: []
};
export let recordHistory;
// export let recordHistory;
export let isCreateMode;
export let isWritable = false;
// export let isWritable = false;
export let users;
let originalContent;
let activeContentTab = "";
@@ -151,51 +151,44 @@
<svelte:window on:beforeunload={beforeUnload}/>
<div class="wrapper-normal transparent">
<Manager managerRecords={recordHistory} {graph}/>
<EditHeader {schema} {record} {isCreateMode} {graph} bind:activeContentTab/>
{#if !["_graph", "_info"].includes(activeContentTab) && isWritable}
<div class="shadow-lg "
style="position:fixed;bottom:0;left:0px;width:100%;background: rgb(206, 223, 210);z-index:1050"
>
<div
class="d-flex mt-3 mb-3 align-items-center justify-content-center"
<div class="record-edit">
<div class="tools-header">
<!-- <Manager managerRecords={recordHistory} {graph}/>-->
<EditHeader {schema} bind:record {isCreateMode} bind:activeContentTab/>
{#if isCreateMode}
<button
class="button primary btn-spinner"
on:click={save}
>
<StatusSelect bind:status={record.status} {record} {schema}/>
{#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"
/>
Create
</button>
{:else if hasUnsavedData}
<button
type="button"
class="ms-2 btn btn-primary btn-spinner"
on:click={save}
>
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>
</div>
{/if}
Save
</button>
{/if}
</div>
<Title {schema} {record} {isCreateMode}/>
<ErrorAlert message={errorMessage}/>
<div class=" mt-4" style="margin-bottom:150px">
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
<ContentTabs
{schema}
{isCreateMode}
@@ -203,7 +196,6 @@
/>
{#if !["_graph", "_info"].includes(activeContentTab)}
<FilePreview {record} {schema}/>
<!-- <fieldset disabled="disabled"> -->
{#each activeFields as field (field.name)}
{#if activeContentTab === field.group}
<FormField
@@ -217,7 +209,6 @@
/>
{/if}
{/each}
<!-- </fieldset> -->
{:else if activeContentTab === "_graph"}
<Graph {graph} {record}/>
{:else if activeContentTab === "_info"}
-73
View File
@@ -1,73 +0,0 @@
<script>
import {getContext} from "svelte";
import Icon from "../common/Icon.svelte";
import {previewTitle} from "./Preview";
const channel = getContext("channel");
export let schema;
export let graph;
export let record;
export let isCreateMode;
export let activeContentTab;
function clone(e) {
e.preventDefault();
axios.post(channel.lucentUrl + "/records/clone/" + record.id).then(response => {
window.location = channel.lucentUrl + "/records/" + response.data.id;
}).catch(error => {
});
}
</script>
<h3 class="header-normal mt-5 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}
{previewTitle(channel.schemas, record, graph)}
{:else}
New Record
{/if}
</span>
{#if !isCreateMode}
<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>
+35 -40
View File
@@ -1,55 +1,50 @@
<script>
import Preview from "../files/Preview.svelte";
import {fileurl} from "../files/imageserver"
import {getContext} from "svelte"
const channel = getContext("channel");
export let record;
export let schema;
</script>
{#if schema.type === "files"}
<div class="row mb-4">
<div class="col" style="max-width:276px">
<div class="record-edit-file-preview">
<div>
<Preview {record} size="large"/>
</div>
<div class="col">
<ul class="list-group ">
<li class="list-group-item border-primary">
<span class="text-muted">Filename</span>
<span>{record._file.path}</span>
</li>
<li class="list-group-item border-primary">
<span class="text-muted">Original name</span>
<span>{record._file.originalName}</span>
</li>
<li class="list-group-item border-primary">
<span class="text-muted">Mime type</span>
<span>{record._file.mime}</span>
</li>
{#if record._file.width}
<li class="list-group-item border-primary">
<span class="text-muted">Dimensions</span>
<span>{record._file.width}x{record._file.height}</span>
</li>
{/if}
<li class="list-group-item border-primary">
<span class="text-muted">File size</span>
<span>{(record._file.size / 1024).toFixed(1)}kB</span>
</li>
<li class="list-group-item border-primary">
<span class="text-muted">Checksum</span>
<span>{record._file.checksum}</span>
</li>
<li class="list-group-item border-primary">
<span class="text-muted">Download</span>
<a href="{fileurl(record)}">{record._file.path}</a>
</li>
</ul>
<div class="file-details">
<div class="file-details-item">
<span class="text-muted">Filename</span>
<span>{record._file.path}</span>
</div>
<div class="file-details-item">
<span class="text-muted">Original name</span>
<span>{record._file.originalName}</span>
</div>
<div class="file-details-item">
<span class="text-muted">Mime type</span>
<span>{record._file.mime}</span>
</div>
{#if record._file.width}
<div class="file-details-item">
<span class="text-muted">Dimensions</span>
<span>{record._file.width}x{record._file.height}</span>
</div>
{/if}
<div class="file-details-item">
<span class="text-muted">File size</span>
<span>{(record._file.size / 1024).toFixed(1)}kB</span>
</div>
<div class="file-details-item">
<span class="text-muted">Checksum</span>
<span>{record._file.checksum}</span>
</div>
<div class="file-details-item">
<a class="button primary" target="_blank" style="display: inline-flex" href="{fileurl(channel,record)}">Download</a>
</div>
</div>
</div>
{/if}
<style>
.list-group {
font-size: 14px;
}
</style>
+23 -30
View File
@@ -2,8 +2,6 @@
import Text from "./elements/Text.svelte";
import Slug from "./elements/Slug.svelte";
import Reference from "./elements/Reference.svelte";
import ReferenceInline from "./elements/ReferenceInline.svelte";
import Block from "./block/Block.svelte";
import Color from "./elements/Color.svelte";
import Checkbox from "./elements/Checkbox.svelte";
import Number from "./elements/Number.svelte";
@@ -17,7 +15,6 @@
import Json from "./elements/JSON.svelte";
import Markdown from "./elements/Markdown.svelte";
import FieldHeader from "./elements/FieldHeader.svelte";
import ReferenceTable from "./elements/ReferenceTable.svelte";
import ReferenceTags from "./elements/ReferenceTags.svelte";
const formElements = {
@@ -47,24 +44,9 @@
const id = `field-${field.name}-${record.id}`;
</script>
<div class="card editor-field">
<FieldHeader {schema} {field} {id}/>
{#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"}
<div class="editor-field">
<FieldHeader {field} {id}/>
{#if field.info.name === "reference" && field.layout === "tags"}
<ReferenceTags
bind:graph
{id}
@@ -82,15 +64,6 @@
/>
{:else if field.info.name === "file"}
<File bind:graph {record} {field} {validationErrors}/>
{:else if field.info.name === "block"}
<Block
bind:graph
bind:value={data[field.name]}
{record}
{id}
{field}
{validationErrors}
/>
{:else if field.info.name === "text"}
<Text
bind:value={data[field.name]}
@@ -115,6 +88,26 @@
{isCreateMode}
{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}
<svelte:component
this={formElement}
+29 -121
View File
@@ -1,15 +1,9 @@
<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 {uniqBy} from "lodash";
import PreviewReference from "./previews/PreviewReference.svelte";
const channel = getContext("channel");
export let graph;
export let record;
function findEdgeField(schema, edgeField){
if(edgeField.includes(":")){
let edgeFieldAr = edgeField.split(":");
@@ -18,121 +12,35 @@
return schema.fields.find((f) => f.name === edgeField);
}
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 edgeField = findEdgeField(schema,edge.field);
let schemaField = edge.sourceSchema + edgeField;
let arecord = graph.records.find((n) => {
return n.id === edge.source;
});
if (!carry[schemaField]) {
carry[schemaField] = {
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;
}, {});
let backlinks = graph.parentEdges.map(edge => {
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
let edgeField = findEdgeField(schema,edge.field);
if(!edgeField){
return null;
}
return {
field: edgeField.label,
record: graph.records.find( record => record.id === edge.source)
}
}).filter( edgeOrNull => !!edgeOrNull)
</script>
<div class="editor-field">
{#each backlinks as backlink}
<div style="margin: 0 0 15px;position: relative;">
<span style="
font-size: 14px;
margin-bottom: 5px;
display: block;
{#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>-->
"
>In <i>{backlink.field}</i> of</span>
<PreviewReference
record={backlink.record}
hasDelete={false}
{graph}
/>
</div>
{:else}
Nothing links to this record
{/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>
{:else}
<div class="ms-2 mb-2">
<PreviewCardSmall {graph} record={node}/>
</div>
{/if}
{/each}
</div>
</div>
{/each}
<style>
</style>
</div>
+29 -44
View File
@@ -2,7 +2,7 @@
import {friendlyDate} from "../../helpers";
import Avatar from "../account/Avatar.svelte";
import {usernameById} from "../account/users";
import {isEqual, sortBy} from "lodash";
import {isEqual} from "lodash";
import Icon from "../common/Icon.svelte";
import RevisionCell from "./revisions/RevisionCell.svelte";
import {getContext} from "svelte";
@@ -123,19 +123,19 @@
</div>
</div>
</div>
<div class="lx-card mt-4">
<div class="revisions">
{#if schema.revisions > 0}
<div class="header-small mb-3">Revisions</div>
{#each revisions as revision}
{#if revision._sys.version != record._sys.version}
{#if revision._sys.version !== record._sys.version}
<div
class="row p-2 rounded"
class="revision"
class:active={revision._sys.version ===
selectedRevision?._sys.version}
>
<div class="col-2">version {revision._sys.version}</div>
<div class="col-5">
<div class="version">
<span>version {revision._sys.version}</span>
<Avatar
name={usernameById(users, revision._sys.updatedBy)}
side={24}
@@ -147,7 +147,7 @@
<button
disabled={revision._sys.version ===
selectedRevision?._sys.version}
class="btn btn-sm btn-outline-primary"
class="button"
on:click={(e) => compare(e, revision)}
>Compare
</button
@@ -164,14 +164,14 @@
</div>
<div bind:this={revisionSection}>
{#if selectedRevision}
<div class="mt-4">
<div class="selected-revision">
{#if fieldsWithDiff.length > 0}
<p class="text-center fw-bold mb-3 mt-5">
If you choose to rollback to this revision
</p>
<button
on:click={rollback}
class="btn btn-primary mb-5 d-block mx-auto"
class="button"
>
Rollback to version {selectedRevision._sys.version}
</button>
@@ -189,29 +189,25 @@
{field.label}
</div> -->
<div
class="lx-card row p-4 mb-4 w-100"
class="revision-field"
style="overflow:hidden"
>
<div class="col-5">
<div class="compare-left">
<RevisionCell
{field}
side={record.data[field.name]}
colorClass="text-danger"
/>
</div>
<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>
<Icon
icon="angle-right"
width="12"
height="12"
/>
</div>
<div class="compare-center">
<span class="me-1">{field.label}</span>
<Icon
icon="angle-right"
width="12"
height="12"
/>
</div>
<div class="col-5">
<div class="compare-right">
<RevisionCell
edges={selectedRevision._edges}
{field}
@@ -235,22 +231,25 @@
</p>
{#each Object.entries(edgeFieldsDiff) as [field, edges]}
<div
class="lx-card row p-4 mb-4 w-100"
class="revision-references"
style="overflow:hidden"
>
<div class="col-4">
<div class="reference-field">
{field}:
</div>
<div class="col-8">
<p class="mb-2 text-danger">Record</p>
<div class="reference-compare">
<p class="">Record</p>
{#each edges.record as edge}
<RevisionEdgeRow {edge} />
<RevisionEdgeRow {edge}/>
{:else}
<p>No references</p>
{/each}
<p class="mt-4 mb-2 text-success">Revision</p>
</div>
<div class="reference-compare">
<p class="text-success">Revision</p>
{#each edges.revision as edge}
<RevisionEdgeRow {edge} />
<RevisionEdgeRow {edge}/>
{:else}
<p>No references</p>
{/each}
@@ -263,17 +262,3 @@
{/if}
</div>
<style>
.label {
width: 180px;
margin-right: 10px;
margin-bottom: 4px;
display: inline-block;
}
.active {
background-color: #eee;
border: 1px solid #ccc;
}
</style>
+38 -53
View File
@@ -1,12 +1,14 @@
<script>
import {afterUpdate, createEventDispatcher, onMount,getContext} from "svelte";
import {afterUpdate, createEventDispatcher, getContext, onMount} from "svelte";
import {isEqual} from "lodash";
import FormField from "./FormField.svelte";
import FilePreview from "./FilePreview.svelte";
import ContentTabs from "./ContentTabs.svelte";
import StatusSelect from "./StatusSelect.svelte";
import ContentTabs from "./header/ContentTabs.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 dispatch = createEventDispatcher();
@@ -135,7 +137,6 @@
resolve(null);
})
.catch(function (error) {
// setOriginalContent();
if (error.response) {
if (typeof error.response.data.error === "string") {
errorMessage = error.response.data.error;
@@ -144,9 +145,6 @@
}
}
resolve(null);
// msgSuccess = null;
// msgError = error.response.data.error;
// submitted = false;
});
});
}
@@ -154,15 +152,45 @@
<svelte:window on:beforeunload={beforeUnload}/>
<div class="inline-edit my-4">
<div class="inline-edit record-edit">
<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}/>
<div class=" mt-1">
<div class=" mt-4" style="margin-bottom:150px;position:relative;">
<ContentTabs
{schema}
{isCreateMode}
bind:active={activeContentTab}
{record}
/>
<FilePreview {record} {schema}/>
<!-- <fieldset disabled="disabled"> -->
@@ -181,48 +209,5 @@
{/each}
<!-- </fieldset> -->
</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>
<style>
.inline-edit {
padding: 44px;
background-color: #eee;
border-radius: 32px;
}
</style>
-34
View File
@@ -1,34 +0,0 @@
<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>
+4 -22
View File
@@ -3,30 +3,12 @@ import {stripHtml} from "../../helpers";
export function previewTitle(schemas, record, graph) {
let schema = schemas.find((aSchema) => aSchema.name === record?.schema);
if (!schema?.titleTemplate) {
if (!schema?.cardTitle) {
return noTemplate(schema, record);
}
let recordData = record.data;
let template = Mustache.parse(schema.titleTemplate);
let referencePreviews = template
.filter(segment => segment[0] === "name") // keep only template tags
.map((segment) => segment[1]) // map to fieldNames
.filter(fieldName => { // keep only references
let schemaField = schema.fields.find(f => f.name === fieldName)
return schemaField?.info.name === "reference";
}).reduce((carry, field) => { // map to records
let edge = graph.edges.find(edge => edge.source === record.id && edge.field === field)
let referenceRecord = graph.records.find(rec => rec.id === edge?.target)
carry[field] = previewTitle(schemas, referenceRecord, graph);
return carry;
}, {});
recordData = {...recordData, ...referencePreviews}
let render = Mustache.render(schema.titleTemplate, recordData);
let render = Mustache.render(schema.cardTitle, recordData);
if (!render || render === "") {
return noTemplate(schema, record);
}
@@ -43,8 +25,8 @@ function noTemplate(schema, record) {
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name]
).slice(0, 300);
if(title == ""){
return "Untitled";
if(title.trim() === ""){
return "~Untitled~";
}
return title;
@@ -1,78 +0,0 @@
<script>
import Icon from "../common/Icon.svelte";
import { getContext, createEventDispatcher } from "svelte";
import Preview from "../files/Preview.svelte";
import { previewTitle } from "./Preview";
import Status from "./Status.svelte";
const dispatch = createEventDispatcher();
const channel = getContext("channel");
export let graph;
export let record;
export let classes = "";
export let hasDelete = false;
let schema = channel.schemas.find((aschema) => aschema.name === record.schema);
let cardTitle = previewTitle(channel.schemas, record, graph);
function remove(e) {
e.preventDefault();
dispatch("remove", record.id);
}
</script>
<div
class="card mb-2 bg-light {classes}"
style="border-color:{schema.color ?? '#ccc'}; border-width: 1px;"
>
<div class="card-body d-flex">
{#if schema.type === "files"}
<div style="max-width:94px;margin-right:15px">
<Preview {record} size="small" />
</div>
{/if}
<div class="overflow-hidden">
<a
class="title-link m-0 fs-5 text-decoration-none text-dark d-block"
href="{channel.lucentUrl}/records/{record.id}"
title={cardTitle}
>
{cardTitle}
</a>
<small class="text-muted">
{schema.label}
</small>
<small class="text-muted">
{#if record.status === "draft"}
<Status status={record.status} />
{/if}
</small>
</div>
</div>
{#if hasDelete}
<div class="position-absolute 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>
<style>
.card .trash-button {
display: none;
}
.card:hover .trash-button {
display: block;
}
.title-link {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>
@@ -1,230 +0,0 @@
<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>
@@ -13,11 +13,8 @@
{#if record?.data}
<a
href="{channel.lucentUrl}/records/{record.id}"
class="text-decoration-none rounded py-1 px-2 d-inline-block"
{title}
style="border:2px solid {!schema.color
? '#999'
: schema.color}!important;white-space: nowrap;"
class="reference"
>
{title}
</a>
@@ -1,57 +0,0 @@
<script>
import {getContext} from "svelte";
import {getStatus, getStatusList} from "./StatusText";
const channel = getContext("channel");
export let status = "draft";
export let record;
export let schema;
let dropdown;
$: currentStatus = getStatus(status);
const statusList = Object.values(getStatusList());
function updateStatus(e, statusValue) {
// e.preventDefault();
status = statusValue;
dropdown.click();
}
</script>
<!-- Example split danger button -->
<div class="d-flex justify-content-between">
<div class="btn-group dropup">
<button type="button" class="btn btn-{currentStatus.bg}"
>{currentStatus.text}</button
>
<button
bind:this={dropdown}
type="button"
class="btn btn-{currentStatus.bg} dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<div class="dropdown-menu">
<div class="dropdown-header">Change status to</div>
{#each statusList as astatus}
{#if astatus.value !== status}
<button
type="button"
class="dropdown-item my-2 rounded w-100 bg-{astatus.bg} text-{astatus.color}"
on:click={(e) => updateStatus(e, astatus.value)}
>
{astatus.text}
</button>
{/if}
{/each}
</div>
</div>
{#if channel.previewTarget}
<a href="{channel.previewTargetUrl}?schema={schema.name}&id={record.id}" target="_blank" class="btn btn-info ms-3">
Preview
</a>
{/if}
</div>
@@ -1,37 +0,0 @@
<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>
@@ -1,61 +0,0 @@
<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>
@@ -1,158 +0,0 @@
<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
@@ -1,25 +0,0 @@
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;
}, []);
}
@@ -1,105 +0,0 @@
<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 "../../elements/reference";
import BrowseModal from "../../elements/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}/>
@@ -1,13 +0,0 @@
<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>
@@ -1,15 +0,0 @@
<script>
import Codemirror from "../../../libs/CodemirrorMarkdown.svelte";
export let block;
// export let id;
</script>
<div class="mb-3">
<Codemirror bind:value={block.value} />
</div>
@@ -1,71 +0,0 @@
<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 "../../elements/reference";
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}
@@ -1,10 +0,0 @@
<script>
import Tinymce from "../../../libs/Tinymce.svelte";
export let block;
let additionalConfig = {};
</script>
<div class="mb-0">
<Tinymce bind:value={block.value} {additionalConfig}/>
</div>
@@ -1,39 +0,0 @@
<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>
@@ -1,114 +0,0 @@
<script>
import {createEventDispatcher, getContext} from "svelte";
import Index from "../../content/Index.svelte";
const dispatch = createEventDispatcher();
const channel = getContext("channel");
$: data = {};
let isOpen = false;
let selectedRecords = [];
// onMount(() => {
// load();
// });
export function open(schema) {
isOpen = true;
load(schema);
}
export function close() {
isOpen = false;
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",
});
}
function replace(e) {
e.preventDefault();
dispatch("insert", {
records: selectedRecords,
action: "replace",
});
}
</script>
{#if data.schema}
<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">
<div class="d-flex align-items-center">
<button
type="button"
class="btn btn-primary me-1"
on:click={insert}
disabled={selectedRecords.length === 0}
>
Insert
</button>
<button
type="button"
class="btn btn-outline-primary me-3"
on:click={replace}
disabled={selectedRecords.length === 0}
>
Replace
</button>
{#if selectedRecords.length > 0}
<span class="">
{selectedRecords.length} records selected
</span>
{/if}
</div>
<button
on:click|preventDefault={(e) => (isOpen = false)}
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
/>
</div>
<div class="modal-body">
<Index {...data} bind:selected={selectedRecords}/>
</div>
</div>
</div>
</div>
{/if}
<style>
.modal-dialog {
width: auto;
max-width: 100%;
}
.modal-content {
margin: 40px auto;
width: auto;
height: 100%;
}
</style>
@@ -8,7 +8,7 @@
$: errorMessage = getErrorMessage(validationErrors, field.name);
</script>
<div>
<div class="field-checkbox">
<div class="form-check form-check-inline">
<input
class="form-check-input"
+12 -13
View File
@@ -1,5 +1,6 @@
<script>
import { getErrorMessage } from "./errorMessage";
import {getErrorMessage} from "./errorMessage";
export let field;
export let value;
export let isCreateMode;
@@ -9,23 +10,21 @@
</script>
<div class="mb-0">
<div class="input-group ">
<div style="width:64px;">
<input
<div style="display: flex; align-items: center;gap: 10px">
<input
type="color"
{id}
class="form-control form-control-color"
style="border: none;background: transparent;padding: 0;width:64px;"
disabled={field.readonly && !isCreateMode}
bind:value
/>
</div>
/>
<input
type="text"
class:is-invalid={errorMessage}
{id}
class="form-control"
bind:value
readonly={field.readonly && !isCreateMode}
type="text"
class:is-invalid={errorMessage}
{id}
class="form-control"
bind:value
readonly={field.readonly && !isCreateMode}
/>
</div>
@@ -1,70 +0,0 @@
<script>
import {getContext} from "svelte";
import {debounce} from "lodash";
import {previewTitle} from "../Preview";
const channel = getContext("channel");
export let field;
export let value;
export let search;
$: options = [];
export const update = debounce((e) => {
axios
.get("/records/suggestions", {
params: {
schema: field.optionsFrom,
field: field.optionsField,
value: search,
ui: field.ui,
},
})
.then((response) => {
options = response.data;
})
.catch((error) => {
console.log(error);
});
}, 500);
function select(e, option) {
e.preventDefault();
value = option.data[field.optionsField];
search = "";
}
</script>
{#if field.optionsFrom}
{#each options as option (option.id)}
<div
on:click={(e) => select(e, option)}
on:keypress={(e) => select(e, option)}
>
<span class="dropdown-item">
{previewTitle(channel.schemas, option)}
<small class="text-muted "
>{option.data[field.optionsField]}</small
>
</span>
</div>
{:else}
{#if search && field.optionsSuggest}
<div
on:click={(e) => {
value = search;
search = "";
}}
on:keypress={(e) => {
value = search;
search = "";
}}
>
<span class="dropdown-item">
Add "{search}"
</span>
</div>
{:else}
No results
{/if}
{/each}
{/if}
+4 -59
View File
@@ -1,21 +1,16 @@
<script>
import Datalist from "./Datalist.svelte";
import {onMount} from "svelte";
import flatpickr from "flatpickr";
import "flatpickr/dist/flatpickr.css";
import "flatpickr/dist/themes/light.css";
import {getErrorMessage} from "./errorMessage";
import Icon from "../../common/Icon.svelte";
export let field;
export let value;
export let id;
export let isCreateMode;
export let validationErrors;
$: search = "";
$: listMode = field.optionsFrom && !(field.readonly && !isCreateMode);
$: errorMessage = getErrorMessage(validationErrors, field.name);
let list;
let pickerInput;
let pickerInstance;
let flatpickrOptions = {
@@ -34,9 +29,7 @@
onMount(() => {
if (!field.readonly || isCreateMode) {
if (listMode) {
flatpickrOptions.clickOpens = false;
}
pickerInstance = flatpickr(pickerInput, flatpickrOptions);
}
});
@@ -44,55 +37,8 @@
<div class="mb-0">
{#if listMode}
<div class="dropdown d-flex">
<input
type="search"
{id}
on:keyup={list.update}
on:focus={list.update}
class="form-control dropdown-toggle"
class:is-invalid={errorMessage}
bind:value={search}
bind:this={pickerInput}
placeholder="Search for options"
data-bs-toggle="dropdown"
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
<button
class="btn btn-light ms-1"
on:click|preventDefault={(e) => pickerInstance.open()}
>
<Icon icon="calendar"/>
</button>
<ul class="dropdown-menu w-100">
{#if field.optionsFrom}
<Datalist
{field}
bind:this={list}
bind:value
bind:search
/>
{/if}
</ul>
</div>
{#if value}
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
<div class="d-flex align-items-center ">
{value}
<button
on:click|preventDefault={(e) => (value = "")}
type="button"
class="btn-close btn-sm ms-1"
style="font-size:10px"
aria-label="Close"
/>
</div>
</span>
{/if}
{:else}
<input
<input
type="text"
{id}
class="form-control"
@@ -101,8 +47,7 @@
bind:this={pickerInput}
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
{/if}
/>
{#if errorMessage}
<div class="invalid-feedback d-block">
@@ -1,27 +1,25 @@
<script>
import Datalist from "./Datalist.svelte";
import {onMount} from "svelte";
import flatpickr from "flatpickr";
import "flatpickr/dist/flatpickr.css";
import "flatpickr/dist/themes/light.css";
import {getErrorMessage} from "./errorMessage";
import Icon from "../../common/Icon.svelte";
export let field;
export let value;
export let isCreateMode;
export let validationErrors;
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
$: search = "";
$: listMode = field.optionsFrom && !(field.readonly && !isCreateMode);
$: errorMessage = getErrorMessage(validationErrors, field.name);
export let id;
let list;
let wrapperDiv;
let pickerInput;
let pickerInstance;
let flatpickrOptions = {
enableTime: false,
appendTo: wrapperDiv,
static: true,
allowInput: true,
altInput: true,
altFormat: "Y-m-d H:i:S",
@@ -41,64 +39,14 @@
onMount(() => {
if (!field.readonly || isCreateMode) {
if (listMode) {
flatpickrOptions.clickOpens = false;
}
pickerInstance = flatpickr(pickerInput, flatpickrOptions);
}
});
</script>
<div class="mb-0">
{#if listMode}
<div class="dropdown d-flex">
<input
type="search"
{id}
on:keyup={list.update}
on:focus={list.update}
class="form-control dropdown-toggle"
class:is-invalid={errorMessage}
bind:value={search}
bind:this={pickerInput}
placeholder="Search for options"
data-bs-toggle="dropdown"
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
<button
class="btn btn-light ms-1"
on:click|preventDefault={(e) => pickerInstance.open()}
>
<Icon icon="calendar"/>
</button>
<ul class="dropdown-menu w-100">
{#if field.optionsFrom}
<Datalist
{field}
bind:this={list}
bind:value
bind:search
/>
{/if}
</ul>
</div>
{#if value}
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
<div class="d-flex align-items-center ">
{value}
<button
on:click|preventDefault={(e) => (value = "")}
type="button"
class="btn-close btn-sm ms-1"
style="font-size:10px"
aria-label="Close"
/>
</div>
</span>
{/if}
{:else}
<input
<div class="mb-0" bind:this={wrapperDiv}>
<input
type="text"
{id}
class="form-control"
@@ -107,10 +55,9 @@
bind:this={pickerInput}
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
{/if}
<small class=" text-primary opacity-50"
>Dates are displayed according to your timezone: {timezone}</small
/>
<span class="system-help-text"
>Dates are displayed according to your timezone: {timezone}</span
>
{#if errorMessage}
@@ -3,20 +3,20 @@
export let id;
</script>
<div class="mb-1">
<div class="d-flex justify-content-between">
<div>
<label for={id} class="form-label"
<div class="field-header">
<div class="labels">
<div class="label-and-help">
<label for={id}
>{field.label}</label
>
{#if field.help}
<small class=" text-primary opacity-50">{field.help}</small>
<small class="help-text light-text">{field.help}</small>
{/if}
</div>
<span
tabindex="-1"
class="text-decoration-none"
><code class="text-primary opacity-50">{field.name}</code>
><code class="field-id">{field.name}</code>
</span>
</div>
</div>
+29 -61
View File
@@ -1,22 +1,21 @@
<script>
import {getContext} from "svelte";
import {uniqBy} from "lodash";
import {sortByField} from "../../edges/sortEdges";
import PreviewCard from "../PreviewCard.svelte";
import Sortable from "../../libs/Sortable.svelte";
import BrowseModal from "./BrowseModal.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);
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) =>
@@ -36,81 +35,50 @@
}
async function reorder(e) {
graph.edges = await sortByField(e.detail.source, e.detail.target, graph.edges, field.name);
graph.edges = await sortByField(e.detail.source, e.detail.target, graph.edges, field.name, references);
}
function insert(e) {
e.preventDefault();
browseModal.close();
const recordsToInsert = e.detail.records;
const action = e.detail.action;
let newEdges = recordsToInsert.map((r) => {
return {
target: r.id,
source: record.id,
sourceSchema: record.schema,
targetSchema: r.schema,
field: field.name,
rank: ""
};
});
let replacedEdges = graph.edges ?? [];
if (action === "replace") {
replacedEdges = replacedEdges.filter((e) => e.field !== field.name);
}
graph.records = uniqBy([...graph.records, ...recordsToInsert], (r) => r.id);
graph.edges = uniqBy([...replacedEdges, ...newEdges], (e) => e.target + e.field);
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action);
}
</script>
<div class="mb-0">
{#if field.collections.length === 1}
<button
class="btn btn-outline-primary"
on:click={(e) => openBrowseModal(e, collections[0].name)}
class="button"
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"
>
<Dropdown>
<div slot="button">
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>
</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="row row-cols-3 mt-3" on:update={reorder}>
<Sortable sortableClass="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}
/>
<!--This div helps the sorting thing-->
<div>
<PreviewFile record={reference} hasDelete={true} on:remove={removeReference}></PreviewFile>
</div>
{/each}
</Sortable>
{/if}
<BrowseModal bind:this={browseModal} on:insert={insert}/>
<Dialog bind:this={browseModal} on:insert={insert}></Dialog>
@@ -1,21 +1,37 @@
<script>
import Codemirror from "../../libs/CodemirrorMarkdown.svelte";
import { getErrorMessage } from "./errorMessage";
import RichEditorFiles from "./RichEditorFiles.svelte";
export let value;
export let field;
export let graph;
export let record;
export let isCreateMode;
// export let id;
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
let editor;
function insertMedia(e){
editor.insertMedia(e.detail)
}
</script>
<div class="mb-3">
<Codemirror bind:value editable={!field.readonly || isCreateMode} />
<Codemirror bind:this={editor} bind:value editable={!field.readonly || isCreateMode} />
{#if field.collections.length > 0}
<RichEditorFiles
bind:graph
{record}
{field}
{validationErrors}
on:editor-insert={insertMedia}
>
</RichEditorFiles>
{/if}
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
+2 -51
View File
@@ -1,14 +1,11 @@
<script>
import Datalist from "./Datalist.svelte";
import {getErrorMessage} from "./errorMessage";
export let field;
export let value;
export let schemas;
export let validationErrors;
export let isCreateMode;
export let id;
$: search = "";
$: errorMessage = getErrorMessage(validationErrors, field.name);
let list;
@@ -23,56 +20,11 @@
return parseFloat(number).toFixed(field.decimals);
}
$: listMode = field.optionsFrom && !(field.readonly && !isCreateMode);
</script>
<div class="mb-0">
{#if listMode}
<div class="dropdown">
<input
type="number"
{id}
on:keyup={list.update}
on:focus={list.update}
bind:value={search}
placeholder="Search for options"
class="form-control dropdown-toggle"
class:is-invalid={errorMessage}
data-bs-toggle="dropdown"
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
<ul class="dropdown-menu w-100">
{#if field.optionsFrom}
<Datalist
{field}
bind:this={list}
{schemas}
bind:value
bind:search
/>
{/if}
</ul>
</div>
{#if value}
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
<div class="d-flex align-items-center ">
{value}
<button
on:click|preventDefault={(e) => (value = "")}
type="button"
class="btn-close btn-sm ms-1"
style="font-size:10px"
aria-label="Close"
/>
</div>
</span>
{/if}
{:else}
<input
<input
type="number"
{id}
class="form-control"
@@ -81,8 +33,7 @@
bind:value
autocomplete="off"
readonly={field.readonly && !isCreateMode}
/>
{/if}
/>
{#if errorMessage}
<div class="invalid-feedback d-block">
@@ -1,11 +1,12 @@
<script>
import {getContext} from "svelte";
import {insertEdges} from "./reference";
import PreviewCard from "../PreviewCard.svelte";
import {getErrorMessage} from "./errorMessage";
import {sortByField} from "../../edges/sortEdges";
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
import Sortable from "../../libs/Sortable.svelte";
import PreviewReference from "../previews/PreviewReference.svelte";
import axios from "axios";
const channel = getContext("channel");
export let record;
@@ -14,6 +15,7 @@
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
$: references = graph.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
@@ -32,12 +34,22 @@
}
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) {
e.preventDefault();
graph = insertEdges(graph,record,e.detail.records,field.name,e.detail.action);
// axios.post(channel.lucentUrl + "/edges/insert-many", {
// 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>
@@ -49,22 +61,21 @@
{/if}
<div class="inline-card-wrapper">
<ReferenceInlineButtons
buttonClass="mt-2"
recordId={null}
schemas={collections}
on:insert={insert}
on:save={insert}
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>
<PreviewReference
{graph}
record={reference}
hasDelete={true}
on:remove={removeReference}
/>
</div>
{/each}
@@ -1,167 +0,0 @@
<script>
import {getContext} from "svelte";
import {uniqBy} from "lodash";
import PreviewCardInline from "../PreviewCardInline.svelte";
import {getErrorMessage} from "./errorMessage";
import {sortByField} from "../../edges/sortEdges";
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
import {flip} from "svelte/animate";
import {quintOut} from 'svelte/easing';
const channel = getContext("channel");
export let field;
export let record;
export let graph;
export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name);
$: references = graph.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id == edge.target && record.id == edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
);
function handleInlinesaved(e) {
const updatedRecord = e.detail.records[0];
graph.edges = graph.edges.map((child) => {
if (child.source === updatedRecord.id) {
return updatedRecord;
}
return child;
});
}
function removeReference(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === field.name)
);
}
function trashReference(e) {
e.preventDefault();
graph.edges = graph.edges.map((edge) => {
if (edge.target === e.detail && edge.field === field.name) {
edge._isTrashed = true;
}
return edge;
});
}
function undoRemoveReference(e) {
e.preventDefault();
graph.edges = graph.edges.map((edge) => {
if (edge.target === e.detail && edge.field === field.name) {
delete edge._isTrashed;
}
return edge;
});
}
function insert(e) {
e.preventDefault();
const recordsToInsert = e.detail.records;
const insertAfter = e.detail.after ?? null;
const action = e.detail.action;
let newEdges = recordsToInsert.map((r) => {
return {
target: r.id,
source: record.id,
sourceSchema: record.schema,
targetSchema: r.schema,
field: field.name,
rank: ""
};
});
let replacedEdges = graph.edges;
if (action === "replace") {
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
}
graph.records = uniqBy([...graph.records, ...recordsToInsert], (r) => r.id);
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.target + edge.field);
if (!insertAfter) {
graph.edges = uniqBy(
[...newEdges, ...replacedEdges],
(edge) => edge.target + edge.field
);
} else {
let isAfter = false;
let beforeAfter = replacedEdges.reduce(
(c, edge) => {
if (isAfter) {
c.after.push(edge);
} else {
c.before.push(edge);
}
if (isAfter === false && edge.target === insertAfter) {
isAfter = true;
}
return c;
},
{before: [], after: []}
);
graph.edges = uniqBy(
[...beforeAfter.before, ...newEdges, ...beforeAfter.after],
(e) => e.target + e.field
);
}
}
function move(e, from, to) {
graph.edges = sortByField(from, to, graph.edges, field.name);
}
</script>
{#if errorMessage}
<div class="invalid-feedback d-block mb-3">
{errorMessage}
</div>
{/if}
<div class="inline-card-wrapper">
<ReferenceInlineButtons
{field}
recordId={null}
schemas={collections}
on:insert={insert}
on:save={insert}
/>
</div>
{#if references.length > 0}
{#each references as reference, i (reference.id)}
<div class="inline-card-wrapper" animate:flip="{{delay: 250, duration: 250, easing: quintOut}}">
<PreviewCardInline
isFirst={i === 0}
isLast={i + 1 === references.length}
bind:record={reference}
toDelete={graph.edges.find(
(edge) =>
edge.field === field.name && edge.target === reference.id
)._isTrashed}
on:inlinesaved={handleInlinesaved}
on:moveup={(e) => move(e, i, i - 1)}
on:movedn={(e) => move(e, i, i + 1)}
on:remove={removeReference}
on:undoremove={undoRemoveReference}
on:trash={trashReference}
/>
<ReferenceInlineButtons
{field}
recordId={reference.id}
schemas={collections}
on:insert={insert}
on:save={insert}
/>
</div>
{/each}
{/if}
@@ -2,18 +2,17 @@
import {createEventDispatcher, getContext} from "svelte";
import Icon from "../../common/Icon.svelte";
import InlineEdit from "../InlineEdit.svelte";
import BrowseModal from "./BrowseModal.svelte";
import Dialog from "../../dialog/Dialog.svelte";
import DialogRecord from "../../dialog/DialogRecord.svelte";
import axios from "axios";
import Dropdown from "../../common/Dropdown.svelte";
const dispatch = createEventDispatcher();
// export let field;
// export let buttonLabel = "";
// export let buttonClass = "";
const channel = getContext("channel");
export let schemas;
export let recordId;
$: showOptions = false;
let browseModal;
let dialogRecord;
let inLineCreateRecord;
function openBrowseModal(e, schema) {
@@ -25,6 +24,7 @@
e.preventDefault();
console.log("Save inline");
inLineCreateRecord = null;
dialogRecord.close()
dispatch("save", {
records: e.detail.records,
after: recordId,
@@ -34,20 +34,21 @@
function insert(e) {
e.preventDefault();
browseModal.close();
showOptions = false;
dispatch("insert", {
records: e.detail.records,
schema: e.detail.schema,
after: recordId,
});
}
function createInlineReference(e, schemaUId) {
e.preventDefault();
inLineCreateRecord = null;
axios
.get(channel.lucentUrl + "/records/newInline?schema=" + schemaUId)
.then((response) => {
inLineCreateRecord = response.data;
showOptions = false;
dialogRecord.open()
})
.catch((error) => {
console.log(error);
@@ -56,94 +57,60 @@
</script>
{#if schemas.length > 1}
<button
type="button"
class:is-first={!recordId}
class=" btn btn-lg btn-link text-decoration-none inline-card-button"
on:click|preventDefault={(e) => (showOptions = !showOptions)}
<div
style="display: flex;align-items: center;gap:4px"
>
<Icon width={24} height={24} icon="circle-plus"/>
</button>
{#if showOptions}
<div class="bg-light lx-card d-flex">
<Dropdown>
<div slot="button">New</div>
{#each schemas as schema}
<div
class="lx-card p-4 text-center me-4"
style="max-width: 250px;"
>
<p>{schema.label}</p>
<div class="mb-2">
<button
class="btn btn-sm btn-primary"
on:click={(e) =>
<button
class=" button"
on:click={(e) =>
createInlineReference(e, schema.name)}
>New
</button>
<button
class="btn btn-sm btn-outline-primary"
on:click={(e) => openBrowseModal(e, schema.name)}
>
<Icon icon="magnifying-glass"/>
</button
>
</div>
</div>
>{schema.label}
</button>
{/each}
</div>
{/if}
</Dropdown>
<Dropdown>
<div slot="button"> <Icon icon="magnifying-glass"/></div>
{#each schemas as schema}
<button
class="button"
on:click={(e) => openBrowseModal(e, schema.name)}
>{schema.label}
</button>
{/each}
</Dropdown>
</div>
{:else}
<div class="pb-2 text-start">
<div class="mb-2">
<button
class="btn btn-sm btn-primary"
on:click={(e) => createInlineReference(e, schemas[0].name)}
>New
</button>
<button
class="btn btn-sm btn-outline-primary"
on:click={(e) => openBrowseModal(e, schemas[0].name)}
>
<Icon icon="magnifying-glass"/>
</button
>
</div>
<div style="display:flex;align-items: center;gap: 4px">
<button
class="button"
on:click={(e) => createInlineReference(e, schemas[0].name)}
>New
</button>
<button
class="button"
on:click={(e) => openBrowseModal(e, schemas[0].name)}
>
<Icon icon="magnifying-glass"/>
</button
>
</div>
{/if}
{#if inLineCreateRecord}
<InlineEdit
{...inLineCreateRecord}
on:cancel={(e) => (inLineCreateRecord = null)}
on:inlinesaved={save}
/>
{/if}
<DialogRecord bind:this={dialogRecord}>
{#if inLineCreateRecord}
<InlineEdit
{...inLineCreateRecord}
isCreateMode={true}
on:cancel={(e) => (inLineCreateRecord = null)}
on:inlinesaved={save}
/>
<BrowseModal bind:this={browseModal} on:insert={insert}/>
{/if}
</DialogRecord>
<style>
:global(.inline-card-wrapper) {
display: flex;
flex-direction: column;
}
:global(.inline-card-wrapper .inline-card-button) {
visibility: hidden;
}
:global(.inline-card-wrapper .inline-card-button.is-first) {
visibility: visible;
}
:global(.inline-card-wrapper:hover .inline-card-button) {
visibility: visible;
}
.inline-card-button {
/* padding: 0 5px; */
display: inline-block;
z-index: 1;
margin: 10px auto 0;
}
</style>
<Dialog bind:this={browseModal} on:insert={insert}/>

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