Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c39ec469df | |||
| 232fcc8845 | |||
| 9d5d4dd930 | |||
| c507dc6031 | |||
| 843f560710 | |||
| 7574d67d80 | |||
| 19931cb4d1 | |||
| 6458c1e71d | |||
| 63232585ab | |||
| 6d15591601 | |||
| 32c8378020 | |||
| d0cd8228cc | |||
| c45a3847f8 | |||
| c0b3878674 | |||
| f868219981 | |||
| 8ac0567e66 | |||
| 02f8f5970a | |||
| 0cd4e08716 | |||
| cf3d621587 | |||
| 6fc0a65b6f | |||
| a73ee21568 | |||
| ff54bcc2ef | |||
| ab1517cc8f | |||
| 9f724a3243 | |||
| ae65ca47f6 | |||
| 74d2fcc4fa | |||
| 82174afdea | |||
| ffc39f078d | |||
| 7c4e19afbc | |||
| 7b10bfca1d | |||
| 0e5ac08641 | |||
| 1505aaa909 | |||
| d9e2c4954a | |||
| 97ad9de3d2 | |||
| 9e140be0ec | |||
| a737c2d571 | |||
| c43c29eb14 | |||
| 0c00f76657 | |||
| 4165bfb95d | |||
| 570dbf747e | |||
| 14cbd0a845 | |||
| c99634bb46 | |||
| 246696f331 | |||
| 0643578d15 | |||
| 3aa9191cba | |||
| c97be8666e | |||
| 509d7c13f2 | |||
| 50c8af7bda | |||
| 5d6869c118 | |||
| ec15f21e67 | |||
| 36165444cf | |||
| 322962403d | |||
| db37653748 | |||
| 5a13ddb2ec | |||
| 9bbd53b586 | |||
| a04e338ce2 | |||
| 2429d4acb5 | |||
| 113533408d | |||
| f9806f60c9 | |||
| 1f3ebafe69 | |||
| 1ab3f678b7 |
@@ -9,8 +9,8 @@ include_toc: true
|
||||
|
||||
### Requirements
|
||||
|
||||
- PHP 8.2
|
||||
- Laravel 10
|
||||
- PHP 8.3
|
||||
- Laravel 11
|
||||
- Postgres or Sqlite database
|
||||
- ImageMagick
|
||||
|
||||
@@ -82,7 +82,9 @@ return [
|
||||
### Database
|
||||
|
||||
The recommended database for small website is sqlite. But you can also use postresql
|
||||
Make sure to delete the existing migration scripts in your database/migrations folder.
|
||||
|
||||
> [!CAUTION]
|
||||
> Make sure to delete the existing migration scripts in your database/migrations folder.
|
||||
|
||||
Then run:
|
||||
|
||||
@@ -90,6 +92,26 @@ Then run:
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
### File Storage
|
||||
|
||||
You can use your local filesystem or s3 compatible storage. Lucent expects you to have a valid configuration inside ``config/filesystems.php``
|
||||
|
||||
example:
|
||||
|
||||
```php
|
||||
return [
|
||||
'disks' => [
|
||||
'lucent' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => true,
|
||||
],
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
### First user
|
||||
|
||||
To create your first user, head to your localhost:8000/lucent
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# Upgrade from 1.1.* to 1.2.0
|
||||
|
||||
## lucent.php config file
|
||||
|
||||
There is now an array of commands, accepting more than one.
|
||||
|
||||
from
|
||||
```php
|
||||
"generateCommand" => env("LUCENT_GENERATE_COMMAND", "generate:static"),
|
||||
```
|
||||
|
||||
to
|
||||
```php
|
||||
"commands" => [
|
||||
"generate:static" => "Build Website",
|
||||
],
|
||||
```
|
||||
## config/filesystems.php
|
||||
|
||||
Lucent has its own filesystem.
|
||||
|
||||
You should now add:
|
||||
|
||||
```
|
||||
'lucent' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
],
|
||||
```
|
||||
+7
-5
@@ -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
File diff suppressed because it is too large
Load Diff
+5
-11
@@ -24,7 +24,8 @@ There are 3 types of schemas
|
||||
- **fields**: The list of your fields. Look the field reference for more
|
||||
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar. Default: false _optional_
|
||||
- **sortBy**: The default sorting in the content browser _optional_
|
||||
- **titleTemplate**: Mustache code to customize the preview field _optional_
|
||||
- **cardTitle**: Mustache code to customize the preview field _optional_
|
||||
- **cardImage**: Field name of image you want to use as a preview image _optional_
|
||||
- **revisions**: How many revisions are going to be kept for each record _optional_
|
||||
- **read**: Array of user groups that have read permissions _optional_
|
||||
- **write**: Array of user groups that have write permissions _optional_
|
||||
@@ -40,20 +41,12 @@ There are 3 types of schemas
|
||||
- **fields**: The list of your fields. Look the field reference for more
|
||||
- **isEntry**: If this schema is important, it will show be visible on the main the sidebar _optional_
|
||||
- **sortBy**: The default sorting in the content browser _optional_
|
||||
- **titleTemplate**: Mustache code to customize the preview field _optional_
|
||||
- **cardTitle**: Mustache code to customize the preview field _optional_
|
||||
- **revisions**: How many revisions are going to be kept for each record _optional_
|
||||
- **read**: Array of user groups that have read permissions _optional_
|
||||
- **write**: Array of user groups that have write permissions _optional_
|
||||
|
||||
|
||||
## Block Reference
|
||||
|
||||
- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts
|
||||
- **label**: The friendly name of the schema
|
||||
- **type**: The type of the collection. Should be "block"
|
||||
- **fields**: The list of your fields. Look the field reference for more
|
||||
|
||||
|
||||
A full Collection example without the fields:
|
||||
|
||||
```json
|
||||
@@ -74,7 +67,8 @@ A full Collection example without the fields:
|
||||
"SEO"
|
||||
],
|
||||
"sortBy": "-_sys.createdAt",
|
||||
"titleTemplate": "{{name}} {{slug}}",
|
||||
"schemaTitle": "{{name}} {{slug}}",
|
||||
"schemaImage": "cover",
|
||||
"revisions": 15,
|
||||
"read": [
|
||||
"admin",
|
||||
|
||||
Vendored
+342
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-5
File diff suppressed because one or more lines are too long
Vendored
-188
File diff suppressed because one or more lines are too long
Vendored
+3
-6
@@ -1,14 +1,11 @@
|
||||
{
|
||||
"main.js": {
|
||||
"file": "assets/main.c1fd60c7.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"
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -5,6 +5,9 @@
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import {loadHtmxFormsBehaviour} from "./htmx-form.js";
|
||||
|
||||
loadHtmxFormsBehaviour();
|
||||
window.axios = axios;
|
||||
export const axiosInstance = axios;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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}/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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}> </div>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.logs{
|
||||
max-height: 70vh;
|
||||
overflow: scroll;
|
||||
background: var(--p90);
|
||||
color: var(--p10);
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -1,10 +1,10 @@
|
||||
<script>
|
||||
|
||||
// https://codesandbox.io/s/codemirror-remark-editor-4m4z9?file=/src/CodeEditor.js:374-387
|
||||
import {onMount, onDestroy} from "svelte";
|
||||
import {onDestroy, onMount} from "svelte";
|
||||
import {basicSetup, EditorView} from "codemirror";
|
||||
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
|
||||
import {EditorState, Compartment} from "@codemirror/state";
|
||||
import {autocompletion, completionKeymap} from "@codemirror/autocomplete";
|
||||
import {Compartment, EditorState} from "@codemirror/state";
|
||||
import {keymap} from "@codemirror/view";
|
||||
import {indentWithTab} from "@codemirror/commands";
|
||||
import {markdown} from "@codemirror/lang-markdown";
|
||||
@@ -15,6 +15,29 @@
|
||||
export let value;
|
||||
export let editable = true;
|
||||
|
||||
export function insertMedia(info) {
|
||||
let insertText = "";
|
||||
if (info.record._file.width > 0) {
|
||||
insertText = ``;
|
||||
} else {
|
||||
insertText = `[${info.record._file.originalName}](${info.originalUrl})`;
|
||||
}
|
||||
const cursor = codeMirrorView.state.selection.main.head;
|
||||
const transaction = codeMirrorView.state.update({
|
||||
changes: {
|
||||
from: cursor,
|
||||
insert: insertText,
|
||||
},
|
||||
// the next 2 lines will set the appropriate cursor position after inserting the new text.
|
||||
selection: {anchor: cursor + 1},
|
||||
scrollIntoView: true,
|
||||
});
|
||||
|
||||
if (transaction) {
|
||||
codeMirrorView.dispatch(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let language = new Compartment();
|
||||
let tabSize = new Compartment();
|
||||
@@ -51,7 +74,6 @@
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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, references);
|
||||
}
|
||||
|
||||
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, references);
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<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)
|
||||
@@ -43,75 +43,42 @@
|
||||
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"
|
||||
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}
|
||||
|
||||
@@ -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;
|
||||
@@ -39,7 +40,16 @@
|
||||
|
||||
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>
|
||||
@@ -51,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, references);
|
||||
}
|
||||
</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}/>
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
<script>
|
||||
import {getContext} from "svelte";
|
||||
import {previewTitle} from "../Preview";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import {sortByField} from "../../edges/sortEdges";
|
||||
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
|
||||
import Sortable from "../../libs/Sortable.svelte";
|
||||
import RenderField from "../../content/RenderField.svelte";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
import {insertEdges} from "./reference.js";
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
let collection = channel.schemas.filter((aschema) =>
|
||||
field.collections.includes(aschema.name)
|
||||
)[0];
|
||||
|
||||
function removeReference(e, recordId) {
|
||||
e.preventDefault();
|
||||
graph.edges = graph.edges.filter(
|
||||
(edge) => !(edge.source === record.id && edge.target === recordId && edge.field === field.name)
|
||||
);
|
||||
}
|
||||
|
||||
function sendToTop(e, recordId) {
|
||||
e.preventDefault();
|
||||
let ref = graph.edges.find(
|
||||
(edge) => edge.source === record.id && edge.target === recordId && edge.field === field.name
|
||||
);
|
||||
removeReference(e, recordId);
|
||||
graph.edges = [ref, ...graph.edges];
|
||||
}
|
||||
|
||||
function sendToBottom(e, recordId) {
|
||||
e.preventDefault();
|
||||
let ref = graph.edges.find(
|
||||
(edge) => edge.source === record.id && edge.target === recordId && edge.field === field.name
|
||||
);
|
||||
removeReference(e, recordId);
|
||||
graph.edges = [...graph.edges, ref];
|
||||
}
|
||||
|
||||
|
||||
function reorder(e) {
|
||||
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);
|
||||
}
|
||||
|
||||
$:visibleColumns = [];
|
||||
// $: visibleColumns = collection.fields
|
||||
// .filter((f) => f.ui !== "tab" && !f.trashed)
|
||||
// .filter((f) => {
|
||||
// return collection.visible.includes(f.name);
|
||||
// });
|
||||
</script>
|
||||
|
||||
{#if errorMessage}
|
||||
<div class="invalid-feedback d-block mb-3">
|
||||
{errorMessage}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="inline-card-wrapper">
|
||||
<ReferenceInlineButtons
|
||||
buttonClass="mt-2"
|
||||
recordId={null}
|
||||
schemas={collections}
|
||||
on:insert={insert}
|
||||
on:save={insert}
|
||||
/>
|
||||
</div>
|
||||
{#if references.length > 0}
|
||||
<div class="lx-table rounded">
|
||||
<table class="">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th/>
|
||||
|
||||
{#each visibleColumns as field}
|
||||
<th
|
||||
class="field-ui-{field.ui}"
|
||||
scope="col"
|
||||
title={field.help}
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top">{field.label}</th
|
||||
>
|
||||
{/each}
|
||||
<th/>
|
||||
</tr>
|
||||
</thead>
|
||||
<Sortable isTable={true} on:update={reorder}>
|
||||
{#each references as record,index (record.id)}
|
||||
<tr>
|
||||
<td class="">
|
||||
<div class="">
|
||||
<div class="d-flex align-items-center">
|
||||
<a
|
||||
class="me-2 text-decoration-none text-dark fs-6"
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
target="_blank"
|
||||
>
|
||||
{previewTitle(channel.schemas, record)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{#each visibleColumns as field, index}
|
||||
<td class="field-ui-{field.ui}">
|
||||
<RenderField
|
||||
{record}
|
||||
{graph}
|
||||
schema={collection}
|
||||
{field}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
<td>
|
||||
<button
|
||||
class="trash-button text-dark btn btn-sm btn-link"
|
||||
on:click={(e) =>
|
||||
removeReference(e, record.id)}
|
||||
>
|
||||
<Icon icon="trash-can"/>
|
||||
</button>
|
||||
{#if references.length > 30 && index > 0}
|
||||
<button
|
||||
title="Send item to top"
|
||||
class="to-top-button text-dark btn btn-sm btn-link"
|
||||
on:click={(e) =>
|
||||
sendToTop(e, record.id)}
|
||||
>
|
||||
<Icon icon="circle-chevron-up"/>
|
||||
</button>
|
||||
{/if}
|
||||
{#if references.length > 30 && index + 1 < references.length}
|
||||
<button
|
||||
title="Send item to bottom"
|
||||
class="to-top-button text-dark btn btn-sm btn-link"
|
||||
on:click={(e) =>
|
||||
sendToBottom(e, record.id)}
|
||||
>
|
||||
<Icon icon="circle-chevron-down"/>
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</Sortable>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -4,13 +4,14 @@
|
||||
import {previewTitle} from "../Preview";
|
||||
import {getErrorMessage} from "./errorMessage";
|
||||
import {insertEdges} from "./reference.js";
|
||||
import Icon from "../../common/Icon.svelte";
|
||||
|
||||
const channel = getContext("channel");
|
||||
export let field;
|
||||
export let id;
|
||||
export let record;
|
||||
export let graph;
|
||||
|
||||
let searchEl;
|
||||
export let validationErrors;
|
||||
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||
|
||||
@@ -58,6 +59,9 @@
|
||||
function insert(e, insertRecord) {
|
||||
e.preventDefault();
|
||||
graph = insertEdges(graph, record, [insertRecord], field.name, e.detail.action);
|
||||
search = ""
|
||||
searchEl.focus()
|
||||
searchEl.blur()
|
||||
}
|
||||
|
||||
const updateResults = debounce((e) => {
|
||||
@@ -81,71 +85,79 @@
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{#if errorMessage}
|
||||
<div class="invalid-feedback d-block mb-3">
|
||||
{errorMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
type="search"
|
||||
{id}
|
||||
on:keyup={updateResults}
|
||||
class="form-control dropdown-toggle"
|
||||
class:is-invalid={errorMessage}
|
||||
bind:value={search}
|
||||
placeholder={"Search for "+field.label}
|
||||
data-bs-toggle="dropdown"
|
||||
autocomplete="off"
|
||||
readonly={field.readonly && !isCreateMode}
|
||||
/>
|
||||
|
||||
<div class="dropdown-menu w-100">
|
||||
|
||||
{#if searchOptions}
|
||||
{#each searchOptions as option (option.id)}
|
||||
<div
|
||||
on:click={(e) => insert(e, option)}
|
||||
on:keypress={(e) => insert(e, option)}
|
||||
>
|
||||
<span class="dropdown-item">
|
||||
{previewTitle(channel.schemas, option)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
|
||||
Start typing...
|
||||
{/each}
|
||||
{/if}
|
||||
{#if search }
|
||||
<div
|
||||
on:click={(e) => saveNew(e,search)}
|
||||
on:keypress={(e) => saveNew(e,search)}
|
||||
>
|
||||
<span class="dropdown-item">
|
||||
Add "{search}"
|
||||
</span>
|
||||
<div class="reference-tags">
|
||||
{#if errorMessage}
|
||||
<div class="invalid-feedback d-block mb-3">
|
||||
{errorMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
type="search"
|
||||
bind:this={searchEl}
|
||||
{id}
|
||||
on:keyup={updateResults}
|
||||
class:is-invalid={errorMessage}
|
||||
bind:value={search}
|
||||
placeholder={"Search for "+field.label}
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
||||
<div class="reference-tags-results">
|
||||
|
||||
{#if searchOptions}
|
||||
{#each searchOptions as option (option.id)}
|
||||
<div
|
||||
class="reference-tags-option"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={(e) => insert(e, option)}
|
||||
on:keypress={(e) => insert(e, option)}
|
||||
>
|
||||
{previewTitle(channel.schemas, option ,graph)}
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div
|
||||
class="start-typing">
|
||||
Start typing...
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if search }
|
||||
<div
|
||||
class="reference-tags-option"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={(e) => saveNew(e,search)}
|
||||
on:keypress={(e) => saveNew(e,search)}
|
||||
>
|
||||
Add "{search}"
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{#if references.length > 0}
|
||||
<div class="d-flex">
|
||||
<div style="display: flex;align-items: center;gap: 4px">
|
||||
{#each references as record (record.id)}
|
||||
<span class="badge rounded-pill bg-light text-dark fs-6 mt-3">
|
||||
<div class="d-flex align-items-center ">
|
||||
<span class="reference-tags-selected-value">
|
||||
<a
|
||||
class="record-title"
|
||||
href="{channel.lucentUrl}/records/{record.id}"
|
||||
>
|
||||
{previewTitle(channel.schemas, record)}
|
||||
<button
|
||||
on:click|preventDefault={(e) => removeReference(e, record.id)}
|
||||
type="button"
|
||||
class="btn-close btn-sm ms-1"
|
||||
style="font-size:10px"
|
||||
aria-label="Close"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<button
|
||||
on:click|preventDefault={(e) => removeReference(e, record.id)}
|
||||
type="button"
|
||||
class="button-text"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Icon width={12} height={12} icon="close"></Icon>
|
||||
</button>
|
||||
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user