init
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
front/node_modules
|
||||||
|
/vendor
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.phpunit.result.cache
|
||||||
|
front/npm-debug.log
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "lexx27/lucent",
|
||||||
|
"type": "project",
|
||||||
|
"description": "Lucent cms",
|
||||||
|
"keywords": [
|
||||||
|
"framework",
|
||||||
|
"laravel"
|
||||||
|
],
|
||||||
|
"bin": [
|
||||||
|
"bin/lucent-serve"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
|
"intervention/image": "^2.7",
|
||||||
|
"phpoption/phpoption": "^1.9",
|
||||||
|
"spatie/image-optimizer": "^1.6",
|
||||||
|
"staudenmeir/laravel-cte": "^1.0",
|
||||||
|
"ext-pdo": "*"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^1.8"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"src\\": "lucent/"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/Response.php",
|
||||||
|
"src/macros.php",
|
||||||
|
"src/Schema/Functions.php",
|
||||||
|
"src/File/Uploader.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
Generated
+2269
File diff suppressed because it is too large
Load Diff
Vendored
+5
File diff suppressed because one or more lines are too long
Vendored
+172
File diff suppressed because one or more lines are too long
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"main.js": {
|
||||||
|
"file": "assets/main.c3dc0395.js",
|
||||||
|
"src": "main.js",
|
||||||
|
"isEntry": true,
|
||||||
|
"css": [
|
||||||
|
"assets/main.778ffe0f.css"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"main.css": {
|
||||||
|
"file": "assets/main.778ffe0f.css",
|
||||||
|
"src": "main.css"
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+70
@@ -0,0 +1,70 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
window._ = _;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We'll load the axios HTTP library which allows us to easily issue requests
|
||||||
|
* to our Laravel back-end. This library automatically handles sending the
|
||||||
|
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
window.axios = axios;
|
||||||
|
|
||||||
|
window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
|
||||||
|
window.axios.interceptors.request.use(
|
||||||
|
function (config) {
|
||||||
|
let list;
|
||||||
|
list = document.querySelectorAll(".btn-spinner");
|
||||||
|
for (var i = 0; i < list.length; ++i) {
|
||||||
|
list[i].classList.add("spinner-on");
|
||||||
|
list[i].disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
window.axios.interceptors.response.use(
|
||||||
|
function (response) {
|
||||||
|
let list;
|
||||||
|
list = document.querySelectorAll(".btn-spinner");
|
||||||
|
for (var i = 0; i < list.length; ++i) {
|
||||||
|
list[i].classList.remove("spinner-on");
|
||||||
|
list[i].disabled = false;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
let list;
|
||||||
|
list = document.querySelectorAll(".btn-spinner");
|
||||||
|
for (var i = 0; i < list.length; ++i) {
|
||||||
|
list[i].classList.remove("spinner-on");
|
||||||
|
list[i].disabled = false;
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Echo exposes an expressive API for subscribing to channels and listening
|
||||||
|
* for events that are broadcast by Laravel. Echo and event broadcasting
|
||||||
|
* allows your team to easily build robust real-time web applications.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// import Echo from 'laravel-echo';
|
||||||
|
|
||||||
|
// import Pusher from 'pusher-js';
|
||||||
|
// window.Pusher = Pusher;
|
||||||
|
|
||||||
|
// window.Echo = new Echo({
|
||||||
|
// broadcaster: 'pusher',
|
||||||
|
// key: import.meta.env.VITE_PUSHER_APP_KEY,
|
||||||
|
// wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_CLUSTER}.pusher.com`,
|
||||||
|
// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
|
||||||
|
// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
|
||||||
|
// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
|
||||||
|
// enabledTransports: ['ws', 'wss'],
|
||||||
|
// });
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import {formatDistanceToNow, parseJSON} from "date-fns";
|
||||||
|
|
||||||
|
export function friendlyDate(date) {
|
||||||
|
|
||||||
|
return formatDistanceToNow(parseJSON(date), {addSuffix: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripHtml(html = "") {
|
||||||
|
let tmp = document.createElement("div");
|
||||||
|
tmp.innerHTML = html;
|
||||||
|
return tmp.textContent || tmp.innerText || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function randomId(length = 10) {
|
||||||
|
return Math.random().toString(36).substring(2, length + 2);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import "./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";
|
||||||
|
|
||||||
|
Mustache.escape = function (value) {
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
function enableTooltipsAnywhere() {
|
||||||
|
// Enable tooltips everywhere
|
||||||
|
var tooltipTriggerList = [].slice.call(
|
||||||
|
document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||||
|
);
|
||||||
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define all components
|
||||||
|
const entryComponents = {
|
||||||
|
account: Account,
|
||||||
|
channel: Channel,
|
||||||
|
};
|
||||||
|
|
||||||
|
let loadedComponents = [];
|
||||||
|
|
||||||
|
let loadSvelte = function () {
|
||||||
|
loadedComponents.map((comp) => comp.$destroy());
|
||||||
|
loadedComponents = [];
|
||||||
|
|
||||||
|
const elements = document.body.querySelectorAll(".lucent-component");
|
||||||
|
if (elements.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const loadElement = function (element) {
|
||||||
|
const componentId = element.attributes["data-layout"].value;
|
||||||
|
const [_, component] = Object.entries(entryComponents).find(
|
||||||
|
([key, _]) => componentId == key
|
||||||
|
);
|
||||||
|
if (!component) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonData = document.getElementById(
|
||||||
|
"json-" + componentId
|
||||||
|
).innerHTML;
|
||||||
|
const props = JSON.parse(jsonData);
|
||||||
|
const compOptions = {
|
||||||
|
target: element,
|
||||||
|
props: props,
|
||||||
|
};
|
||||||
|
|
||||||
|
loadedComponents = [...loadedComponents, new component(compOptions)];
|
||||||
|
};
|
||||||
|
Array.from(elements).map(loadElement);
|
||||||
|
};
|
||||||
|
|
||||||
|
// document.addEventListener("turbo:load", loadSvelte);
|
||||||
|
document.addEventListener("DOMContentLoaded", loadSvelte);
|
||||||
|
document.addEventListener("DOMContentLoaded", enableTooltipsAnywhere);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script>
|
||||||
|
import Register from "./account/Register.svelte";
|
||||||
|
import Login from "./account/Login.svelte";
|
||||||
|
import Verify from "./account/Verify.svelte";
|
||||||
|
import Profile from "./account/Profile.svelte";
|
||||||
|
import {setContext} from "svelte";
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
register: Register,
|
||||||
|
login: Login,
|
||||||
|
verify: Verify,
|
||||||
|
profile: Profile,
|
||||||
|
};
|
||||||
|
|
||||||
|
export let title;
|
||||||
|
export let view;
|
||||||
|
export let user;
|
||||||
|
export let data;
|
||||||
|
export let channel;
|
||||||
|
|
||||||
|
setContext("channel", channel);
|
||||||
|
setContext("user", user);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:component this={components[view]} {title} {...data}/>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script>
|
||||||
|
import Members from "./members/Members.svelte";
|
||||||
|
import RecordNotFound from "./records/NotFound.svelte";
|
||||||
|
import RecordEdit from "./records/Edit.svelte";
|
||||||
|
import ContentIndex from "./content/Index.svelte";
|
||||||
|
import {setContext} from "svelte";
|
||||||
|
import Navbar from "./Navbar.svelte";
|
||||||
|
import HomeIndex from "./home/Index.svelte";
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
members: Members,
|
||||||
|
recordEdit: RecordEdit,
|
||||||
|
recordNotFound: RecordNotFound,
|
||||||
|
contentIndex: ContentIndex,
|
||||||
|
homeIndex: HomeIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
export let title;
|
||||||
|
export let view;
|
||||||
|
export let user;
|
||||||
|
export let data;
|
||||||
|
export let layout;
|
||||||
|
export let channel;
|
||||||
|
|
||||||
|
|
||||||
|
setContext("channel", channel);
|
||||||
|
setContext("user", user);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Navbar schema={data.schema}/>
|
||||||
|
|
||||||
|
<svelte:component this={components[view]} {title} {...data}/>
|
||||||
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script>
|
||||||
|
import Avatar from "./account/Avatar.svelte";
|
||||||
|
import NavbarMenu from "./NavbarMenu.svelte";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
|
export let schema;
|
||||||
|
const channel = getContext("channel");
|
||||||
|
const user = getContext("user");
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="lx-nav">
|
||||||
|
<a class="nav-item" href="{channel.lucentUrl}">{channel.name}</a>
|
||||||
|
<a class="nav-item" href="{channel.lucentUrl}/members">Members</a>
|
||||||
|
<a class="nav-item" href="{channel.lucentUrl}/profile">
|
||||||
|
<Avatar side="28" name={user.name}/>
|
||||||
|
</a
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="offcanvas offcanvas-start show border-0 bg-light-subtle" style="margin-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">
|
||||||
|
|
||||||
|
<div class="accordion" id="accordionPanelsStayOpenExample">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="panelsStayOpen-headingOne">
|
||||||
|
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#panelsStayOpen-collapseOne" aria-expanded="true" aria-controls="panelsStayOpen-collapseOne">
|
||||||
|
Main
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="panelsStayOpen-collapseOne" class="accordion-collapse collapse show" aria-labelledby="panelsStayOpen-headingOne">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<NavbarMenu
|
||||||
|
schemas={ channel.schemas.filter((sc) => sc.isEntry)}
|
||||||
|
schema={schema}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="panelsStayOpen-headingTwo">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#panelsStayOpen-collapseTwo" aria-expanded="false" aria-controls="panelsStayOpen-collapseTwo">
|
||||||
|
Other
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="panelsStayOpen-collapseTwo" class="accordion-collapse collapse" aria-labelledby="panelsStayOpen-headingTwo">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<NavbarMenu
|
||||||
|
schemas={ channel.schemas.filter((sc) => !sc.isEntry)}
|
||||||
|
schema={schema}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "./common/Icon.svelte";
|
||||||
|
|
||||||
|
export let label;
|
||||||
|
let isExpanded = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="sidebar-header d-flex align-items-center">
|
||||||
|
{#if !isExpanded}
|
||||||
|
<span class="d-flex" on:click={(e) => (isExpanded = true)}>
|
||||||
|
<Icon icon="circle-chevron-down" viewBox="0 0 512 512" />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if isExpanded}
|
||||||
|
<span class="d-flex" on:click={(e) => (isExpanded = false)}>
|
||||||
|
<Icon icon="circle-chevron-up" viewBox="0 0 512 512" />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<span class="ms-1">{label}</span>
|
||||||
|
<div class="actions">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
{#if isExpanded}
|
||||||
|
<div class="mb-2">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<script>
|
||||||
|
export let active = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="sidebar-item" class:active>
|
||||||
|
<slot />
|
||||||
|
<div class="actions">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script>
|
||||||
|
export let name;
|
||||||
|
export let side = "48";
|
||||||
|
const colors = [
|
||||||
|
"#00AA55",
|
||||||
|
"#009FD4",
|
||||||
|
"#B381B3",
|
||||||
|
"#939393",
|
||||||
|
"#E3BC00",
|
||||||
|
"#D47500",
|
||||||
|
"#DC2A2A",
|
||||||
|
"#3ede91",
|
||||||
|
"#377dd4",
|
||||||
|
"#0256b0",
|
||||||
|
"#053d82",
|
||||||
|
"#3d026e",
|
||||||
|
"#b378e3",
|
||||||
|
"#c4065c",
|
||||||
|
"#543208",
|
||||||
|
"#d97811",
|
||||||
|
"#0c6b40",
|
||||||
|
];
|
||||||
|
let initials = "";
|
||||||
|
if (name.split(" ").length > 1) {
|
||||||
|
initials =
|
||||||
|
name.split(" ")[0].charAt(0).toUpperCase() +
|
||||||
|
name.split(" ")[1].charAt(0).toUpperCase();
|
||||||
|
} else {
|
||||||
|
initials =
|
||||||
|
name.split(" ")[0].charAt(0).toUpperCase() +
|
||||||
|
name.split(" ")[0].charAt(1).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
let charIndex = name.charCodeAt(1) + name.length;
|
||||||
|
let colorIndex = charIndex % 19;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="avatar"
|
||||||
|
title={name}
|
||||||
|
style="background-color:{colors[
|
||||||
|
colorIndex
|
||||||
|
]}; height: {side}px;width: {side}px; font-size:{side / 2}px"
|
||||||
|
>
|
||||||
|
<div class="avatar__letters">{initials}</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script>
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
|
import SuccessAlert from "../common/SuccessAlert.svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
let email = "";
|
||||||
|
let successAlert;
|
||||||
|
|
||||||
|
function login(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post(channel.lucentUrl + "/login", {
|
||||||
|
email: email,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SuccessAlert bind:this={successAlert}/>
|
||||||
|
<div class="wrapper-tiny">
|
||||||
|
|
||||||
|
<form on:submit={login}>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="emailaddress" class="form-label">Email address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
bind:value={email}
|
||||||
|
class="form-control"
|
||||||
|
id="emailaddress"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="text-center mt-5 d-block">
|
||||||
|
<SpinnerButton label="Login"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script>
|
||||||
|
export let active = "";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
const user = getContext("user");
|
||||||
|
let logged = user?.id ? true : false;
|
||||||
|
let menuItems = [
|
||||||
|
{
|
||||||
|
name: "Account",
|
||||||
|
link: "/profile",
|
||||||
|
auth: true,
|
||||||
|
guest: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Login",
|
||||||
|
link: "/login",
|
||||||
|
auth: false,
|
||||||
|
guest: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Register",
|
||||||
|
link: "/register",
|
||||||
|
auth: false,
|
||||||
|
guest: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ul class="nav justify-content-center mt-4 mb-5">
|
||||||
|
{#each menuItems as item}
|
||||||
|
{#if item.auth == logged || item.guest == !logged}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link"
|
||||||
|
class:active={active == item.name}
|
||||||
|
href={item.link}>{item.name}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<style>
|
||||||
|
.nav-item a.active{
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<script>
|
||||||
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
|
import Nav from "./Nav.svelte";
|
||||||
|
import Avatar from "./Avatar.svelte";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
|
const user = getContext("user");
|
||||||
|
let name = user.name;
|
||||||
|
let email = user.email;
|
||||||
|
let errorMessage = "";
|
||||||
|
|
||||||
|
function saveName(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
errorMessage = "";
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post("/account/update-name", {
|
||||||
|
name: name,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
// window.reload();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
errorMessage = error.response?.data.error;
|
||||||
|
console.log({errorMessage});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEmail(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
errorMessage = "";
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post("/account/update-email", {
|
||||||
|
email: email,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
// window.reload();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
errorMessage = error.response?.data.error;
|
||||||
|
console.log({errorMessage});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Nav active="Account"/>
|
||||||
|
|
||||||
|
<div class="wrapper-tiny">
|
||||||
|
<ErrorAlert message={errorMessage}/>
|
||||||
|
<h3 class="header-small mb-5">
|
||||||
|
<Avatar name={user.name}/>
|
||||||
|
</h3>
|
||||||
|
<form on:submit={saveName}>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={name}
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Name"
|
||||||
|
/>
|
||||||
|
<SpinnerButton label="Update"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- <form on:submit={saveEmail}>-->
|
||||||
|
<!-- <div class="input-group mb-3">-->
|
||||||
|
<!-- <input-->
|
||||||
|
<!-- type="text"-->
|
||||||
|
<!-- bind:value={email}-->
|
||||||
|
<!-- class="form-control"-->
|
||||||
|
<!-- placeholder="Email"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- <SpinnerButton label="Update" />-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </form>-->
|
||||||
|
<div class="list-group">
|
||||||
|
<a class="list-group-item list-group-item-action" href="/logout">Logout from this device</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<script>
|
||||||
|
import ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
|
|
||||||
|
import Nav from "./Nav.svelte";
|
||||||
|
|
||||||
|
let name = "";
|
||||||
|
export let userCount = 1;
|
||||||
|
export let email = "";
|
||||||
|
export let token = "";
|
||||||
|
let password = "";
|
||||||
|
let errorMessage = "";
|
||||||
|
|
||||||
|
function register(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
errorMessage = "";
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post("/register", {
|
||||||
|
name: name,
|
||||||
|
password: password,
|
||||||
|
email: email,
|
||||||
|
token: token,
|
||||||
|
isAdmin: userCount === 0,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
window.location = "/login";
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
errorMessage = error.response?.data.error;
|
||||||
|
console.log({errorMessage});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Nav active="Register"/>
|
||||||
|
<div class="wrapper-tiny">
|
||||||
|
{#if token || userCount === 0}
|
||||||
|
<ErrorAlert message={errorMessage}/>
|
||||||
|
|
||||||
|
<form on:submit={register}>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={name}
|
||||||
|
class="form-control"
|
||||||
|
id="name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">Email address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
bind:value={email}
|
||||||
|
class="form-control"
|
||||||
|
id="email"
|
||||||
|
disabled={userCount !== 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
bind:value={password}
|
||||||
|
class="form-control"
|
||||||
|
id="password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="terms"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="terms"
|
||||||
|
>I Agree to the <a
|
||||||
|
href="https://www.radical-elements.com/terms-of-service"
|
||||||
|
target="_blank">terms and conditions</a
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-5 d-block">
|
||||||
|
<SpinnerButton label="Register"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<p class="text-center mb-0">Registrations are currently closed</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script>
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
import SpinnerButton from "../common/SpinnerButton.svelte";
|
||||||
|
import SuccessAlert from "../common/SuccessAlert.svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let email;
|
||||||
|
export let token;
|
||||||
|
let successAlert;
|
||||||
|
|
||||||
|
function login(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post(channel.lucentUrl + "/verify", {
|
||||||
|
email: email,
|
||||||
|
token: token,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
window.location = channel.lucentUrl;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SuccessAlert bind:this={successAlert}/>
|
||||||
|
<div class="wrapper-tiny">
|
||||||
|
|
||||||
|
<form on:submit={login}>
|
||||||
|
<div class="mb-3 text-center">
|
||||||
|
<h3>Login as {email}</h3>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="text-center mt-5 d-block">
|
||||||
|
<SpinnerButton label="Enter"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
export function avatar(name, side = 48) {
|
||||||
|
const colors = [
|
||||||
|
"#00AA55",
|
||||||
|
"#009FD4",
|
||||||
|
"#B381B3",
|
||||||
|
"#939393",
|
||||||
|
"#E3BC00",
|
||||||
|
"#D47500",
|
||||||
|
"#DC2A2A",
|
||||||
|
"#3ede91",
|
||||||
|
"#377dd4",
|
||||||
|
"#0256b0",
|
||||||
|
"#053d82",
|
||||||
|
"#3d026e",
|
||||||
|
"#b378e3",
|
||||||
|
"#c4065c",
|
||||||
|
"#543208",
|
||||||
|
"#d97811",
|
||||||
|
"#0c6b40",
|
||||||
|
];
|
||||||
|
let initials = "";
|
||||||
|
if (name.split(" ").length > 1) {
|
||||||
|
initials =
|
||||||
|
name.split(" ")[0].charAt(0).toUpperCase() +
|
||||||
|
name.split(" ")[1].charAt(0).toUpperCase();
|
||||||
|
} else {
|
||||||
|
initials =
|
||||||
|
name.split(" ")[0].charAt(0).toUpperCase() +
|
||||||
|
name.split(" ")[0].charAt(1).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
let charIndex = name.charCodeAt(1) + name.length;
|
||||||
|
let colorIndex = charIndex % 19;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div
|
||||||
|
class="avatar"
|
||||||
|
style="background-color:${
|
||||||
|
colors[colorIndex]
|
||||||
|
};height: ${side}px;width: ${side}px; font-size:${side / 2}px">
|
||||||
|
<div class="avatar__letters">${initials}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export function usernameById(users, id) {
|
||||||
|
if (users) {
|
||||||
|
return users.find((u) => u.id === id)?.name ?? id;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "./Icon.svelte";
|
||||||
|
|
||||||
|
export let label = "";
|
||||||
|
export let show = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-link p-0 text-decoration-none d-flex align-items-center mb-2 "
|
||||||
|
on:click|preventDefault={(e) => (show = !show)}
|
||||||
|
>
|
||||||
|
<span class="me-1">{label}</span>
|
||||||
|
{#if !show}
|
||||||
|
<Icon icon="circle-chevron-down" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if show}
|
||||||
|
<Icon icon="circle-chevron-up" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#if show}
|
||||||
|
<div class="mb-3" style="padding: 22px; background: rgb(249, 249, 249); border-radius: 32px;">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
export let message = "";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if message}
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<script>
|
||||||
|
const icons = {
|
||||||
|
"trash-can": {
|
||||||
|
path: '<path d="M135.2 17.69C140.6 6.848 151.7 0 163.8 0H284.2C296.3 0 307.4 6.848 312.8 17.69L320 32H416C433.7 32 448 46.33 448 64C448 81.67 433.7 96 416 96H32C14.33 96 0 81.67 0 64C0 46.33 14.33 32 32 32H128L135.2 17.69zM31.1 128H416V448C416 483.3 387.3 512 352 512H95.1C60.65 512 31.1 483.3 31.1 448V128zM111.1 208V432C111.1 440.8 119.2 448 127.1 448C136.8 448 143.1 440.8 143.1 432V208C143.1 199.2 136.8 192 127.1 192C119.2 192 111.1 199.2 111.1 208zM207.1 208V432C207.1 440.8 215.2 448 223.1 448C232.8 448 240 440.8 240 432V208C240 199.2 232.8 192 223.1 192C215.2 192 207.1 199.2 207.1 208zM304 208V432C304 440.8 311.2 448 320 448C328.8 448 336 440.8 336 432V208C336 199.2 328.8 192 320 192C311.2 192 304 199.2 304 208z"/>',
|
||||||
|
viewBox: "0 0 448 512",
|
||||||
|
},
|
||||||
|
"circle-chevron-down": {
|
||||||
|
path: '<path d="M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM390.6 246.6l-112 112C272.4 364.9 264.2 368 256 368s-16.38-3.125-22.62-9.375l-112-112c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L256 290.8l89.38-89.38c12.5-12.5 32.75-12.5 45.25 0S403.1 234.1 390.6 246.6z"/>',
|
||||||
|
viewBox: "0 0 512 512",
|
||||||
|
},
|
||||||
|
"circle-chevron-up": {
|
||||||
|
path: '<path d="M256 0C114.6 0 0 114.6 0 256c0 141.4 114.6 256 256 256s256-114.6 256-256C512 114.6 397.4 0 256 0zM390.6 310.6c-12.5 12.5-32.75 12.5-45.25 0L256 221.3L166.6 310.6c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25l112-112C239.6 147.1 247.8 144 256 144s16.38 3.125 22.62 9.375l112 112C403.1 277.9 403.1 298.1 390.6 310.6z"/>',
|
||||||
|
viewBox: "0 0 512 512",
|
||||||
|
},
|
||||||
|
ellipsis: {
|
||||||
|
path: '<path d="M120 256C120 286.9 94.93 312 64 312C33.07 312 8 286.9 8 256C8 225.1 33.07 200 64 200C94.93 200 120 225.1 120 256zM280 256C280 286.9 254.9 312 224 312C193.1 312 168 286.9 168 256C168 225.1 193.1 200 224 200C254.9 200 280 225.1 280 256zM328 256C328 225.1 353.1 200 384 200C414.9 200 440 225.1 440 256C440 286.9 414.9 312 384 312C353.1 312 328 286.9 328 256z"/>',
|
||||||
|
viewBox: "0 0 448 512",
|
||||||
|
},
|
||||||
|
"ellipsis-vertical": {
|
||||||
|
path: '<path d="M64 360C94.93 360 120 385.1 120 416C120 446.9 94.93 472 64 472C33.07 472 8 446.9 8 416C8 385.1 33.07 360 64 360zM64 200C94.93 200 120 225.1 120 256C120 286.9 94.93 312 64 312C33.07 312 8 286.9 8 256C8 225.1 33.07 200 64 200zM64 152C33.07 152 8 126.9 8 96C8 65.07 33.07 40 64 40C94.93 40 120 65.07 120 96C120 126.9 94.93 152 64 152z"/>',
|
||||||
|
viewBox: "0 0 128 512",
|
||||||
|
},
|
||||||
|
"angles-down": {
|
||||||
|
path: '<path d="M169.4 278.6C175.6 284.9 183.8 288 192 288s16.38-3.125 22.62-9.375l160-160c12.5-12.5 12.5-32.75 0-45.25s-32.75-12.5-45.25 0L192 210.8L54.63 73.38c-12.5-12.5-32.75-12.5-45.25 0s-12.5 32.75 0 45.25L169.4 278.6zM329.4 265.4L192 402.8L54.63 265.4c-12.5-12.5-32.75-12.5-45.25 0s-12.5 32.75 0 45.25l160 160C175.6 476.9 183.8 480 192 480s16.38-3.125 22.62-9.375l160-160c12.5-12.5 12.5-32.75 0-45.25S341.9 252.9 329.4 265.4z"/>',
|
||||||
|
viewBox: "0 0 384 512",
|
||||||
|
},
|
||||||
|
"angle-right": {
|
||||||
|
path: '<path d="M64 448c-8.188 0-16.38-3.125-22.62-9.375c-12.5-12.5-12.5-32.75 0-45.25L178.8 256L41.38 118.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0l160 160c12.5 12.5 12.5 32.75 0 45.25l-160 160C80.38 444.9 72.19 448 64 448z"/>',
|
||||||
|
viewBox: "0 0 256 512",
|
||||||
|
},
|
||||||
|
"photo-film": {
|
||||||
|
path: '<path d="M352 432c0 8.836-7.164 16-16 16H176c-8.838 0-16-7.164-16-16L160 128H48C21.49 128 .0003 149.5 .0003 176v288c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48L512 384h-160L352 432zM104 439c0 4.969-4.031 9-9 9h-30c-4.969 0-9-4.031-9-9v-30c0-4.969 4.031-9 9-9h30c4.969 0 9 4.031 9 9V439zM104 335c0 4.969-4.031 9-9 9h-30c-4.969 0-9-4.031-9-9v-30c0-4.969 4.031-9 9-9h30c4.969 0 9 4.031 9 9V335zM104 231c0 4.969-4.031 9-9 9h-30c-4.969 0-9-4.031-9-9v-30C56 196 60.03 192 65 192h30c4.969 0 9 4.031 9 9V231zM408 409c0-4.969 4.031-9 9-9h30c4.969 0 9 4.031 9 9v30c0 4.969-4.031 9-9 9h-30c-4.969 0-9-4.031-9-9V409zM591.1 0H239.1C213.5 0 191.1 21.49 191.1 48v256c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48v-256C640 21.49 618.5 0 591.1 0zM303.1 64c17.68 0 32 14.33 32 32s-14.32 32-32 32C286.3 128 271.1 113.7 271.1 96S286.3 64 303.1 64zM574.1 279.6C571.3 284.8 565.9 288 560 288H271.1C265.1 288 260.5 284.6 257.7 279.3C255 273.9 255.5 267.4 259.1 262.6l70-96C332.1 162.4 336.9 160 341.1 160c5.11 0 9.914 2.441 12.93 6.574l22.35 30.66l62.74-94.11C442.1 98.67 447.1 96 453.3 96c5.348 0 10.34 2.672 13.31 7.125l106.7 160C576.6 268 576.9 274.3 574.1 279.6z"/>',
|
||||||
|
viewBox: "0 0 640 512",
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
path: '<path d="M0 64C0 28.65 28.65 0 64 0H224V128C224 145.7 238.3 160 256 160H384V448C384 483.3 355.3 512 320 512H64C28.65 512 0 483.3 0 448V64zM256 128V0L384 128H256z"/>',
|
||||||
|
viewBox: "0 0 384 512",
|
||||||
|
},
|
||||||
|
"circle-info": {
|
||||||
|
path: '<path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM256 128c17.67 0 32 14.33 32 32c0 17.67-14.33 32-32 32S224 177.7 224 160C224 142.3 238.3 128 256 128zM296 384h-80C202.8 384 192 373.3 192 360s10.75-24 24-24h16v-64H224c-13.25 0-24-10.75-24-24S210.8 224 224 224h32c13.25 0 24 10.75 24 24v88h16c13.25 0 24 10.75 24 24S309.3 384 296 384z"/>',
|
||||||
|
viewBox: "0 0 512 512",
|
||||||
|
},
|
||||||
|
"table-columns": {
|
||||||
|
path: '<path d="M0 96C0 60.65 28.65 32 64 32H448C483.3 32 512 60.65 512 96V416C512 451.3 483.3 480 448 480H64C28.65 480 0 451.3 0 416V96zM64 416H224V160H64V416zM448 160H288V416H448V160z"/>',
|
||||||
|
viewBox: "0 0 512 512",
|
||||||
|
},
|
||||||
|
"arrow-down-a-z": {
|
||||||
|
path: '<path d="M239.6 373.1c11.94-13.05 11.06-33.31-1.969-45.27c-13.55-12.42-33.76-10.52-45.22 1.973L160 366.1V64.03c0-17.7-14.33-32.03-32-32.03S96 46.33 96 64.03v302l-32.4-35.39C51.64 317.7 31.39 316.7 18.38 328.7c-13.03 11.95-13.9 32.22-1.969 45.27l87.1 96.09c12.12 13.26 35.06 13.26 47.19 0L239.6 373.1zM448 416h-50.75l73.38-73.38c9.156-9.156 11.89-22.91 6.938-34.88S460.9 288 447.1 288H319.1C302.3 288 288 302.3 288 320s14.33 32 32 32h50.75l-73.38 73.38c-9.156 9.156-11.89 22.91-6.938 34.88S307.1 480 319.1 480h127.1C465.7 480 480 465.7 480 448S465.7 416 448 416zM492.6 209.3l-79.99-160.1c-10.84-21.81-46.4-21.81-57.24 0L275.4 209.3c-7.906 15.91-1.5 35.24 14.31 43.19c15.87 7.922 35.04 1.477 42.93-14.4l7.154-14.39h88.43l7.154 14.39c6.174 12.43 23.97 23.87 42.93 14.4C494.1 244.6 500.5 225.2 492.6 209.3zM367.8 167.4L384 134.7l16.22 32.63H367.8z"/>',
|
||||||
|
viewBox: "0 0 512 512",
|
||||||
|
},
|
||||||
|
"arrow-up-short-wide": {
|
||||||
|
path: '<path d="M544 416h-223.1c-17.67 0-32 14.33-32 32s14.33 32 32 32H544c17.67 0 32-14.33 32-32S561.7 416 544 416zM320 96h32c17.67 0 31.1-14.33 31.1-32s-14.33-32-31.1-32h-32c-17.67 0-32 14.33-32 32S302.3 96 320 96zM320 224H416c17.67 0 32-14.33 32-32s-14.33-32-32-32h-95.1c-17.67 0-32 14.33-32 32S302.3 224 320 224zM320 352H480c17.67 0 32-14.33 32-32s-14.33-32-32-32h-159.1c-17.67 0-32 14.33-32 32S302.3 352 320 352zM151.6 41.95c-12.12-13.26-35.06-13.26-47.19 0l-87.1 96.09C4.475 151.1 5.35 171.4 18.38 183.3c6.141 5.629 13.89 8.414 21.61 8.414c8.672 0 17.3-3.504 23.61-10.39L96 145.9v302C96 465.7 110.3 480 128 480s32-14.33 32-32.03V145.9L192.4 181.3C204.4 194.3 224.6 195.3 237.6 183.3c13.03-11.95 13.9-32.22 1.969-45.27L151.6 41.95z"/>',
|
||||||
|
viewBox: "0 0 576 512",
|
||||||
|
},
|
||||||
|
"arrow-down-wide-short": {
|
||||||
|
path: '<path d="M416 288h-95.1c-17.67 0-32 14.33-32 32s14.33 32 32 32H416c17.67 0 32-14.33 32-32S433.7 288 416 288zM544 32h-223.1c-17.67 0-32 14.33-32 32s14.33 32 32 32H544c17.67 0 32-14.33 32-32S561.7 32 544 32zM352 416h-32c-17.67 0-32 14.33-32 32s14.33 32 32 32h32c17.67 0 31.1-14.33 31.1-32S369.7 416 352 416zM480 160h-159.1c-17.67 0-32 14.33-32 32s14.33 32 32 32H480c17.67 0 32-14.33 32-32S497.7 160 480 160zM192.4 330.7L160 366.1V64.03C160 46.33 145.7 32 128 32S96 46.33 96 64.03v302L63.6 330.7c-6.312-6.883-14.94-10.38-23.61-10.38c-7.719 0-15.47 2.781-21.61 8.414c-13.03 11.95-13.9 32.22-1.969 45.27l87.1 96.09c12.12 13.26 35.06 13.26 47.19 0l87.1-96.09c11.94-13.05 11.06-33.31-1.969-45.27C224.6 316.8 204.4 317.7 192.4 330.7z"/>',
|
||||||
|
viewBox: "0 0 576 512",
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
path: '<path d="M3.853 54.87C10.47 40.9 24.54 32 40 32H472C487.5 32 501.5 40.9 508.1 54.87C514.8 68.84 512.7 85.37 502.1 97.33L320 320.9V448C320 460.1 313.2 471.2 302.3 476.6C291.5 482 278.5 480.9 268.8 473.6L204.8 425.6C196.7 419.6 192 410.1 192 400V320.9L9.042 97.33C-.745 85.37-2.765 68.84 3.854 54.87L3.853 54.87z"/>',
|
||||||
|
viewBox: "0 0 512 512",
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
path: '<path d="M96 32C96 14.33 110.3 0 128 0C145.7 0 160 14.33 160 32V64H288V32C288 14.33 302.3 0 320 0C337.7 0 352 14.33 352 32V64H400C426.5 64 448 85.49 448 112V160H0V112C0 85.49 21.49 64 48 64H96V32zM448 464C448 490.5 426.5 512 400 512H48C21.49 512 0 490.5 0 464V192H448V464z"/>',
|
||||||
|
viewBox: "0 0 448 512",
|
||||||
|
},
|
||||||
|
"pencil": {
|
||||||
|
path: '<path d="M421.7 220.3L188.5 453.4L154.6 419.5L158.1 416H112C103.2 416 96 408.8 96 400V353.9L92.51 357.4C87.78 362.2 84.31 368 82.42 374.4L59.44 452.6L137.6 429.6C143.1 427.7 149.8 424.2 154.6 419.5L188.5 453.4C178.1 463.8 165.2 471.5 151.1 475.6L30.77 511C22.35 513.5 13.24 511.2 7.03 504.1C.8198 498.8-1.502 489.7 .976 481.2L36.37 360.9C40.53 346.8 48.16 333.9 58.57 323.5L291.7 90.34L421.7 220.3zM492.7 58.75C517.7 83.74 517.7 124.3 492.7 149.3L444.3 197.7L314.3 67.72L362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32L492.7 58.75z"/>',
|
||||||
|
viewBox: "0 0 512 512",
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
path: '<path d="M448 80V128C448 172.2 347.7 208 224 208C100.3 208 0 172.2 0 128V80C0 35.82 100.3 0 224 0C347.7 0 448 35.82 448 80zM393.2 214.7C413.1 207.3 433.1 197.8 448 186.1V288C448 332.2 347.7 368 224 368C100.3 368 0 332.2 0 288V186.1C14.93 197.8 34.02 207.3 54.85 214.7C99.66 230.7 159.5 240 224 240C288.5 240 348.3 230.7 393.2 214.7V214.7zM54.85 374.7C99.66 390.7 159.5 400 224 400C288.5 400 348.3 390.7 393.2 374.7C413.1 367.3 433.1 357.8 448 346.1V432C448 476.2 347.7 512 224 512C100.3 512 0 476.2 0 432V346.1C14.93 357.8 34.02 367.3 54.85 374.7z"/>',
|
||||||
|
viewBox: "0 0 448 512",
|
||||||
|
},
|
||||||
|
"dice": {
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
"eye": {
|
||||||
|
path: '<path d="M279.6 160.4C282.4 160.1 285.2 160 288 160C341 160 384 202.1 384 256C384 309 341 352 288 352C234.1 352 192 309 192 256C192 253.2 192.1 250.4 192.4 247.6C201.7 252.1 212.5 256 224 256C259.3 256 288 227.3 288 192C288 180.5 284.1 169.7 279.6 160.4zM480.6 112.6C527.4 156 558.7 207.1 573.5 243.7C576.8 251.6 576.8 260.4 573.5 268.3C558.7 304 527.4 355.1 480.6 399.4C433.5 443.2 368.8 480 288 480C207.2 480 142.5 443.2 95.42 399.4C48.62 355.1 17.34 304 2.461 268.3C-.8205 260.4-.8205 251.6 2.461 243.7C17.34 207.1 48.62 156 95.42 112.6C142.5 68.84 207.2 32 288 32C368.8 32 433.5 68.84 480.6 112.6V112.6zM288 112C208.5 112 144 176.5 144 256C144 335.5 208.5 400 288 400C367.5 400 432 335.5 432 256C432 176.5 367.5 112 288 112z"/>',
|
||||||
|
viewBox: "0 0 576 512",
|
||||||
|
},
|
||||||
|
"circle-plus": {
|
||||||
|
path: '<path d="M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM256 368C269.3 368 280 357.3 280 344V280H344C357.3 280 368 269.3 368 256C368 242.7 357.3 232 344 232H280V168C280 154.7 269.3 144 256 144C242.7 144 232 154.7 232 168V232H168C154.7 232 144 242.7 144 256C144 269.3 154.7 280 168 280H232V344C232 357.3 242.7 368 256 368z"/>',
|
||||||
|
viewBox: "0 0 512 512",
|
||||||
|
},
|
||||||
|
"magnifying-glass": {
|
||||||
|
path: '<path d="M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z"/>',
|
||||||
|
viewBox: "0 0 512 512",
|
||||||
|
},
|
||||||
|
"expand": {
|
||||||
|
path: '<path d="M128 32H32C14.31 32 0 46.31 0 64v96c0 17.69 14.31 32 32 32s32-14.31 32-32V96h64c17.69 0 32-14.31 32-32S145.7 32 128 32zM416 32h-96c-17.69 0-32 14.31-32 32s14.31 32 32 32h64v64c0 17.69 14.31 32 32 32s32-14.31 32-32V64C448 46.31 433.7 32 416 32zM128 416H64v-64c0-17.69-14.31-32-32-32s-32 14.31-32 32v96c0 17.69 14.31 32 32 32h96c17.69 0 32-14.31 32-32S145.7 416 128 416zM416 320c-17.69 0-32 14.31-32 32v64h-64c-17.69 0-32 14.31-32 32s14.31 32 32 32h96c17.69 0 32-14.31 32-32v-96C448 334.3 433.7 320 416 320z"/>',
|
||||||
|
viewBox: "0 0 448 512",
|
||||||
|
},
|
||||||
|
"compress": {
|
||||||
|
path: '<path d="M128 320H32c-17.69 0-32 14.31-32 32s14.31 32 32 32h64v64c0 17.69 14.31 32 32 32s32-14.31 32-32v-96C160 334.3 145.7 320 128 320zM416 320h-96c-17.69 0-32 14.31-32 32v96c0 17.69 14.31 32 32 32s32-14.31 32-32v-64h64c17.69 0 32-14.31 32-32S433.7 320 416 320zM320 192h96c17.69 0 32-14.31 32-32s-14.31-32-32-32h-64V64c0-17.69-14.31-32-32-32s-32 14.31-32 32v96C288 177.7 302.3 192 320 192zM128 32C110.3 32 96 46.31 96 64v64H32C14.31 128 0 142.3 0 160s14.31 32 32 32h96c17.69 0 32-14.31 32-32V64C160 46.31 145.7 32 128 32z"/>',
|
||||||
|
viewBox: "0 0 448 512",
|
||||||
|
},
|
||||||
|
"check": {
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export let width = 16;
|
||||||
|
export let height = 16;
|
||||||
|
export let icon = "";
|
||||||
|
export let fill = "currentColor";
|
||||||
|
export let stroke = "currentColor";
|
||||||
|
|
||||||
|
let selectedIcon = icons[icon];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
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>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
svg {
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
export let label = "";
|
||||||
|
export let disabled = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-spinner" {disabled}>
|
||||||
|
<span
|
||||||
|
class="spinner-border spinner-border-sm"
|
||||||
|
role="status"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script>
|
||||||
|
import { fly } from "svelte/transition";
|
||||||
|
$: message = "Saved";
|
||||||
|
$: isVisible = false;
|
||||||
|
export function show(amessage = "Saved") {
|
||||||
|
message = amessage;
|
||||||
|
isVisible = true;
|
||||||
|
setTimeout(function () {
|
||||||
|
isVisible = false;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lx-alert {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
top: 45px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<script>
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let selected;
|
||||||
|
export let schema;
|
||||||
|
export let filter;
|
||||||
|
|
||||||
|
function deleteRecords(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
axios
|
||||||
|
.post(channel.lucentUrl + "/records/delete", {
|
||||||
|
ids: selected.map((s) => s.id),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeStatus(e, status) {
|
||||||
|
axios
|
||||||
|
.post(channel.lucentUrl + "/records/status/" + status, {
|
||||||
|
schemaName: schema.name,
|
||||||
|
records: selected
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<span class="me-2">{selected.length} records selected</span>
|
||||||
|
<div class="btn-group " role="group" aria-label="Basic example">
|
||||||
|
<button
|
||||||
|
on:click|preventDefault={(e) => changeStatus(e, "published")}
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-primary">Publish
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
on:click|preventDefault={(e) => changeStatus(e, "draft")}
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-primary">Make Draft
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
{#if filter["_sys.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
|
||||||
|
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
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<script>
|
||||||
|
import Tools from "./tools/Tools.svelte";
|
||||||
|
import Pagination from "./pagination/Pagination.svelte";
|
||||||
|
import ActionsOnSelected from "./ActionsOnSelected.svelte";
|
||||||
|
import Preview from "../files/Preview.svelte";
|
||||||
|
import Table from "./Table.svelte";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let title;
|
||||||
|
export let schema;
|
||||||
|
export let users;
|
||||||
|
export let records;
|
||||||
|
export let graph;
|
||||||
|
|
||||||
|
export let visibleFields;
|
||||||
|
export let systemFields;
|
||||||
|
export let sort;
|
||||||
|
export let operators;
|
||||||
|
export let filter;
|
||||||
|
export let limit;
|
||||||
|
export let skip;
|
||||||
|
export let total;
|
||||||
|
export let inModal;
|
||||||
|
export let modalUrl;
|
||||||
|
export let selected = [];
|
||||||
|
|
||||||
|
|
||||||
|
function selectRecord(e, record) {
|
||||||
|
let recordExists = selected.find((r) => r.id === record.id);
|
||||||
|
|
||||||
|
if (recordExists) {
|
||||||
|
selected = selected.filter((r) => r.id !== record.id);
|
||||||
|
} else {
|
||||||
|
selected = [...selected, record];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh(e) {
|
||||||
|
const newUrl = e.detail;
|
||||||
|
axios
|
||||||
|
.get(newUrl)
|
||||||
|
.then((response) => {
|
||||||
|
records = response.data.records;
|
||||||
|
sort = response.data.sort;
|
||||||
|
operators = response.data.operators;
|
||||||
|
filter = response.data.filter;
|
||||||
|
skip = response.data.skip;
|
||||||
|
limit = response.data.limit;
|
||||||
|
total = response.data.total;
|
||||||
|
modalUrl = response.data.modalUrl;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper-large transparent ">
|
||||||
|
<!-- <Manager managerRecords={recordHistory} {schemas} /> -->
|
||||||
|
|
||||||
|
<div class="lx-card mb-4 {inModal ? 'mt-0' : 'mt-5'}">
|
||||||
|
<h3 class="header-normal mb-5 ">
|
||||||
|
{schema.label}
|
||||||
|
</h3>
|
||||||
|
{#if selected.length > 0 && !inModal}
|
||||||
|
<ActionsOnSelected {schema} {selected} {inModal} {filter}/>
|
||||||
|
{:else}
|
||||||
|
<Tools
|
||||||
|
bind:schema
|
||||||
|
bind:records
|
||||||
|
{systemFields}
|
||||||
|
{sort}
|
||||||
|
{operators}
|
||||||
|
{filter}
|
||||||
|
{inModal}
|
||||||
|
{modalUrl}
|
||||||
|
on:refresh={refresh}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if schema.type === "collection"}
|
||||||
|
<Table
|
||||||
|
{records}
|
||||||
|
{graph}
|
||||||
|
{schema}
|
||||||
|
{sort}
|
||||||
|
{systemFields}
|
||||||
|
{inModal}
|
||||||
|
{users}
|
||||||
|
bind:selected
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="row" style="max-width:1000px">
|
||||||
|
{#each records as record (record.id)}
|
||||||
|
<div class="col-6 col-md-4">
|
||||||
|
<div
|
||||||
|
class="file-wrapper rounded p-2 mb-4 bg-light"
|
||||||
|
class:selected={selected.includes(record)}
|
||||||
|
>
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
on:change={(e) => selectRecord(e, record)}
|
||||||
|
class="form-check-input "
|
||||||
|
type="checkbox"
|
||||||
|
checked={selected.find(
|
||||||
|
(r) => r.id === record.id
|
||||||
|
)}
|
||||||
|
value={record}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<Preview {record} size="medium"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
|
title={record._file.path}
|
||||||
|
class="d-block text-center overflow-hidden text-nowrap my-2 "
|
||||||
|
style="
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
">{record._file.path}</a
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="lx-small-text text-muted d-block text-center"
|
||||||
|
>{record._file.mime}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
{limit}
|
||||||
|
{skip}
|
||||||
|
{total}
|
||||||
|
on:refresh={refresh}
|
||||||
|
{inModal}
|
||||||
|
{modalUrl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-check {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<script>
|
||||||
|
import RenderField from "./RenderField.svelte";
|
||||||
|
import Avatar from "../account/Avatar.svelte";
|
||||||
|
import Status from "../records/Status.svelte";
|
||||||
|
import {usernameById} from "../account/users";
|
||||||
|
import {friendlyDate} from "../../helpers";
|
||||||
|
|
||||||
|
export let schema;
|
||||||
|
export let users;
|
||||||
|
export let graph;
|
||||||
|
export let record;
|
||||||
|
export let sort;
|
||||||
|
export let visibleColumns;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each visibleColumns as field, index}
|
||||||
|
<td
|
||||||
|
class="field-ui-{field.info.name}"
|
||||||
|
class:is-sort={"-" + field.name == sort || field.name == sort}
|
||||||
|
>
|
||||||
|
<RenderField {record} {schema} {graph} {field}/>
|
||||||
|
</td>
|
||||||
|
{/each}
|
||||||
|
{#if schema.visible.includes("_sys.status")}
|
||||||
|
<td
|
||||||
|
class="text-center"
|
||||||
|
class:is-sort={"-_sys.status" == sort || "_sys.status" == sort}
|
||||||
|
>
|
||||||
|
<Status status={record._sys.status}/>
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
|
{#if schema.visible.includes("_sys.createdBy")}
|
||||||
|
<td
|
||||||
|
class="text-center"
|
||||||
|
class:is-sort={"-_sys.createdBy" == sort || "_sys.createdBy" == sort}
|
||||||
|
>
|
||||||
|
<Avatar name={usernameById(users, record._sys.createdBy)} side={24}/>
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
|
{#if schema.visible.includes("_sys.updatedBy")}
|
||||||
|
<td
|
||||||
|
class="text-center"
|
||||||
|
class:is-sort={"-_sys.updatedBy" == sort || "_sys.updatedBy" == sort}
|
||||||
|
>
|
||||||
|
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
|
{#if schema.visible.includes("_sys.createdAt")}
|
||||||
|
<td class:is-sort={"-_sys.createdAt" == sort || "_sys.createdAt" == sort}>
|
||||||
|
{friendlyDate(record._sys.createdAt)}
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
|
{#if schema.visible.includes("_sys.updatedAt")}
|
||||||
|
<td class:is-sort={"-_sys.updatedAt" == sort || "_sys.updatedAt" == sort}>
|
||||||
|
{friendlyDate(record._sys.updatedAt)}
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script>
|
||||||
|
import Checkbox from "./elements/Checkbox.svelte";
|
||||||
|
|
||||||
|
import Color from "./elements/Color.svelte";
|
||||||
|
import Reference from "./elements/Reference.svelte";
|
||||||
|
import Number from "./elements/Number.svelte";
|
||||||
|
|
||||||
|
import Text from "./elements/Text.svelte";
|
||||||
|
import Url from "./elements/Url.svelte";
|
||||||
|
import Date from "./elements/Date.svelte";
|
||||||
|
import File from "./elements/File.svelte";
|
||||||
|
import Uuid from "./elements/UUID.svelte";
|
||||||
|
import Rich from "./elements/Rich.svelte";
|
||||||
|
|
||||||
|
const renderElements = {
|
||||||
|
text: Text,
|
||||||
|
rich: Rich,
|
||||||
|
textarea: Text,
|
||||||
|
color: Color,
|
||||||
|
checkbox: Checkbox,
|
||||||
|
reference: Reference,
|
||||||
|
number: Number,
|
||||||
|
url: Url,
|
||||||
|
date: Date,
|
||||||
|
datetime: Date,
|
||||||
|
uuid: Uuid,
|
||||||
|
file: File,
|
||||||
|
};
|
||||||
|
export let field;
|
||||||
|
export let schema;
|
||||||
|
export let record;
|
||||||
|
export let graph;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:component
|
||||||
|
this={renderElements[field.info.name]}
|
||||||
|
value={record.data[field.name]}
|
||||||
|
{record}
|
||||||
|
{graph}
|
||||||
|
{schema}
|
||||||
|
{field}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<script>
|
||||||
|
import RecordRow from "./RecordRow.svelte";
|
||||||
|
import {previewTitle} from "../records/Preview";
|
||||||
|
import {usernameById} from "../account/users";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
import Avatar from "../account/Avatar.svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
|
||||||
|
export let schema;
|
||||||
|
export let users;
|
||||||
|
export let records;
|
||||||
|
export let graph;
|
||||||
|
export let systemFields;
|
||||||
|
export let sort;
|
||||||
|
export let inModal;
|
||||||
|
export let selected = [];
|
||||||
|
|
||||||
|
function toggleAll(e) {
|
||||||
|
// e.preventDefault();
|
||||||
|
if (selected.length === records.length) {
|
||||||
|
selected = [];
|
||||||
|
} else {
|
||||||
|
selected = records;
|
||||||
|
}
|
||||||
|
e.currentTarget.checked = selected.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRecord(e, record) {
|
||||||
|
let recordExists = selected.find((r) => r.id == record.id);
|
||||||
|
|
||||||
|
if (recordExists) {
|
||||||
|
selected = selected.filter((r) => r.id !== record.id);
|
||||||
|
} else {
|
||||||
|
selected = [...selected, record];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: visibleColumns = schema.fields.filter(c => schema.visible.includes(c.name))
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="lx-table rounded">
|
||||||
|
<table class="">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<input
|
||||||
|
on:change|preventDefault={toggleAll}
|
||||||
|
indeterminate={selected.length > 0 &&
|
||||||
|
selected.length < records.length}
|
||||||
|
checked={selected.length == records.length}
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
{#each visibleColumns as field}
|
||||||
|
<th
|
||||||
|
class="field-ui-{field.ui}"
|
||||||
|
class:is-sort={"-" + field.name == sort ||
|
||||||
|
field.name == sort}
|
||||||
|
scope="col"
|
||||||
|
title={field.help}
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="top">{field.label}</th
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
{#each systemFields.filter(c => schema.visible.includes(c.name)) as sysField}
|
||||||
|
<th>{sysField.label}</th>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each records as record (record.id)}
|
||||||
|
<tr>
|
||||||
|
<td class="title-td">
|
||||||
|
<div
|
||||||
|
class="title-td-contents d-inline-flex justify-content-between w-100 align-items-center"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-items-center ">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
on:change={(e) =>
|
||||||
|
selectRecord(e, record)}
|
||||||
|
class="form-check-input "
|
||||||
|
type="checkbox"
|
||||||
|
checked={selected.find(
|
||||||
|
(r) => r.id === record.id
|
||||||
|
)}
|
||||||
|
value={record}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="me-2 text-decoration-none text-dark fs-6"
|
||||||
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
|
target={inModal ? "_blank" : "_self"}
|
||||||
|
>
|
||||||
|
{previewTitle(channel.schemas, record, graph)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Avatar
|
||||||
|
name={usernameById(
|
||||||
|
users,
|
||||||
|
record._sys.updatedBy
|
||||||
|
)}
|
||||||
|
side={24}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<RecordRow
|
||||||
|
{record}
|
||||||
|
{graph}
|
||||||
|
{schema}
|
||||||
|
{visibleColumns}
|
||||||
|
{sort}
|
||||||
|
{systemFields}
|
||||||
|
{inModal}
|
||||||
|
{users}
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* .title-td:hover {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-td:hover .title-td-contents a {
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: inset 0em 0em 0em 10em rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
} */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{value}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if value}
|
||||||
|
<div class="d-inline-flex">
|
||||||
|
<span class="color border border-2" style="background:{value}" />
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.color {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
top: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{value}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script>
|
||||||
|
import Preview from "../../files/Preview.svelte";
|
||||||
|
|
||||||
|
export let record;
|
||||||
|
export let field;
|
||||||
|
export let graph;
|
||||||
|
|
||||||
|
|
||||||
|
let filePreviews = graph.edges?.filter((ed) => ed.field === field.name && ed.source === record.id)
|
||||||
|
.map((ed) => graph.records.find((r) => r.id === ed.target));
|
||||||
|
// if (edges[0]) {
|
||||||
|
// firstRecord = record._children.find((r) => r.data.id === edges[0].to);
|
||||||
|
// }
|
||||||
|
|
||||||
|
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">
|
||||||
|
<Preview record={file} size="tiny"/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
// let display = new Intl.NumberFormat().format(value);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{value}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script>
|
||||||
|
import PreviewCardSmall from "../../records/PreviewCardSmall.svelte";
|
||||||
|
|
||||||
|
export let record;
|
||||||
|
export let field;
|
||||||
|
export let schemas;
|
||||||
|
export let graph;
|
||||||
|
|
||||||
|
$: recordEdges =
|
||||||
|
graph.edges
|
||||||
|
?.filter((ed) => ed.field === field.name && ed.source === record.id)
|
||||||
|
.map((edge) => {
|
||||||
|
return graph.records.find((r) => r.id === edge.target);
|
||||||
|
})
|
||||||
|
.filter((record) => (!record ? false : true)) ?? [];
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="references">
|
||||||
|
{#each recordEdges as recordEdge}
|
||||||
|
<span class="mr-3">
|
||||||
|
<PreviewCardSmall {schemas} {graph} record={recordEdge}/>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div.references {
|
||||||
|
/* max-width: 148px; */
|
||||||
|
max-height: 48px;
|
||||||
|
/* text-overflow: ellipsis; */
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
/* max-width: 128px; */
|
||||||
|
max-height: 24px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
/* white-space: nowrap; */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div title={value} data-bs-toggle="tooltip" data-bs-placement="top">
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
/* max-width: 128px; */
|
||||||
|
max-height: 24px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
/* white-space: nowrap; */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
export let field;
|
||||||
|
</script>
|
||||||
|
<span
|
||||||
|
class="badge rounded-pill bg-primary bg-opacity-75"
|
||||||
|
style="max-width:64px; overflow:hidden; white-space: nowrap; text-overflow: ellipsis;"
|
||||||
|
title={value}
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
>{value}</span
|
||||||
|
>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a href={value} target="_blank">{value}</a>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
export let pages;
|
||||||
|
export let limit;
|
||||||
|
export let currentPage;
|
||||||
|
export let inModal;
|
||||||
|
export let modalUrl;
|
||||||
|
|
||||||
|
function url(page) {
|
||||||
|
const url = new URL(modalUrl ?? window.location.href);
|
||||||
|
let skip = page * limit - limit;
|
||||||
|
url.searchParams.set("skip", skip);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goto(e, pagenum) {
|
||||||
|
e.preventDefault();
|
||||||
|
const url = new URL(modalUrl ?? window.location.href);
|
||||||
|
let skip = pagenum * limit - limit;
|
||||||
|
url.searchParams.set("skip", skip);
|
||||||
|
if (inModal) {
|
||||||
|
dispatch("refresh", url);
|
||||||
|
} else {
|
||||||
|
window.location = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each pages as i}
|
||||||
|
<li class="page-item">
|
||||||
|
{#if currentPage == i}
|
||||||
|
<span class="page-link active">{i}</span>
|
||||||
|
{:else}
|
||||||
|
<a class="page-link" on:click={(e) => goto(e, i)} href={url(i)}
|
||||||
|
>{i}</a
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
import { range } from "lodash";
|
||||||
|
import NavItem from "./NavItem.svelte";
|
||||||
|
export let inModal;
|
||||||
|
export let modalUrl;
|
||||||
|
export let limit;
|
||||||
|
export let skip;
|
||||||
|
export let total;
|
||||||
|
|
||||||
|
$: totalPages = Math.ceil(total / limit);
|
||||||
|
$: currentPage = Math.ceil((skip - 1) / limit) + 1;
|
||||||
|
|
||||||
|
$: pageRange = range(currentPage - 3, currentPage + 4).filter((i) => {
|
||||||
|
return i > 0 && i <= totalPages;
|
||||||
|
});
|
||||||
|
|
||||||
|
function last(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
goto(totalPages);
|
||||||
|
}
|
||||||
|
function first(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
goto(1);
|
||||||
|
}
|
||||||
|
function page(e, page) {
|
||||||
|
e.preventDefault();
|
||||||
|
goto(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goto(page) {
|
||||||
|
const url = new URL(modalUrl ?? window.location.href);
|
||||||
|
let skip = page * limit - limit;
|
||||||
|
url.searchParams.set("skip", skip);
|
||||||
|
|
||||||
|
if (inModal) {
|
||||||
|
dispatch("refresh", url);
|
||||||
|
} else {
|
||||||
|
window.location = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{#if totalPages > 1}
|
||||||
|
<li class="page-item disabled" class:disabled={currentPage === 1}>
|
||||||
|
<a on:click={first} href="/" class="page-link"> First </a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<NavItem
|
||||||
|
pages={pageRange}
|
||||||
|
{currentPage}
|
||||||
|
{limit}
|
||||||
|
{inModal}
|
||||||
|
{modalUrl}
|
||||||
|
on:refresh
|
||||||
|
/>
|
||||||
|
|
||||||
|
<li class="page-item">
|
||||||
|
<a
|
||||||
|
on:click={last}
|
||||||
|
class="page-link"
|
||||||
|
href="/"
|
||||||
|
class:disabled={currentPage === totalPages}>Last</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<p class="text-muted text-center">
|
||||||
|
Showing
|
||||||
|
<span class="font-medium">{+skip + 1}</span>
|
||||||
|
to
|
||||||
|
<span class="font-medium"
|
||||||
|
>{+skip + limit > total ? total : +skip + limit}</span
|
||||||
|
>
|
||||||
|
of
|
||||||
|
<span class="font-medium">{total}</span>
|
||||||
|
total
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<script>
|
||||||
|
import {createEventDispatcher} from "svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
export let schema;
|
||||||
|
export let operators;
|
||||||
|
export let key;
|
||||||
|
export let value;
|
||||||
|
export let inModal;
|
||||||
|
export let modalUrl;
|
||||||
|
export let systemFields;
|
||||||
|
|
||||||
|
let filterSplit = key.split("_");
|
||||||
|
let operator = filterSplit[filterSplit.length - 1] ?? "eq";
|
||||||
|
|
||||||
|
let fieldName = key.replace("_" + operator, "");
|
||||||
|
let filterField = schema.fields.find((f) => f.name === fieldName);
|
||||||
|
let filterLabel = filterField?.label ?? fieldName;
|
||||||
|
|
||||||
|
function removeFilter(e, k) {
|
||||||
|
e.preventDefault();
|
||||||
|
let filterKey = `filter[${k}]`;
|
||||||
|
const url = new URL(modalUrl ?? window.location.href);
|
||||||
|
url.searchParams.set("skip", "0");
|
||||||
|
url.searchParams.delete(filterKey);
|
||||||
|
if (inModal) {
|
||||||
|
dispatch("refresh", url);
|
||||||
|
} else {
|
||||||
|
window.location = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="applied-filter d-inline-block border border-primary rounded lx-small-text me-1 px-2 py-1"
|
||||||
|
style="line-height:22px ;"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-items-center justify-content-center">
|
||||||
|
{filterLabel}
|
||||||
|
{operators.find((o) => o.name === operator)?.symbol ?? ""}
|
||||||
|
{value}
|
||||||
|
<button
|
||||||
|
on:click={(e) => removeFilter(e, key)}
|
||||||
|
type="button"
|
||||||
|
class="btn-close btn-close ms-1"
|
||||||
|
aria-label="Close"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.applied-filter {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.applied-filter:hover {
|
||||||
|
opacity: .8;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
import {createEventDispatcher} from "svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
export let schema;
|
||||||
|
export let systemFields = [];
|
||||||
|
export let operators;
|
||||||
|
export let inModal;
|
||||||
|
export let modalUrl;
|
||||||
|
let search = "";
|
||||||
|
let systemFieldsFiltered = systemFields;
|
||||||
|
if (schema.type == "collection") {
|
||||||
|
systemFieldsFiltered = systemFields.filter((f) => f.files === false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let filterableFields = [...schema.fields, ...systemFieldsFiltered].filter(
|
||||||
|
(f) => !["file", "json", "tab"].includes(f.ui)
|
||||||
|
);
|
||||||
|
|
||||||
|
let selectedField;
|
||||||
|
|
||||||
|
let selectedInput = "";
|
||||||
|
$: operatorsFiltered = operators.filter(
|
||||||
|
(o) => o.uis.includes(selectedField?.ui) || o.uis[0] == "*"
|
||||||
|
);
|
||||||
|
|
||||||
|
$: selectedOperator = operatorsFiltered[0];
|
||||||
|
|
||||||
|
function addFilter(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let filterPrefix = "";
|
||||||
|
if (schema.fields.find(f => f.name === selectedField.name)) {
|
||||||
|
filterPrefix = "data.";
|
||||||
|
}
|
||||||
|
|
||||||
|
let 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);
|
||||||
|
} else {
|
||||||
|
window.location = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitSearch(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let filterKeyValue = search.split("=")[0] ?? "";
|
||||||
|
if (!filterKeyValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let filterKey = `filter[${filterKeyValue}]`;
|
||||||
|
let filterValue = search.split("=")[1] ?? "";
|
||||||
|
|
||||||
|
if (!filterValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(modalUrl ?? window.location.href);
|
||||||
|
url.searchParams.set("skip", "0");
|
||||||
|
url.searchParams.set(filterKey, filterValue);
|
||||||
|
if (inModal) {
|
||||||
|
dispatch("refresh", url);
|
||||||
|
} else {
|
||||||
|
window.location = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-2 d-flex align-items-center">
|
||||||
|
<div class="btn-group">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<Icon icon="filter"/>
|
||||||
|
<span class="ms-1">Filter</span>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" style="width:300px;">
|
||||||
|
<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">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
bind:value={selectedInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="px-3 py-1 d-flex align-items-center">
|
||||||
|
<button
|
||||||
|
on:click={addFilter}
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Add filter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
import {createEventDispatcher} from "svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
export let schema;
|
||||||
|
export let sort;
|
||||||
|
export let inModal;
|
||||||
|
export let modalUrl;
|
||||||
|
export let systemFields = [];
|
||||||
|
|
||||||
|
$: activeField = [...schema.fields, ...systemFields].find(
|
||||||
|
(f) => f.name === sort || "-" + f.name === sort || "data." + f.name === sort || "-data." + f.name === sort
|
||||||
|
);
|
||||||
|
|
||||||
|
$: sortableFields = schema.fields.filter(
|
||||||
|
(f) => !["reference", "file", "json", "id", "tab"].includes(f.ui)
|
||||||
|
);
|
||||||
|
$: systemFieldsFiltered = systemFields;
|
||||||
|
$: if (schema.type === "collection") {
|
||||||
|
systemFieldsFiltered = systemFields.filter((f) => f.files === false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortField(fieldSort) {
|
||||||
|
const url = new URL(modalUrl ?? window.location.href);
|
||||||
|
url.searchParams.set("sort", fieldSort);
|
||||||
|
if (inModal) {
|
||||||
|
dispatch("refresh", url);
|
||||||
|
} else {
|
||||||
|
window.location = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortAsc(e, field) {
|
||||||
|
e.preventDefault();
|
||||||
|
let prefix = systemFields.includes(el => el.name === field.name) ? "" : "data.";
|
||||||
|
return sortField(prefix + field.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortDesc(e, field) {
|
||||||
|
e.preventDefault();
|
||||||
|
let prefix = systemFields.includes(el => el.name === field.name) ? "" : "data.";
|
||||||
|
return sortField("-" + 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"
|
||||||
|
>
|
||||||
|
{#if sort.startsWith("-")}
|
||||||
|
<Icon icon="arrow-down-wide-short"/>
|
||||||
|
{:else}
|
||||||
|
<Icon icon="arrow-up-short-wide"/>
|
||||||
|
{/if}
|
||||||
|
<span class="ms-1">{activeField.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 == sort
|
||||||
|
? '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 == sort
|
||||||
|
? '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 == sort
|
||||||
|
? '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 == sort
|
||||||
|
? '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>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<script>
|
||||||
|
import FilterFields from "./FilterFields.svelte";
|
||||||
|
import Uploader from "../../files/Uploader.svelte";
|
||||||
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
import SortFields from "./SortFields.svelte";
|
||||||
|
import AppliedFilter from "./AppliedFilter.svelte";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let sort;
|
||||||
|
export let schema;
|
||||||
|
export let operators;
|
||||||
|
export let filter;
|
||||||
|
export let inModal;
|
||||||
|
export let modalUrl;
|
||||||
|
export let records;
|
||||||
|
export let systemFields = [];
|
||||||
|
export let visibleFields = [];
|
||||||
|
|
||||||
|
let url = new URL(window.location.href);
|
||||||
|
|
||||||
|
let csvUrl = url.pathname + "/csv?" + url.searchParams.toString();
|
||||||
|
|
||||||
|
function uploadComplete(e) {
|
||||||
|
records = e.detail;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-3 d-flex align-items-center justify-content-between">
|
||||||
|
<div class=" d-flex">
|
||||||
|
|
||||||
|
<SortFields
|
||||||
|
{schema}
|
||||||
|
{sort}
|
||||||
|
{systemFields}
|
||||||
|
{inModal}
|
||||||
|
{modalUrl}
|
||||||
|
on:refresh
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<FilterFields
|
||||||
|
bind:schema
|
||||||
|
{systemFields}
|
||||||
|
{operators}
|
||||||
|
{filter}
|
||||||
|
{inModal}
|
||||||
|
{modalUrl}
|
||||||
|
on:refresh
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if Object.entries(filter).length > 0}
|
||||||
|
{#each Object.entries(filter) as [k, v]}
|
||||||
|
<AppliedFilter
|
||||||
|
{schema}
|
||||||
|
{operators}
|
||||||
|
key={k}
|
||||||
|
value={v}
|
||||||
|
{inModal}
|
||||||
|
{modalUrl}
|
||||||
|
{systemFields}
|
||||||
|
on:refresh
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center ">
|
||||||
|
{#if schema.type === "collection"}
|
||||||
|
{#if !inModal}
|
||||||
|
<a
|
||||||
|
href="{channel.lucentUrl}/records/new?schema={schema.name}"
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
>
|
||||||
|
New Record
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{:else }
|
||||||
|
<div class="d-inline-block ms-1">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<Icon icon="ellipsis-vertical"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href={csvUrl}
|
||||||
|
>Export to CSV</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href="{channel.lucentUrl}/content/{schema.name}?filter[_sys.status_in]=trashed"
|
||||||
|
>View trashed records</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export function sortByField(from, to, edges, fieldName) {
|
||||||
|
|
||||||
|
if (from === to) {
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
let edgesTosort = edges?.filter((ed) => ed.field === fieldName) ?? [];
|
||||||
|
let remainingEdge = edges?.filter((ed) => ed.field !== fieldName) ?? [];
|
||||||
|
|
||||||
|
let fromElem = edgesTosort.splice(from, 1)[0];
|
||||||
|
edgesTosort.splice(to, 0, fromElem);
|
||||||
|
|
||||||
|
return [...remainingEdge, ...edgesTosort];
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
import { imgurl } from "../files/imageserver";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
export let record;
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let size = "small";
|
||||||
|
export let showFilename = false;
|
||||||
|
let imageSide;
|
||||||
|
let fileSide;
|
||||||
|
let fontSize;
|
||||||
|
if (size == "large") {
|
||||||
|
imageSide = 256;
|
||||||
|
fileSide = 32;
|
||||||
|
fontSize = "20";
|
||||||
|
} else if (size == "medium") {
|
||||||
|
imageSide = 128;
|
||||||
|
fileSide = 12;
|
||||||
|
fontSize = "17";
|
||||||
|
} else if (size == "small") {
|
||||||
|
imageSide = 64;
|
||||||
|
fileSide = 12;
|
||||||
|
fontSize = "15";
|
||||||
|
} else if (size == "tiny") {
|
||||||
|
imageSide = 42;
|
||||||
|
fileSide = 12;
|
||||||
|
fontSize = "13";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if record}
|
||||||
|
{#if record._file.mime.startsWith("image")}
|
||||||
|
<!-- href={imgurl(record)} -->
|
||||||
|
<a
|
||||||
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
|
title={record._file.path}
|
||||||
|
class="d-flex align-items-center justify-content-center "
|
||||||
|
style="width:{imageSide}px;height:{imageSide}px"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="rounded w-100"
|
||||||
|
src={imgurl(record, imageSide, imageSide, "crop")}
|
||||||
|
alt={record._file.path}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<!-- href="{channelurl}/files/download?schema={record._sys.schema}&path={record._file.path}" -->
|
||||||
|
<a
|
||||||
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
|
title={record._file.path}
|
||||||
|
class="btn btn-outline-primary btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
style="width:{imageSide}px;height:{imageSide}px"
|
||||||
|
>
|
||||||
|
<Icon icon="file" width={fileSide} height={fileSide} />
|
||||||
|
<span class="ms-2" style="font-size:{fontSize}px"
|
||||||
|
>.{record._file.path.split(".").pop()}</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#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}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script>
|
||||||
|
import {createEventDispatcher, getContext} from "svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let schema;
|
||||||
|
let mimeTypes = "";
|
||||||
|
let files = [];
|
||||||
|
let isLoading = false;
|
||||||
|
// export function onUploadComplete(files){
|
||||||
|
// console.log(files)
|
||||||
|
// }
|
||||||
|
|
||||||
|
function upload(e) {
|
||||||
|
isLoading = true;
|
||||||
|
files = e.target.files ? [...e.target.files] : [];
|
||||||
|
let formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("schema", schema.name);
|
||||||
|
Array.from(files).forEach(function (file) {
|
||||||
|
formData.append("files[]", file);
|
||||||
|
});
|
||||||
|
dispatch("beforeUpload", files);
|
||||||
|
axios
|
||||||
|
.post(channel.lucentUrl + "/files/upload", formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.data.error) {
|
||||||
|
dispatch("uploadError", response.data.error);
|
||||||
|
} else {
|
||||||
|
dispatch("uploadComplete", response.data);
|
||||||
|
}
|
||||||
|
isLoading = false;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
isLoading = false;
|
||||||
|
console.log(error.response.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<fieldset disabled={isLoading}>
|
||||||
|
<label class="btn btn-primary btn-sm btn-spinner ">
|
||||||
|
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"
|
||||||
|
type="file"
|
||||||
|
id="formFile"
|
||||||
|
multiple
|
||||||
|
accept={mimeTypes}
|
||||||
|
disabled={isLoading}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
|
export function imgurl(record, width = "", height = "", mode = "") {
|
||||||
|
// let argumentString = "-o";
|
||||||
|
// if (mode) {
|
||||||
|
// argumentString += "-mode_fit";
|
||||||
|
// }
|
||||||
|
// if (width) {
|
||||||
|
// argumentString += "-w_" + width;
|
||||||
|
// }
|
||||||
|
// if (height) {
|
||||||
|
// argumentString += "-h_" + height;
|
||||||
|
// }
|
||||||
|
// let pathAr = record._file.path.split(".");
|
||||||
|
// let ext = pathAr.pop();
|
||||||
|
// let filename = record._file.path.replace("." + ext, "");
|
||||||
|
// let cache = "cache/"
|
||||||
|
// if (!mode && !width && !height) {
|
||||||
|
// argumentString = "";
|
||||||
|
// cache = "";
|
||||||
|
// }
|
||||||
|
|
||||||
|
const channel = getContext("channel")
|
||||||
|
return channel.filesUrl + `/thumbs/${record._file.path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fileurl(record) {
|
||||||
|
|
||||||
|
const channel = getContext("channel")
|
||||||
|
return channel.filesUrl + `/${record._file.path}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script>
|
||||||
|
import { uniqueId } from "lodash";
|
||||||
|
export let label;
|
||||||
|
export let value;
|
||||||
|
let id = uniqueId();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
value=""
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={value}
|
||||||
|
id={id}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for={id}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script>
|
||||||
|
import { uniqueId } from "lodash";
|
||||||
|
export let label;
|
||||||
|
export let value;
|
||||||
|
let id = uniqueId();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if label}
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<label for={id} class="form-label">{label}</label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="input-group ">
|
||||||
|
<div style="width:64px;">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
class="form-control form-control-color"
|
||||||
|
bind:value
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input type="text" {id} class="form-control" bind:value />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import flatpickr from "flatpickr";
|
||||||
|
import "flatpickr/dist/flatpickr.css";
|
||||||
|
import "flatpickr/dist/themes/light.css";
|
||||||
|
|
||||||
|
import { uniqueId } from "lodash";
|
||||||
|
export let label;
|
||||||
|
export let value;
|
||||||
|
let pickerInput;
|
||||||
|
let id = uniqueId();
|
||||||
|
|
||||||
|
let flatpickrOptions = {
|
||||||
|
enableTime: false,
|
||||||
|
allowInput: true,
|
||||||
|
dateFormat: "Y-m-d",
|
||||||
|
defaultDate: value,
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
flatpickr(pickerInput, flatpickrOptions);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if label}
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<label for={id} class="form-label">{label}</label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{id}
|
||||||
|
class="form-control"
|
||||||
|
bind:value
|
||||||
|
bind:this={pickerInput}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script>
|
||||||
|
import { uniqueId } from "lodash";
|
||||||
|
export let label;
|
||||||
|
export let name;
|
||||||
|
export let group;
|
||||||
|
export let value;
|
||||||
|
export let help;
|
||||||
|
let id = uniqueId();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
{value}
|
||||||
|
{name}
|
||||||
|
bind:group
|
||||||
|
{id}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for={id}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
{#if help}
|
||||||
|
<span class="text-muted">{help}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import {getContext, onMount} from "svelte";
|
||||||
|
import RecordRow from "./RecordRow.svelte"
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
let records = [];
|
||||||
|
let graph = null;
|
||||||
|
let users = [];
|
||||||
|
onMount(() => {
|
||||||
|
axios
|
||||||
|
.get(channel.lucentUrl + "/home/records")
|
||||||
|
.then((response) => {
|
||||||
|
records = response.data.records;
|
||||||
|
graph = response.data.graph;
|
||||||
|
users = response.data.users;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<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";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let users;
|
||||||
|
export let graph;
|
||||||
|
export let record;
|
||||||
|
let schema = channel.schemas.find((s) => s.name === record._sys.schema);
|
||||||
|
let frieldlyUpdatedAt = formatDistanceToNow(
|
||||||
|
parseJSON(record._sys.updatedAt),
|
||||||
|
{addSuffix: true}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{#if schema.type === "files"}
|
||||||
|
<Preview {record} size="tiny"/>
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
href="{channel.lucentUrl}/records/{record.id}"
|
||||||
|
class="text-decoration-none text-dark d-block"
|
||||||
|
>
|
||||||
|
{previewTitle(channel.schemas, record, graph)}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td><a
|
||||||
|
class="text-decoration-none lx-small-text"
|
||||||
|
href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-center">
|
||||||
|
<Status status={record._sys.status}/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex">
|
||||||
|
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
|
||||||
|
<div class="ms-2">
|
||||||
|
{frieldlyUpdatedAt}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from "svelte";
|
||||||
|
import { basicSetup, EditorView } from "codemirror";
|
||||||
|
import { EditorState, Compartment } from "@codemirror/state";
|
||||||
|
import { keymap } from "@codemirror/view";
|
||||||
|
import { indentWithTab } from "@codemirror/commands";
|
||||||
|
import { json, jsonParseLinter } from "@codemirror/lang-json";
|
||||||
|
import { linter, lintGutter } from "@codemirror/lint";
|
||||||
|
let parentElement;
|
||||||
|
let codeMirrorView;
|
||||||
|
export let value;
|
||||||
|
export let editable = true;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
let language = new Compartment();
|
||||||
|
let tabSize = new Compartment();
|
||||||
|
|
||||||
|
let state = EditorState.create({
|
||||||
|
doc: JSON.stringify(value, null, 4),
|
||||||
|
extensions: [
|
||||||
|
basicSetup,
|
||||||
|
keymap.of([indentWithTab]),
|
||||||
|
language.of(json()),
|
||||||
|
json(),
|
||||||
|
tabSize.of(EditorState.tabSize.of(4)),
|
||||||
|
lintGutter(),
|
||||||
|
basicSetup,
|
||||||
|
EditorView.editable.of(editable),
|
||||||
|
EditorView.updateListener.of(function (e) {
|
||||||
|
if (e.docChanged) {
|
||||||
|
value = e.state.doc.toString();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
linter(jsonParseLinter()),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
codeMirrorView = new EditorView({
|
||||||
|
state,
|
||||||
|
|
||||||
|
parent: parentElement,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (codeMirrorView) {
|
||||||
|
codeMirrorView.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="is-editable-{editable}" bind:this={parentElement} />
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<script>
|
||||||
|
import Sortable from "sortablejs";
|
||||||
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
|
export let sortableClass;
|
||||||
|
// export let handle;
|
||||||
|
export let isTable = false;
|
||||||
|
export let sortableInstance;
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
let sortableContainer;
|
||||||
|
|
||||||
|
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)",
|
||||||
|
onUpdate: function (/**Event*/ evt) {
|
||||||
|
// reorder(evt.oldIndex,evt.newIndex);
|
||||||
|
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>
|
||||||
|
{:else}
|
||||||
|
<div class="sortable-container {sortableClass}" bind:this={sortableContainer}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<script>
|
||||||
|
import {onDestroy, onMount} from "svelte";
|
||||||
|
|
||||||
|
import tinymce from "tinymce/tinymce";
|
||||||
|
import "tinymce/models/dom";
|
||||||
|
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 "tinymce/plugins/link";
|
||||||
|
import "tinymce/plugins/code";
|
||||||
|
import "tinymce/plugins/image";
|
||||||
|
import "tinymce/plugins/table";
|
||||||
|
import "tinymce/plugins/codesample";
|
||||||
|
import "tinymce/plugins/media";
|
||||||
|
|
||||||
|
import "tinymce/plugins/lists";
|
||||||
|
import "tinymce/plugins/autoresize";
|
||||||
|
import "tinymce/plugins/wordcount";
|
||||||
|
|
||||||
|
export let value = "";
|
||||||
|
export let additionalConfig = {};
|
||||||
|
let lastVal = "";
|
||||||
|
let textareaEl;
|
||||||
|
let activeEditor;
|
||||||
|
let editorWrapper;
|
||||||
|
const plugins = [
|
||||||
|
"autoresize",
|
||||||
|
"code",
|
||||||
|
"image",
|
||||||
|
"table",
|
||||||
|
"codesample",
|
||||||
|
"link",
|
||||||
|
"lists",
|
||||||
|
"media",
|
||||||
|
"wordcount",
|
||||||
|
];
|
||||||
|
const toolbar =
|
||||||
|
"bold italic underline strikethrough removeformat | link | subscript superscript bullist numlist media image codesample table code wordcount blockquote indent outdent blocks";
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (activeEditor) {
|
||||||
|
activeEditor.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const config = {
|
||||||
|
target: textareaEl,
|
||||||
|
toolbar_mode: "sliding",
|
||||||
|
toolbar_sticky: true,
|
||||||
|
skin: false,
|
||||||
|
content_css: false,
|
||||||
|
content_style: contentUiSkinCss.toString(),
|
||||||
|
branding: false,
|
||||||
|
inline: false,
|
||||||
|
plugins: plugins,
|
||||||
|
contextmenu: false,
|
||||||
|
menubar: false,
|
||||||
|
statusbar: false,
|
||||||
|
entity_encoding: "raw",
|
||||||
|
convert_urls: false,
|
||||||
|
toolbar: toolbar,
|
||||||
|
image_caption: true,
|
||||||
|
relative_urls: false,
|
||||||
|
browser_spellcheck: true,
|
||||||
|
max_height: 600,
|
||||||
|
// media_poster: false,
|
||||||
|
// content_style:
|
||||||
|
// "body {font-family: 'Averta Std', sans serif; color: #152F77}",
|
||||||
|
setup: function (editor) {
|
||||||
|
activeEditor = editor;
|
||||||
|
|
||||||
|
editor.on("init", function (e) {
|
||||||
|
editor.setContent(value ?? "");
|
||||||
|
});
|
||||||
|
|
||||||
|
// editor.on("blur", function (e) {
|
||||||
|
// let content = setImageDimensions(editor.getContent());
|
||||||
|
// dispatch("editorBlur", content);
|
||||||
|
// editorWrapper.classList.remove("editorFocus");
|
||||||
|
// // return false;
|
||||||
|
// });
|
||||||
|
|
||||||
|
// editor.on("focus", function (e) {
|
||||||
|
// editorWrapper.classList.add("editorFocus");
|
||||||
|
// // return false;
|
||||||
|
// });
|
||||||
|
|
||||||
|
editor.on("change input undo redo", function (e) {
|
||||||
|
lastVal = editor.getContent();
|
||||||
|
if (lastVal !== value) {
|
||||||
|
value = lastVal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
tinymce.init({...config, ...additionalConfig});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={editorWrapper} class="tox-wrapper">
|
||||||
|
<div class="form-control" bind:this={textareaEl}>
|
||||||
|
{@html value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.tox:not(.tox-tinymce-inline) .tox-editor-header) {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ced4da;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 4px 0;
|
||||||
|
transition: box-shadow 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.tox-tinymce) {
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount, onDestroy, tick } from "svelte";
|
||||||
|
|
||||||
|
import tinymce from "tinymce/tinymce";
|
||||||
|
import "tinymce/models/dom";
|
||||||
|
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 "tinymce/plugins/link";
|
||||||
|
import "tinymce/plugins/code";
|
||||||
|
import "tinymce/plugins/image";
|
||||||
|
import "tinymce/plugins/table";
|
||||||
|
import "tinymce/plugins/codesample";
|
||||||
|
import "tinymce/plugins/media";
|
||||||
|
|
||||||
|
import "tinymce/plugins/lists";
|
||||||
|
import "tinymce/plugins/autoresize";
|
||||||
|
import "tinymce/plugins/wordcount";
|
||||||
|
import BrowseModal from "../records/elements/BrowseModal.svelte";
|
||||||
|
|
||||||
|
export let schemas;
|
||||||
|
export let schema;
|
||||||
|
export let field;
|
||||||
|
export let value = "";
|
||||||
|
export let additionalConfig = {};
|
||||||
|
let browseModal;
|
||||||
|
let lastVal = "";
|
||||||
|
let textareaEl;
|
||||||
|
let activeEditor;
|
||||||
|
let editorWrapper;
|
||||||
|
const plugins = [
|
||||||
|
"autoresize",
|
||||||
|
"code",
|
||||||
|
"image",
|
||||||
|
"table",
|
||||||
|
"codesample",
|
||||||
|
"link",
|
||||||
|
"lists",
|
||||||
|
"media",
|
||||||
|
"wordcount",
|
||||||
|
];
|
||||||
|
const toolbar =
|
||||||
|
"bold italic underline strikethrough removeformat | link image fileManager | subscript superscript bullist numlist media codesample table code wordcount blockquote indent outdent blocks";
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (activeEditor) {
|
||||||
|
activeEditor.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const config = {
|
||||||
|
target: textareaEl,
|
||||||
|
toolbar_mode: "sliding",
|
||||||
|
toolbar_sticky: true,
|
||||||
|
skin: false,
|
||||||
|
content_css: false,
|
||||||
|
content_style: contentUiSkinCss.toString(),
|
||||||
|
branding: false,
|
||||||
|
inline: false,
|
||||||
|
plugins: plugins,
|
||||||
|
contextmenu: false,
|
||||||
|
menubar: false,
|
||||||
|
statusbar: false,
|
||||||
|
entity_encoding: "raw",
|
||||||
|
convert_urls: false,
|
||||||
|
toolbar: toolbar,
|
||||||
|
image_caption: true,
|
||||||
|
relative_urls: false,
|
||||||
|
browser_spellcheck: true,
|
||||||
|
max_height: 600,
|
||||||
|
// media_poster: false,
|
||||||
|
// content_style:
|
||||||
|
// "body {font-family: 'Averta Std', sans serif; color: #152F77}",
|
||||||
|
setup: function (editor) {
|
||||||
|
activeEditor = editor;
|
||||||
|
|
||||||
|
editor.on("init", function (e) {
|
||||||
|
editor.setContent(value ?? "");
|
||||||
|
});
|
||||||
|
|
||||||
|
// editor.on("blur", function (e) {
|
||||||
|
// let content = setImageDimensions(editor.getContent());
|
||||||
|
// dispatch("editorBlur", content);
|
||||||
|
// editorWrapper.classList.remove("editorFocus");
|
||||||
|
// // return false;
|
||||||
|
// });
|
||||||
|
|
||||||
|
// editor.on("focus", function (e) {
|
||||||
|
// editorWrapper.classList.add("editorFocus");
|
||||||
|
// // return false;
|
||||||
|
// });
|
||||||
|
|
||||||
|
editor.on("change input undo redo", function (e) {
|
||||||
|
lastVal = editor.getContent();
|
||||||
|
if (lastVal !== value) {
|
||||||
|
value = lastVal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.ui.registry.addMenuButton("fileManager", {
|
||||||
|
icon: "upload",
|
||||||
|
fetch: (callback) => {
|
||||||
|
const items = field.collections.map((c) => {
|
||||||
|
return {
|
||||||
|
type: "menuitem",
|
||||||
|
text:
|
||||||
|
schemas.find((s) => s.name == c).label ??
|
||||||
|
"Schema missing",
|
||||||
|
onAction: () => {
|
||||||
|
openBrowseModal(c);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
callback(items);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
tinymce.init({ ...config, ...additionalConfig });
|
||||||
|
});
|
||||||
|
|
||||||
|
function openBrowseModal(aschema) {
|
||||||
|
browseModal.open(aschema);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function insert(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const recordsToInsert = e.detail.records;
|
||||||
|
let fileSchema = schemas.find(
|
||||||
|
(s) => s.name === recordsToInsert[0]._sys.schema
|
||||||
|
);
|
||||||
|
let contentTonInsert = recordsToInsert
|
||||||
|
.map((r) => {
|
||||||
|
if (r._file.mime.startsWith("image")) {
|
||||||
|
let fileUrl =
|
||||||
|
fileSchema.objectStorageProxy + "/" + r._file.path;
|
||||||
|
return `<img src="${fileUrl}" alt="${r._file.path}" width="${r._file.width}" height="${r._file.height}" />`;
|
||||||
|
} else {
|
||||||
|
let fileUrl =
|
||||||
|
fileSchema.objectStorageUrl + "/" + r._file.path;
|
||||||
|
return `<a href="${fileUrl}" title="${r._file.path}">${r._file.path}</a>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join("<br />");
|
||||||
|
|
||||||
|
activeEditor.insertContent(contentTonInsert);
|
||||||
|
await tick();
|
||||||
|
browseModal.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={editorWrapper} class="tox-wrapper">
|
||||||
|
<div class="form-control" bind:this={textareaEl}>
|
||||||
|
{@html value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if field && schema}
|
||||||
|
<BrowseModal bind:this={browseModal} on:insert={insert} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.tox:not(.tox-tinymce-inline) .tox-editor-header) {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ced4da;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 4px 0;
|
||||||
|
transition: box-shadow 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.tox-tinymce) {
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script>
|
||||||
|
import Avatar from "../account/Avatar.svelte";
|
||||||
|
import {fly} from "svelte/transition";
|
||||||
|
import {createEventDispatcher} from "svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
export let member;
|
||||||
|
export let roles;
|
||||||
|
|
||||||
|
function update(e, newRole) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("update", {
|
||||||
|
user: member.id,
|
||||||
|
role: newRole,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
transition:fly={{ duration: 200 }}
|
||||||
|
class="d-flex justify-content-between align-items-center mb-3 "
|
||||||
|
>
|
||||||
|
<div class="d-flex align-items-center status-{member.role}">
|
||||||
|
<Avatar name={member.name ?? "" } side="32"/>
|
||||||
|
<div class="ms-3 ">
|
||||||
|
<div>
|
||||||
|
<span class="fs-5">
|
||||||
|
{member.name}
|
||||||
|
</span>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
{member.role}
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
{#each roles as role}
|
||||||
|
{#if member.role !== role}
|
||||||
|
<button
|
||||||
|
class="dropdown-item"
|
||||||
|
on:click={(e) => update(e,role)}
|
||||||
|
>
|
||||||
|
Convert to {role}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.status-removed {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<script>
|
||||||
|
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";
|
||||||
|
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let title;
|
||||||
|
export let users;
|
||||||
|
export let roles;
|
||||||
|
let name;
|
||||||
|
let email;
|
||||||
|
let role;
|
||||||
|
let errorMessage = "";
|
||||||
|
let successAlert;
|
||||||
|
|
||||||
|
function submitInvite(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
invite(name, email, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
function invite(newName, newEmail, newRole) {
|
||||||
|
errorMessage = "";
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post(channel.lucentUrl + "/members/invite", {
|
||||||
|
name: newName,
|
||||||
|
email: newEmail,
|
||||||
|
role: newRole,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
successAlert.show("User was invited");
|
||||||
|
users = [...users, response.data.user];
|
||||||
|
name = null;
|
||||||
|
email = null;
|
||||||
|
role = null;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
errorMessage = error.response?.data?.error ?? "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
errorMessage = "";
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post(channel.lucentUrl + "/members/update", {
|
||||||
|
id: e.detail.user,
|
||||||
|
role: e.detail.role,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
successAlert.show("Users updated");
|
||||||
|
users = response.data.users;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
errorMessage = error.response?.data?.error ?? "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="wrapper-tiny transparent mb-5">
|
||||||
|
<div class="lx-card mt-5">
|
||||||
|
<h3 class="header-small mb-5">Invite people</h3>
|
||||||
|
<ErrorAlert message={errorMessage}/>
|
||||||
|
<SuccessAlert bind:this={successAlert}/>
|
||||||
|
|
||||||
|
<form on:submit={submitInvite}>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="inviteeName" class="form-label"
|
||||||
|
>Invitee Name</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={name}
|
||||||
|
class="form-control"
|
||||||
|
id="inviteeName"
|
||||||
|
placeholder="Member name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="inviteeEmail" class="form-label"
|
||||||
|
>Invitee Email Address</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
bind:value={email}
|
||||||
|
class="form-control"
|
||||||
|
id="inviteeEmail"
|
||||||
|
placeholder="Member email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="me-3">
|
||||||
|
{#each roles.filter((r) => r !== "removed") as arole}
|
||||||
|
<Radio
|
||||||
|
bind:group={role}
|
||||||
|
value={arole}
|
||||||
|
name="role"
|
||||||
|
label={arole}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 d-block text-center">
|
||||||
|
<SpinnerButton label="Invite"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lx-card mt-3">
|
||||||
|
<h3 class="header-small mb-5">Members</h3>
|
||||||
|
{#each users as user}
|
||||||
|
<MemberSettingsCard
|
||||||
|
member={user}
|
||||||
|
roles={roles}
|
||||||
|
on:update={update}
|
||||||
|
on:reinvite={(e) => invite(e.detail.email, e.detail.role)}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
export let recordGraph;
|
||||||
|
export let record;
|
||||||
|
export let schema;
|
||||||
|
export let isCreateMode;
|
||||||
|
export let active = "_default";
|
||||||
|
|
||||||
|
let tabs = schema.fields.filter((f) => f.ui === "tab");
|
||||||
|
let mainTab = {
|
||||||
|
label: "Main",
|
||||||
|
name: "_default",
|
||||||
|
};
|
||||||
|
let graphTab = {
|
||||||
|
label: "Graph",
|
||||||
|
name: "_graph",
|
||||||
|
};
|
||||||
|
if (isCreateMode) {
|
||||||
|
tabs = [mainTab, ...tabs];
|
||||||
|
} else {
|
||||||
|
tabs = [mainTab, ...tabs, graphTab];
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGraph(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
active = "_graph";
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeTab(e, tabName) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (tabName == "_graph") {
|
||||||
|
showGraph(e);
|
||||||
|
} else {
|
||||||
|
active = tabName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if tabs.length > 1}
|
||||||
|
<ul class="nav nav-pills mb-4 justify-content-center">
|
||||||
|
{#each tabs as tab}
|
||||||
|
<li class="nav-item">
|
||||||
|
<button
|
||||||
|
on:click={(e) => changeTab(e, tab.name)}
|
||||||
|
class="nav-link"
|
||||||
|
class:active={active === tab.name}
|
||||||
|
aria-current="page"
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<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 FilePreview from "./FilePreview.svelte"
|
||||||
|
import ContentTabs from "./ContentTabs.svelte"
|
||||||
|
import FormField from "./FormField.svelte"
|
||||||
|
import Graph from "./Graph.svelte"
|
||||||
|
import Info from "./Info.svelte"
|
||||||
|
import ErrorAlert from "../common/ErrorAlert.svelte"
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
|
||||||
|
export let schema;
|
||||||
|
export let title;
|
||||||
|
export let record;
|
||||||
|
export let graph = {
|
||||||
|
records: [],
|
||||||
|
edges: []
|
||||||
|
};
|
||||||
|
export let recordHistory;
|
||||||
|
export let isCreateMode;
|
||||||
|
export let users;
|
||||||
|
let originalContent;
|
||||||
|
let activeContentTab = "_default";
|
||||||
|
let recordGraph = null;
|
||||||
|
$: hasUnsavedData = false;
|
||||||
|
$: validationErrors = null;
|
||||||
|
$: errorMessage = validationErrors
|
||||||
|
? `Record submission failed. ${
|
||||||
|
Object.entries(validationErrors).length
|
||||||
|
} error(s)`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let activeFields = schema.fields.filter(
|
||||||
|
(f) => f.name !== "id"
|
||||||
|
);
|
||||||
|
|
||||||
|
let tabname = "_default";
|
||||||
|
let fieldToTabs = schema.fields.reduce((c, f) => {
|
||||||
|
if (f.ui === "tab") {
|
||||||
|
tabname = f.name;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
c[tabname] = [...(c[tabname] ?? []), f.name];
|
||||||
|
return c;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setOriginalContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
function setOriginalContent() {
|
||||||
|
originalContent = {
|
||||||
|
data: JSON.parse(JSON.stringify(record.data)),
|
||||||
|
_sys: JSON.parse(JSON.stringify(record._sys)),
|
||||||
|
_file: JSON.parse(JSON.stringify(record._file)),
|
||||||
|
edges: JSON.parse(JSON.stringify(graph.edges)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
hasUnsavedData = checkUnsavedData();
|
||||||
|
});
|
||||||
|
|
||||||
|
function beforeUnload(e) {
|
||||||
|
// Cancel the event as stated by the standard.
|
||||||
|
// e.preventDefault();
|
||||||
|
// console.log(hasUnsavedData);
|
||||||
|
if (hasUnsavedData) {
|
||||||
|
return (e.returnValue =
|
||||||
|
"You have unsaved changes. Are you sure you want to exit?");
|
||||||
|
}
|
||||||
|
// Chrome requires returnValue to be set.
|
||||||
|
// e.returnValue = "";
|
||||||
|
delete e["returnValue"];
|
||||||
|
// more compatibility
|
||||||
|
// return true;
|
||||||
|
return "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkUnsavedData() {
|
||||||
|
if (isCreateMode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !isEqual(originalContent, {
|
||||||
|
data: record.data,
|
||||||
|
_sys: record._sys,
|
||||||
|
_file: record._file,
|
||||||
|
edges: graph.edges,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
console.log("SAVE: Attempt");
|
||||||
|
validationErrors = null;
|
||||||
|
errorMessage = "";
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (!hasUnsavedData && !isCreateMode) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!record) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove trashed edges
|
||||||
|
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id) ?? null;
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post(channel.lucentUrl + "/records", {
|
||||||
|
record: record,
|
||||||
|
edges: graph.edges,
|
||||||
|
isCreateMode: isCreateMode,
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
console.log("SAVE: SAVED");
|
||||||
|
|
||||||
|
if (isCreateMode) {
|
||||||
|
window.location = channel.lucentUrl + "/records/" + record.id;
|
||||||
|
} else {
|
||||||
|
record = response.data.records[0] ?? null;
|
||||||
|
if (!record) {
|
||||||
|
// means trashed
|
||||||
|
hasUnsavedData = false;
|
||||||
|
window.location = channel.lucentUrl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
graph = response.data;
|
||||||
|
setOriginalContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(null);
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
// setOriginalContent();
|
||||||
|
if (error.response) {
|
||||||
|
if (typeof error.response.data.error === "string") {
|
||||||
|
errorMessage = error.response.data.error;
|
||||||
|
} else {
|
||||||
|
validationErrors = error.response.data.error;
|
||||||
|
console.log(validationErrors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(null);
|
||||||
|
// msgSuccess = null;
|
||||||
|
// msgError = error.response.data.error;
|
||||||
|
// submitted = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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)}
|
||||||
|
<div
|
||||||
|
style="position:fixed;bottom:0;left:0px;width:100%;background:rgba(255,255,255,.7);z-index:10"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex mt-4 mb-3 align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<StatusSelect bind:status={record._sys.status} {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}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="spinner-border spinner-border-sm"
|
||||||
|
role="status"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<ErrorAlert message={errorMessage}/>
|
||||||
|
|
||||||
|
<div class=" mt-4" style="margin-bottom:150px">
|
||||||
|
<ContentTabs
|
||||||
|
{schema}
|
||||||
|
{isCreateMode}
|
||||||
|
bind:active={activeContentTab}
|
||||||
|
{record}
|
||||||
|
bind:recordGraph
|
||||||
|
/>
|
||||||
|
{#if !["_graph", "_info"].includes(activeContentTab)}
|
||||||
|
<FilePreview {record} {schema}/>
|
||||||
|
<!-- <fieldset disabled="disabled"> -->
|
||||||
|
{#each activeFields as field (field.name)}
|
||||||
|
{#if fieldToTabs[activeContentTab].includes(field.name)}
|
||||||
|
<FormField
|
||||||
|
bind:data={record.data}
|
||||||
|
bind:graph={graph}
|
||||||
|
{field}
|
||||||
|
{schema}
|
||||||
|
{record}
|
||||||
|
{validationErrors}
|
||||||
|
{isCreateMode}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<!-- </fieldset> -->
|
||||||
|
{:else if activeContentTab === "_graph"}
|
||||||
|
<Graph {graph} {record}/>
|
||||||
|
{:else if activeContentTab === "_info"}
|
||||||
|
<Info bind:record {users} {schema}/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<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">
|
||||||
|
<!--{#if channel.previewTargets.length > 0}-->
|
||||||
|
<!-- <h6 class="dropdown-header">Preview targets</h6>-->
|
||||||
|
<!-- {#each channel.previewTargets as previewTarget}-->
|
||||||
|
<!-- <a-->
|
||||||
|
<!-- class="dropdown-item"-->
|
||||||
|
<!-- target="_blank"-->
|
||||||
|
<!-- rel="noreferrer"-->
|
||||||
|
<!-- href="{previewTarget.url}?id={record.data-->
|
||||||
|
<!-- .id}&schema={record._sys.schema}"-->
|
||||||
|
<!-- >{previewTarget.label}</a-->
|
||||||
|
<!-- >-->
|
||||||
|
<!-- {/each}-->
|
||||||
|
<!--{/if}-->
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script>
|
||||||
|
import Preview from "../files/Preview.svelte";
|
||||||
|
import {fileurl} from "../files/imageserver"
|
||||||
|
|
||||||
|
export let record;
|
||||||
|
export let schema;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if schema.type === "files"}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col" style="max-width:276px">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.list-group {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<script>
|
||||||
|
import Text from "./elements/Text.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";
|
||||||
|
import Url from "./elements/Url.svelte";
|
||||||
|
import Date from "./elements/Date.svelte";
|
||||||
|
import UUID from "./elements/UUID.svelte";
|
||||||
|
import File from "./elements/File.svelte";
|
||||||
|
import Textarea from "./elements/Textarea.svelte";
|
||||||
|
import Datetime from "./elements/Datetime.svelte";
|
||||||
|
import RichEditor from "./elements/RichEditor.svelte";
|
||||||
|
import Json from "./elements/JSON.svelte";
|
||||||
|
import FieldHeader from "./elements/FieldHeader.svelte";
|
||||||
|
import ReferenceTable from "./elements/ReferenceTable.svelte";
|
||||||
|
|
||||||
|
const formElements = {
|
||||||
|
text: Text,
|
||||||
|
textarea: Textarea,
|
||||||
|
rich: RichEditor,
|
||||||
|
color: Color,
|
||||||
|
checkbox: Checkbox,
|
||||||
|
number: Number,
|
||||||
|
url: Url,
|
||||||
|
date: Date,
|
||||||
|
datetime: Datetime,
|
||||||
|
uuid: UUID,
|
||||||
|
json: Json,
|
||||||
|
};
|
||||||
|
|
||||||
|
export let field;
|
||||||
|
export let data;
|
||||||
|
export let schema;
|
||||||
|
export let record;
|
||||||
|
export let graph;
|
||||||
|
export let validationErrors;
|
||||||
|
export let isCreateMode;
|
||||||
|
let formElement = formElements[field.info.name];
|
||||||
|
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}
|
||||||
|
{schema}
|
||||||
|
{field}
|
||||||
|
{validationErrors}
|
||||||
|
/>
|
||||||
|
{:else if field.info.name === "reference" && field.layout === "table"}
|
||||||
|
<ReferenceTable
|
||||||
|
bind:graph
|
||||||
|
{record}
|
||||||
|
{schema}
|
||||||
|
{field}
|
||||||
|
{validationErrors}
|
||||||
|
/>
|
||||||
|
{:else if field.info.name === "reference"}
|
||||||
|
<Reference
|
||||||
|
bind:graph
|
||||||
|
{record}
|
||||||
|
{schema}
|
||||||
|
{field}
|
||||||
|
{validationErrors}
|
||||||
|
/>
|
||||||
|
{: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]}
|
||||||
|
{field}
|
||||||
|
{id}
|
||||||
|
{validationErrors}
|
||||||
|
{isCreateMode}
|
||||||
|
/>
|
||||||
|
{:else if field.info.name === "textarea"}
|
||||||
|
<Textarea
|
||||||
|
bind:value={data[field.name]}
|
||||||
|
{field}
|
||||||
|
{validationErrors}
|
||||||
|
{isCreateMode}
|
||||||
|
{id}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<svelte:component
|
||||||
|
this={formElement}
|
||||||
|
bind:value={data[field.name]}
|
||||||
|
{schema}
|
||||||
|
{field}
|
||||||
|
{validationErrors}
|
||||||
|
{isCreateMode}
|
||||||
|
{id}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<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";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let graph;
|
||||||
|
export let record;
|
||||||
|
|
||||||
|
let parentEdgesByField = graph.edges
|
||||||
|
.filter((edge) => edge.source !== record.id && edge.depth === 0)
|
||||||
|
.reduce((carry, edge) => {
|
||||||
|
|
||||||
|
let schemaField = edge.sourceSchema + edge.field;
|
||||||
|
|
||||||
|
let arecord = graph.records.find((n) => {
|
||||||
|
return n.id === edge.source;
|
||||||
|
});
|
||||||
|
if (!carry[schemaField]) {
|
||||||
|
|
||||||
|
let schema = channel.schemas.find((s) => s.name === edge.sourceSchema);
|
||||||
|
carry[schemaField] = {
|
||||||
|
field: schema.fields.find((f) => f.name === edge.field),
|
||||||
|
schema: schema,
|
||||||
|
nodes: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (arecord) {
|
||||||
|
carry[schemaField].nodes.push(arecord);
|
||||||
|
}
|
||||||
|
return carry;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
let childrenEdgesByField = graph.edges
|
||||||
|
.filter((edge) => edge.source === record.id && edge.depth === 0)
|
||||||
|
.reduce((carry, edge) => {
|
||||||
|
|
||||||
|
let schemaField = edge.targetSchema + edge.field;
|
||||||
|
|
||||||
|
if (!carry[schemaField]) {
|
||||||
|
carry[schemaField] = {
|
||||||
|
field: channel.schemas
|
||||||
|
.find((s) => s.name === record._sys.schema)
|
||||||
|
.fields.find((f) => f.name === edge.field),
|
||||||
|
nodes: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let arecord = graph.records.find((n) => {
|
||||||
|
return n.id === edge.target;
|
||||||
|
});
|
||||||
|
if (arecord) {
|
||||||
|
carry[schemaField].nodes.push(arecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
return carry;
|
||||||
|
}, {});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each Object.entries(parentEdgesByField) as [fieldName, fieldData]}
|
||||||
|
<div class="lx-card mt-3">
|
||||||
|
<div class="text-center mb-3 d-flex justify-content-center align-items-center text-uppercase ">
|
||||||
|
<span>{fieldData.schema.label}</span>
|
||||||
|
<Icon icon="angle-right" width="12" height="12"/>
|
||||||
|
<span>{fieldData.field.label}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-center text-center flex-wrap">
|
||||||
|
{#each fieldData.nodes as node}
|
||||||
|
{#if node._file?.path}
|
||||||
|
<div class="ms-2 mb-2" style="max-height:64px;">
|
||||||
|
<Preview record={node} size="small"/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="ms-2 mb-2">
|
||||||
|
<PreviewCardSmall {graph} record={node}/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<!-- <div class="text-center mt-3 d-block">{fieldData.field.label}</div>-->
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#if Object.entries(parentEdgesByField).length > 0}
|
||||||
|
<div class="text-center my-4">
|
||||||
|
<Icon icon="angles-down" width="32" height="32"/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div style="max-width:400px;margin:0 auto;">
|
||||||
|
<PreviewCard {graph} record={record}/>
|
||||||
|
</div>
|
||||||
|
{#if Object.entries(childrenEdgesByField).length > 0}
|
||||||
|
<div class="text-center my-4">
|
||||||
|
<Icon icon="angles-down" width="32" height="32"/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each Object.entries(childrenEdgesByField) as [fieldName, fieldData]}
|
||||||
|
<div class="lx-card mt-3">
|
||||||
|
<div class="text-center mb-5 d-block">{fieldData.field.label}</div>
|
||||||
|
<div class="d-flex justify-content-center text-center flex-wrap">
|
||||||
|
{#each fieldData.nodes as node}
|
||||||
|
{#if fieldData.field.info.ui === "file"}
|
||||||
|
<div
|
||||||
|
class="ms-2 mb-2"
|
||||||
|
style="max-width:64px;overflow:hidden;white-space: nowrap;text-overflow: ellipsis;"
|
||||||
|
>
|
||||||
|
<Preview
|
||||||
|
record={node}
|
||||||
|
size="small"
|
||||||
|
showFilename={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="ms-2 mb-2">
|
||||||
|
<PreviewCardSmall {graph} record={node}/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
<script>
|
||||||
|
import {friendlyDate} from "../../helpers";
|
||||||
|
import Avatar from "../account/Avatar.svelte";
|
||||||
|
import {usernameById} from "../account/users";
|
||||||
|
import {isEqual} from "lodash";
|
||||||
|
import Status from "./Status.svelte";
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
import RevisionCell from "./revisions/RevisionCell.svelte";
|
||||||
|
|
||||||
|
export let record;
|
||||||
|
export let users;
|
||||||
|
export let schema;
|
||||||
|
let rollbackError = "";
|
||||||
|
$: revisions = [];
|
||||||
|
$: fieldsWithDiff = [];
|
||||||
|
$: selectedRevision = null;
|
||||||
|
$: recordEdges = {};
|
||||||
|
$: selectedRevisionEdges = {};
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(`/records/${record.id}/revisions`)
|
||||||
|
.then((response) => {
|
||||||
|
revisions = response.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
function getEdgesByField(edges) {
|
||||||
|
return schema.fields
|
||||||
|
.filter((f) => ["file", "reference"].includes(f.ui))
|
||||||
|
.reduce((c, f) => {
|
||||||
|
let fieldEdges = edges
|
||||||
|
.filter((e) => e.field === f.name)
|
||||||
|
.map((e) =>
|
||||||
|
record._children.find((child) => child.id === e.to)
|
||||||
|
);
|
||||||
|
|
||||||
|
c[f.name] = fieldEdges;
|
||||||
|
return c;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare(e, revision) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedRevision = revision;
|
||||||
|
|
||||||
|
fieldsWithDiff = schema.fields.filter((f) => {
|
||||||
|
return !isEqual(selectedRevision.data[f.name], record.data[f.name]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollback(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
rollbackError = "";
|
||||||
|
axios
|
||||||
|
.post(
|
||||||
|
`/records/${record.id}/rollback/${selectedRevision._sys.version}`
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const firstError = error.response.data.error;
|
||||||
|
rollbackError =
|
||||||
|
firstError.fieldLabel + ": " + firstError.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="lx-card ">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-8">
|
||||||
|
<div>
|
||||||
|
<span class="label text-end text-muted">record id </span>
|
||||||
|
<small>{record.id}</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="label text-end text-muted">current version </span>
|
||||||
|
{record._sys.version}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="label text-end text-muted"> created </span>
|
||||||
|
<Avatar
|
||||||
|
name={usernameById(users, record._sys.createdBy)}
|
||||||
|
side={24}
|
||||||
|
/>
|
||||||
|
{friendlyDate(record._sys.createdAt)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="label text-end text-muted">updated </span>
|
||||||
|
<Avatar
|
||||||
|
name={usernameById(users, record._sys.updatedBy)}
|
||||||
|
side={24}
|
||||||
|
/>
|
||||||
|
{friendlyDate(record._sys.updatedAt)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<span class="label d-block text-muted "
|
||||||
|
>Rules for this schema
|
||||||
|
</span>
|
||||||
|
<small>
|
||||||
|
Revisions are retained for {schema.revisionRetentionDays} days
|
||||||
|
<br/>
|
||||||
|
Each record maintains the last {schema.revisionRetentionNumber}
|
||||||
|
versions
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lx-card mt-4">
|
||||||
|
{#if schema.revisionRetentionDays > 0}
|
||||||
|
<div class="header-small mb-3">Revisions</div>
|
||||||
|
{#each revisions as revision}
|
||||||
|
{#if revision._sys.version != record._sys.version}
|
||||||
|
<div
|
||||||
|
class="row p-2 rounded"
|
||||||
|
class:active={revision._sys.version ===
|
||||||
|
selectedRevision?._sys.version}
|
||||||
|
>
|
||||||
|
<div class="col-2">
|
||||||
|
<Status status={revision._sys.status}/>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">version {revision._sys.version}</div>
|
||||||
|
<div class="col-5">
|
||||||
|
<Avatar
|
||||||
|
name={usernameById(users, revision._sys.updatedBy)}
|
||||||
|
side={24}
|
||||||
|
/>
|
||||||
|
{friendlyDate(revision._sys.updatedAt)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-3 text-center">
|
||||||
|
<button
|
||||||
|
disabled={revision._sys.version ===
|
||||||
|
selectedRevision?._sys.version}
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
on:click={(e) => compare(e, revision)}
|
||||||
|
>Compare
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<div class="card-body">
|
||||||
|
<span>Revisions are not enabled for this Schema</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if selectedRevision}
|
||||||
|
<div class="mt-4">
|
||||||
|
{#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"
|
||||||
|
>
|
||||||
|
Rollback to version {selectedRevision._sys.version}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if rollbackError}
|
||||||
|
<span class="d-block text-danger mt-3">{rollbackError}</span>
|
||||||
|
{/if}
|
||||||
|
<div class="mt-3">
|
||||||
|
{#each fieldsWithDiff as field}
|
||||||
|
<!-- <div
|
||||||
|
class=" lx-card d-flex p-4 mb-4"
|
||||||
|
style="overflow:hidden"
|
||||||
|
> -->
|
||||||
|
<!-- <div class="d-block" style="width:200px;">
|
||||||
|
{field.label}
|
||||||
|
</div> -->
|
||||||
|
<div
|
||||||
|
class="lx-card row p-4 mb-4 w-100"
|
||||||
|
style="overflow:hidden"
|
||||||
|
>
|
||||||
|
<div class="col-5">
|
||||||
|
<RevisionCell
|
||||||
|
edges={recordEdges}
|
||||||
|
{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>
|
||||||
|
<div class="col-5">
|
||||||
|
<RevisionCell
|
||||||
|
edges={selectedRevisionEdges}
|
||||||
|
{field}
|
||||||
|
side={selectedRevision.data[field.name]}
|
||||||
|
colorClass="text-success"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- </div> -->
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class=" lx-card text-center">
|
||||||
|
<span>Nothing will change</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.label {
|
||||||
|
width: 180px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
vertical-align: inherit;
|
||||||
|
white-space: normal;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background-color: #eee;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
<script>
|
||||||
|
import {afterUpdate, createEventDispatcher, 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 ErrorAlert from "../common/ErrorAlert.svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
export let schema;
|
||||||
|
export let schemas;
|
||||||
|
export let record;
|
||||||
|
export let graph = {
|
||||||
|
records: [],
|
||||||
|
edges: []
|
||||||
|
};
|
||||||
|
export let isCreateMode;
|
||||||
|
let originalContent;
|
||||||
|
let activeContentTab = "_default";
|
||||||
|
let hasUnsavedData = false;
|
||||||
|
$: validationErrors = null;
|
||||||
|
$: errorMessage = validationErrors
|
||||||
|
? `Record submission failed. ${
|
||||||
|
Object.entries(validationErrors).length
|
||||||
|
} error(s)`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let activeFields = schema.fields.filter(
|
||||||
|
(f) => f.trashed === false && f.name !== "id"
|
||||||
|
);
|
||||||
|
|
||||||
|
let tabname = "_default";
|
||||||
|
let fieldToTabs = schema.fields.reduce((c, f) => {
|
||||||
|
if (f.ui === "tab") {
|
||||||
|
tabname = f.name;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
c[tabname] = [...(c[tabname] ?? []), f.name];
|
||||||
|
return c;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setOriginalContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
function setOriginalContent() {
|
||||||
|
originalContent = {
|
||||||
|
data: JSON.parse(JSON.stringify(record.data)),
|
||||||
|
_sys: JSON.parse(JSON.stringify(record._sys)),
|
||||||
|
_file: JSON.parse(JSON.stringify(record._file)),
|
||||||
|
edges: JSON.parse(JSON.stringify(graph.edges)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
hasUnsavedData = checkUnsavedData();
|
||||||
|
});
|
||||||
|
|
||||||
|
function beforeUnload(e) {
|
||||||
|
// Cancel the event as stated by the standard.
|
||||||
|
// e.preventDefault();
|
||||||
|
// console.log(hasUnsavedData);
|
||||||
|
if (hasUnsavedData) {
|
||||||
|
return (e.returnValue =
|
||||||
|
"You have unsaved changes. Are you sure you want to exit?");
|
||||||
|
}
|
||||||
|
// Chrome requires returnValue to be set.
|
||||||
|
// e.returnValue = "";
|
||||||
|
delete e["returnValue"];
|
||||||
|
// more compatibility
|
||||||
|
// return true;
|
||||||
|
return "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkUnsavedData() {
|
||||||
|
if (isCreateMode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !isEqual(originalContent, {
|
||||||
|
data: record.data,
|
||||||
|
_sys: record._sys,
|
||||||
|
_file: record._file,
|
||||||
|
edges: graph.edges,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dispatch("cancel");
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
console.log("SAVE: Attempt");
|
||||||
|
validationErrors = null;
|
||||||
|
errorMessage = "";
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (!hasUnsavedData && !isCreateMode) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!record) {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// remove trashed edges
|
||||||
|
graph.edges = graph.edges?.filter((edge) => !edge._isTrashed && edge.source === record.id) ?? [];
|
||||||
|
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post("/records", {
|
||||||
|
record: record,
|
||||||
|
edges: graph.edges,
|
||||||
|
isCreateMode: isCreateMode,
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
console.log("SAVE: SAVED INLINE");
|
||||||
|
|
||||||
|
record = response.data.records[0];
|
||||||
|
graph = response.data;
|
||||||
|
if (!isCreateMode) {
|
||||||
|
setOriginalContent();
|
||||||
|
}
|
||||||
|
dispatch("inlinesaved", {
|
||||||
|
records: [record],
|
||||||
|
});
|
||||||
|
resolve(null);
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
// setOriginalContent();
|
||||||
|
if (error.response) {
|
||||||
|
if (typeof error.response.data.error === "string") {
|
||||||
|
errorMessage = error.response.data.error;
|
||||||
|
} else {
|
||||||
|
validationErrors = error.response.data.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(null);
|
||||||
|
// msgSuccess = null;
|
||||||
|
// msgError = error.response.data.error;
|
||||||
|
// submitted = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:beforeunload={beforeUnload}/>
|
||||||
|
|
||||||
|
<div class="inline-edit my-4">
|
||||||
|
<ErrorAlert message={errorMessage}/>
|
||||||
|
|
||||||
|
<div class=" mt-1">
|
||||||
|
<ContentTabs
|
||||||
|
{schema}
|
||||||
|
{isCreateMode}
|
||||||
|
bind:active={activeContentTab}
|
||||||
|
{record}
|
||||||
|
/>
|
||||||
|
<FilePreview {record} {schema}/>
|
||||||
|
<!-- <fieldset disabled="disabled"> -->
|
||||||
|
{#each activeFields as field (field.name)}
|
||||||
|
{#if fieldToTabs[activeContentTab].includes(field.name)}
|
||||||
|
<FormField
|
||||||
|
bind:data={record.data}
|
||||||
|
bind:graph={graph}
|
||||||
|
{field}
|
||||||
|
{schema}
|
||||||
|
{schemas}
|
||||||
|
{record}
|
||||||
|
{validationErrors}
|
||||||
|
{isCreateMode}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<!-- </fieldset> -->
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="d-flex mt-3 align-items-center justify-content-center">
|
||||||
|
{#if schema.hasDrafts}
|
||||||
|
<StatusSelect bind:status={record._sys.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>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
import PreviewCardSmall from "./PreviewCardSmall.svelte";
|
||||||
|
|
||||||
|
export let managerRecords;
|
||||||
|
|
||||||
|
export let graph;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if managerRecords.length > 0}
|
||||||
|
<div
|
||||||
|
class="record-history d-flex justify-content-center align-items-center w-100 mb-4 mt-4"
|
||||||
|
>
|
||||||
|
{#each managerRecords.reverse() as arecord, i}
|
||||||
|
{#if i !== 0}
|
||||||
|
<Icon icon="angle-right"/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mx-3 p-0 my-0">
|
||||||
|
<PreviewCardSmall record={arecord} {graph}/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.record-history {
|
||||||
|
/* background-color: #fff; */
|
||||||
|
padding: 15px 10px;
|
||||||
|
border-radius: 32px;
|
||||||
|
line-height: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="wrapper-normal ">
|
||||||
|
<div class="header-normal">
|
||||||
|
Record Not Found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import Mustache from "mustache";
|
||||||
|
import {stripHtml} from "../../helpers";
|
||||||
|
|
||||||
|
export function previewTitle(schemas, record, graph) {
|
||||||
|
let schema = schemas.find((aschema) => aschema.name === record?._sys.schema);
|
||||||
|
|
||||||
|
if (!schema?.titleTemplate) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (!render || render === "") {
|
||||||
|
return noTemplate(schema, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stripHtml(render.slice(0, 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
function noTemplate(schema, record) {
|
||||||
|
if (schema?.type === "files") {
|
||||||
|
return record._file.path;
|
||||||
|
}
|
||||||
|
return stripHtml(
|
||||||
|
record?.data[schema.fields.filter((f) => f.info.name === "text")[0]?.name]
|
||||||
|
).slice(0, 300);
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<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._sys.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._sys.status === "draft"}
|
||||||
|
<Status status={record._sys.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>
|
||||||
|
.button-file {
|
||||||
|
width: 64px;
|
||||||
|
height: 65px;
|
||||||
|
}
|
||||||
|
.card .trash-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.card:hover .trash-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.title-link {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "../common/Icon.svelte";
|
||||||
|
|
||||||
|
|
||||||
|
import {createEventDispatcher, onMount} 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 dispatch = createEventDispatcher();
|
||||||
|
export let isFirst;
|
||||||
|
export let isLast;
|
||||||
|
export let toDelete = false;
|
||||||
|
export let schemas;
|
||||||
|
export let record;
|
||||||
|
let editRecord;
|
||||||
|
let schema = schemas.find((aschema) => aschema.name === record._sys.schema);
|
||||||
|
$: editMode = false;
|
||||||
|
$: expanded = false;
|
||||||
|
|
||||||
|
function editInline(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
axios
|
||||||
|
.get("/records/editInline/" + record.id)
|
||||||
|
.then((response) => {
|
||||||
|
record = response.data;
|
||||||
|
editRecord = response.data;
|
||||||
|
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("/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}
|
||||||
|
{schemas}
|
||||||
|
record={editRecord}
|
||||||
|
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} {schemas} {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>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script>
|
||||||
|
import {previewTitle} from "./Preview";
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let record;
|
||||||
|
export let graph;
|
||||||
|
$: schema = channel.schemas.find((aschema) => aschema.name === record._sys.schema);
|
||||||
|
|
||||||
|
$: title = previewTitle(channel.schemas, record, graph);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#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;"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
opacity: 0.5;
|
||||||
|
/* color: #fff; */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<script>
|
||||||
|
import { getStatus } from "./StatusText";
|
||||||
|
export let status;
|
||||||
|
|
||||||
|
let statusObj = getStatus(status);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="badge text-bg-{statusObj.bg}" style="max-width:84px"
|
||||||
|
>{statusObj.text}</span
|
||||||
|
>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script>
|
||||||
|
import Status from "./Status.svelte";
|
||||||
|
import {getStatus, getStatusList} from "./StatusText";
|
||||||
|
|
||||||
|
export let status;
|
||||||
|
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="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>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export function getStatus(status) {
|
||||||
|
const statusList = getStatusList();
|
||||||
|
|
||||||
|
return statusList[status];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStatusList(){
|
||||||
|
return {
|
||||||
|
published: {
|
||||||
|
value: "published",
|
||||||
|
text: "Published",
|
||||||
|
bg: "success",
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
|
trashed: {
|
||||||
|
value: "trashed",
|
||||||
|
text: "Trashed",
|
||||||
|
bg: "danger",
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
|
draft: {
|
||||||
|
value: "draft",
|
||||||
|
text: "Draft",
|
||||||
|
bg: "warning",
|
||||||
|
color: "dark",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<script>
|
||||||
|
import Mustache from "mustache";
|
||||||
|
import { imgurl } from "../files/imageserver";
|
||||||
|
import { uniqBy } from "lodash";
|
||||||
|
import { onMount, getContext } from "svelte";
|
||||||
|
import { Network } from "vis-network/esnext";
|
||||||
|
import "vis-network/styles/vis-network.css";
|
||||||
|
|
||||||
|
const channelurl = getContext("channelurl");
|
||||||
|
export let schemas;
|
||||||
|
export let recordGraph;
|
||||||
|
let network;
|
||||||
|
let allEdges = recordGraph._graph.edges.map((e) => {
|
||||||
|
e.fieldLabel = schemas
|
||||||
|
.find((s) => s.uid === e.fromSchema)
|
||||||
|
.fields.find((f) => f.name === e.field).label;
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
|
||||||
|
let nodes = recordGraph._graph.nodes.map((r) => {
|
||||||
|
let nodeOptions = {
|
||||||
|
id: r.data.id,
|
||||||
|
label: renderTitle(
|
||||||
|
schemas.find((s) => s.uid === r._sys.schema),
|
||||||
|
r
|
||||||
|
),
|
||||||
|
borderWidth: 0,
|
||||||
|
color: {
|
||||||
|
background:
|
||||||
|
r.data.id === recordGraph.data.id ? "#0b5d1e" : "#eeeeee",
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
multi: true,
|
||||||
|
color: r.data.id === recordGraph.data.id ? "#fff" : "#333",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
nodeOptions.shape = "box";
|
||||||
|
if (r._file?.path) {
|
||||||
|
nodeOptions.shape = "image";
|
||||||
|
nodeOptions.image = imgurl(r, 64, 64, "crop");
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeOptions;
|
||||||
|
});
|
||||||
|
|
||||||
|
nodes = uniqBy(nodes, (n) => n.id);
|
||||||
|
|
||||||
|
// create an array with edges
|
||||||
|
let networkEdges = allEdges.map((e) => {
|
||||||
|
return {
|
||||||
|
from: e.from,
|
||||||
|
to: e.to,
|
||||||
|
label: e.fieldLabel,
|
||||||
|
arrows: {
|
||||||
|
to: {
|
||||||
|
enabled: true,
|
||||||
|
type: "arrow",
|
||||||
|
scaleFactor: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
font: { align: "middle" },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
nodes: nodes,
|
||||||
|
edges: networkEdges,
|
||||||
|
};
|
||||||
|
let options = {
|
||||||
|
physics: false,
|
||||||
|
layout: {
|
||||||
|
hierarchical: {
|
||||||
|
enabled: true,
|
||||||
|
nodeSpacing: 150,
|
||||||
|
treeSpacing: 200,
|
||||||
|
direction: "UD",
|
||||||
|
sortMethod: "directed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
let networkInstance = new Network(network, data, options);
|
||||||
|
|
||||||
|
networkInstance.on("doubleClick", function (params) {
|
||||||
|
if (params.nodes[0]) {
|
||||||
|
window.location = channelurl + "/records/" + params.nodes[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderTitle(schema, record) {
|
||||||
|
const template = `${schema.titleTemplate}\n <i>${schema.label}</i>`;
|
||||||
|
Mustache.parse(template);
|
||||||
|
|
||||||
|
let recordData = { ...record, ...record.data };
|
||||||
|
|
||||||
|
return Mustache.render(template, recordData);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="lx-card">
|
||||||
|
<div class="network-container" bind:this={network} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.network-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 80vh;
|
||||||
|
max-height: 800px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import BlockButtons from "./BlockButtons.svelte";
|
||||||
|
import BlockElements from "./BlockElements.svelte";
|
||||||
|
import {flip} from "svelte/animate";
|
||||||
|
import {quintOut} from 'svelte/easing';
|
||||||
|
|
||||||
|
export let record;
|
||||||
|
export let field;
|
||||||
|
export let value = [];
|
||||||
|
export let schemas;
|
||||||
|
export let graph;
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="inline-card-wrapper">
|
||||||
|
<BlockButtons
|
||||||
|
bind:blockData={value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#each value as blockItemData (blockItemData.id)}
|
||||||
|
<div class="block-field-wrapper" animate:flip="{{delay: 250, duration: 250, easing: quintOut}}">
|
||||||
|
<BlockElements
|
||||||
|
bind:block={blockItemData}
|
||||||
|
{record}
|
||||||
|
{field}
|
||||||
|
{schemas}
|
||||||
|
bind:graph
|
||||||
|
/>
|
||||||
|
<BlockButtons
|
||||||
|
bind:blockData={value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script>
|
||||||
|
import Icon from "../../common/Icon.svelte";
|
||||||
|
import {randomId} from "../../../helpers";
|
||||||
|
|
||||||
|
export let blockId;
|
||||||
|
export let blockData;
|
||||||
|
$: showOptions = false;
|
||||||
|
let validUis = ["text", "textarea", "rich", "reference"];
|
||||||
|
|
||||||
|
function createBlock(e, validUI) {
|
||||||
|
e.preventDefault();
|
||||||
|
blockData = [...blockData, {
|
||||||
|
ui: validUI,
|
||||||
|
id: randomId(),
|
||||||
|
key: "",
|
||||||
|
value: null
|
||||||
|
}];
|
||||||
|
showOptions = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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="bg-light lx-card d-flex">
|
||||||
|
{#each validUis as validUi}
|
||||||
|
|
||||||
|
<div class="me-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
on:click={(e) => createBlock(e, validUi)}
|
||||||
|
>{validUi}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<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: 0 5px; */
|
||||||
|
display: inline-block;
|
||||||
|
z-index: 1;
|
||||||
|
margin: 10px auto 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script>
|
||||||
|
import Text from "./elements/Text.svelte";
|
||||||
|
import Textarea from "./elements/Textarea.svelte";
|
||||||
|
import Rich from "./elements/Rich.svelte";
|
||||||
|
import Reference from "./elements/Reference.svelte";
|
||||||
|
|
||||||
|
export let record;
|
||||||
|
export let field;
|
||||||
|
export let schemas;
|
||||||
|
export let graph;
|
||||||
|
|
||||||
|
export let block;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card editor-field bg-light lx-card d-flex">
|
||||||
|
<span class="text-muted d-block fs-6 mb-1">{block.ui}</span>
|
||||||
|
{#if block.ui === "text"}
|
||||||
|
|
||||||
|
<Text
|
||||||
|
bind:block={block}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{:else if block.ui === "textarea"}
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
bind:block={block}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{:else if block.ui === "rich"}
|
||||||
|
<Rich
|
||||||
|
bind:block={block}
|
||||||
|
/>
|
||||||
|
{:else if block.ui === "reference"}
|
||||||
|
<Reference
|
||||||
|
{record}
|
||||||
|
{field}
|
||||||
|
{schemas}
|
||||||
|
bind:graph
|
||||||
|
bind:block={block}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<script>
|
||||||
|
import {uniq, uniqBy} from "lodash";
|
||||||
|
import PreviewCard from "../../PreviewCard.svelte";
|
||||||
|
import {sortByField} from "../../../edges/sortEdges";
|
||||||
|
import ReferenceInlineButtons from "../../elements/ReferenceInlineButtons.svelte"
|
||||||
|
import Sortable from "../../../libs/Sortable.svelte";
|
||||||
|
|
||||||
|
export let block;
|
||||||
|
export let record;
|
||||||
|
export let field;
|
||||||
|
export let schemas;
|
||||||
|
export let graph;
|
||||||
|
|
||||||
|
$: references = graph.edges
|
||||||
|
.filter((edge) => edge.field === field.name && block.value?.includes(edge.target))
|
||||||
|
.map((edge) => {
|
||||||
|
return graph.records.find((increc) => increc.data.id === edge.target && record.data.id === edge.source);
|
||||||
|
}).filter((rec) => (rec?.data?.id ? true : false)) ?? [];
|
||||||
|
|
||||||
|
let collections = schemas.filter((aschema) =>
|
||||||
|
field.collections.includes(aschema.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
function removeReference(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
graph.edges = graph.edges.filter(
|
||||||
|
(edge) => !(edge.target === e.detail && edge.field === field.name)
|
||||||
|
);
|
||||||
|
block.value = graph.edges.filter(edge => edge.field === field.name && block.value?.includes(edge.target)).map((edge) => edge.target) ?? [];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function reorder(e) {
|
||||||
|
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, field.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insert(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const recordsToInsert = e.detail.records;
|
||||||
|
const action = e.detail.action;
|
||||||
|
let newEdges = recordsToInsert.map((r) => {
|
||||||
|
return {
|
||||||
|
schema: r._sys.schema,
|
||||||
|
target: r.data.id,
|
||||||
|
source: record.data.id,
|
||||||
|
field: field.name,
|
||||||
|
rank: ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let replacedEdges = graph.edges;
|
||||||
|
let newBlockValue = [];
|
||||||
|
if (action === "replace") {
|
||||||
|
newBlockValue = newEdges.map(edge => edge.target)
|
||||||
|
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
|
||||||
|
} else {
|
||||||
|
newBlockValue = [...block.value ?? [], ...newEdges.map(edge => edge.target)]
|
||||||
|
}
|
||||||
|
block.value = uniq(newBlockValue);
|
||||||
|
graph.records = uniqBy([...graph.records, ...recordsToInsert], (r) => r.data.id);
|
||||||
|
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.target + edge.field);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="inline-card-wrapper">
|
||||||
|
<ReferenceInlineButtons
|
||||||
|
{field}
|
||||||
|
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.data.id)}
|
||||||
|
<div class="col mb-3">
|
||||||
|
<PreviewCard
|
||||||
|
classes="h-100"
|
||||||
|
{schemas}
|
||||||
|
record={reference}
|
||||||
|
hasDelete={true}
|
||||||
|
on:remove={removeReference}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Sortable>
|
||||||
|
{/if}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<script>
|
||||||
|
import Tinymce from "../../../libs/Tinymce.svelte";
|
||||||
|
|
||||||
|
export let block;
|
||||||
|
let additionalConfig = {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-0">
|
||||||
|
<Tinymce bind:value={block.value} {additionalConfig}/>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
export let block;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-0">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id={block.id}
|
||||||
|
class="form-control"
|
||||||
|
bind:value={block.value}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script>
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
|
||||||
|
export let block;
|
||||||
|
let thisEl;
|
||||||
|
|
||||||
|
function resize(e) {
|
||||||
|
let el;
|
||||||
|
if (e.target) {
|
||||||
|
el = e.target;
|
||||||
|
} else {
|
||||||
|
el = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.style.overflow = "hidden";
|
||||||
|
el.style.height = "1px";
|
||||||
|
el.style.height = +el.scrollHeight + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
resize(thisEl);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-0">
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
bind:value={block.value}
|
||||||
|
bind:this={thisEl}
|
||||||
|
on:input={resize}
|
||||||
|
id={block.id}
|
||||||
|
class="form-control"
|
||||||
|
autocomplete="off"></textarea>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
textarea {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script>
|
||||||
|
import { getErrorMessage } from "./errorMessage";
|
||||||
|
export let id;
|
||||||
|
export let field;
|
||||||
|
export let value;
|
||||||
|
export let isCreateMode;
|
||||||
|
export let validationErrors;
|
||||||
|
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
class:is-invalid={errorMessage}
|
||||||
|
bind:group={value}
|
||||||
|
id="{id}-1"
|
||||||
|
value={true}
|
||||||
|
disabled={field.readonly && !isCreateMode}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="{id}-1">Yes</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
id="{id}-2"
|
||||||
|
class:is-invalid={errorMessage}
|
||||||
|
bind:group={value}
|
||||||
|
value={false}
|
||||||
|
disabled={field.readonly && !isCreateMode}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="{id}-2">No</label>
|
||||||
|
</div>
|
||||||
|
{#if field.nullable}
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
class:is-invalid={errorMessage}
|
||||||
|
id="{id}-3"
|
||||||
|
type="radio"
|
||||||
|
bind:group={value}
|
||||||
|
value={null}
|
||||||
|
disabled={field.readonly && !isCreateMode}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="{id}-3">Don't Know</label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script>
|
||||||
|
import { getErrorMessage } from "./errorMessage";
|
||||||
|
export let field;
|
||||||
|
export let value;
|
||||||
|
export let isCreateMode;
|
||||||
|
export let validationErrors;
|
||||||
|
export let id;
|
||||||
|
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-0">
|
||||||
|
<div class="input-group ">
|
||||||
|
<div style="width:64px;">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
{id}
|
||||||
|
class="form-control form-control-color"
|
||||||
|
disabled={field.readonly && !isCreateMode}
|
||||||
|
bind:value
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class:is-invalid={errorMessage}
|
||||||
|
{id}
|
||||||
|
class="form-control"
|
||||||
|
bind:value
|
||||||
|
readonly={field.readonly && !isCreateMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<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)}>
|
||||||
|
<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 = "";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="dropdown-item">
|
||||||
|
Add "{search}"
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
No results
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<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 = {
|
||||||
|
enableTime: false,
|
||||||
|
allowInput: true,
|
||||||
|
dateFormat: "Y-m-d",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (field.min) {
|
||||||
|
flatpickrOptions.minDate = field.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.max) {
|
||||||
|
flatpickrOptions.maxDate = field.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!field.readonly || isCreateMode) {
|
||||||
|
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"
|
||||||
|
aria-expanded="false"
|
||||||
|
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
|
||||||
|
type="text"
|
||||||
|
{id}
|
||||||
|
class="form-control"
|
||||||
|
class:is-invalid={errorMessage}
|
||||||
|
bind:value
|
||||||
|
bind:this={pickerInput}
|
||||||
|
autocomplete="off"
|
||||||
|
readonly={field.readonly && !isCreateMode}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<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 schema;
|
||||||
|
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 pickerInput;
|
||||||
|
let pickerInstance;
|
||||||
|
let flatpickrOptions = {
|
||||||
|
enableTime: false,
|
||||||
|
allowInput: true,
|
||||||
|
altInput: true,
|
||||||
|
altFormat: "Y-m-d H:i:S",
|
||||||
|
dateFormat: "Z",
|
||||||
|
enableTime: true,
|
||||||
|
time_24hr: true,
|
||||||
|
enableSeconds: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (field.min) {
|
||||||
|
flatpickrOptions.minDate = field.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.max) {
|
||||||
|
flatpickrOptions.maxDate = field.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!field.readonly || isCreateMode) {
|
||||||
|
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"
|
||||||
|
aria-expanded="false"
|
||||||
|
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
|
||||||
|
type="text"
|
||||||
|
{id}
|
||||||
|
class="form-control"
|
||||||
|
class:is-invalid={errorMessage}
|
||||||
|
bind:value
|
||||||
|
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
|
||||||
|
>
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script>
|
||||||
|
import {getContext} from "svelte";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let field;
|
||||||
|
export let schema;
|
||||||
|
export let id;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-1">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<label for={id} class="form-label"
|
||||||
|
>{field.label}</label
|
||||||
|
>
|
||||||
|
{#if field.help}
|
||||||
|
<small class=" text-primary opacity-50">{field.help}</small>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
tabindex="-1"
|
||||||
|
class="text-decoration-none"
|
||||||
|
href="{channel.lucentUrl}/schemas/{schema.name}/fields/edit/{field.name}"
|
||||||
|
><code class="text-primary opacity-50">{field.name}</code>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<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";
|
||||||
|
|
||||||
|
const channel = getContext("channel");
|
||||||
|
export let field;
|
||||||
|
export let record;
|
||||||
|
export let graph
|
||||||
|
|
||||||
|
let browseModal;
|
||||||
|
|
||||||
|
$: references = graph?.edges
|
||||||
|
.filter((edge) => edge.field === field.name)
|
||||||
|
.map((edge) => {
|
||||||
|
return graph.records.find((increc) => increc.id == edge.target && record.id == edge.source);
|
||||||
|
}).filter((rec) => (rec?.id ? true : false)) ?? [];
|
||||||
|
|
||||||
|
let collections = channel.schemas.filter((aschema) =>
|
||||||
|
field.collections.includes(aschema.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
function removeReference(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
graph.edges = graph.edges.filter(
|
||||||
|
(edge) => !(edge.target === e.detail && edge.field === field.name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBrowseModal(e, schema) {
|
||||||
|
e.preventDefault();
|
||||||
|
browseModal.open(schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reorder(e) {
|
||||||
|
graph.edges = await sortByField(e.detail.from, e.detail.to, graph.edges, field.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
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._sys.schema,
|
||||||
|
targetSchema: r._sys.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);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-0">
|
||||||
|
{#if field.collections.length === 1}
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary"
|
||||||
|
on:click={(e) => openBrowseModal(e, collections[0].name)}
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<div class="dropdown d-inline-block">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary btn-sm"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{#each collections as collection}
|
||||||
|
<li>
|
||||||
|
<!-- {`${channelurl}/content/${collection.name}?parent=${record.id}&parentfield=${field.name}`} -->
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
on:click={(e) =>
|
||||||
|
openBrowseModal(e, collection.name)}
|
||||||
|
href="/">{collection.label}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if references.length > 0}
|
||||||
|
<Sortable sortableClass="row row-cols-3 mt-3" on:update={reorder}>
|
||||||
|
{#each references as reference (reference.id)}
|
||||||
|
<div class="col mb-3">
|
||||||
|
<PreviewCard
|
||||||
|
classes="h-100"
|
||||||
|
record={reference}
|
||||||
|
hasDelete={true}
|
||||||
|
on:remove={removeReference}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Sortable>
|
||||||
|
{/if}
|
||||||
|
<BrowseModal bind:this={browseModal} on:insert={insert}/>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script>
|
||||||
|
import Codemirror from "../../libs/Codemirror.svelte";
|
||||||
|
import { getErrorMessage } from "./errorMessage";
|
||||||
|
|
||||||
|
|
||||||
|
export let value;
|
||||||
|
export let field;
|
||||||
|
export let isCreateMode;
|
||||||
|
// export let id;
|
||||||
|
export let validationErrors;
|
||||||
|
$: errorMessage = getErrorMessage(validationErrors, field.name);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
|
||||||
|
<Codemirror bind:value editable={!field.readonly || isCreateMode} />
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<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;
|
||||||
|
|
||||||
|
function fixDecimals(e) {
|
||||||
|
const number = e.currentTarget.value;
|
||||||
|
const formattedNumber = formatNumber(number);
|
||||||
|
value = isNaN(formattedNumber) ? null : formattedNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(number) {
|
||||||
|
return parseFloat(number).toFixed(field.decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: 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"
|
||||||
|
aria-expanded="false"
|
||||||
|
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
|
||||||
|
type="number"
|
||||||
|
{id}
|
||||||
|
class="form-control"
|
||||||
|
class:is-invalid={errorMessage}
|
||||||
|
on:change={fixDecimals}
|
||||||
|
bind:value
|
||||||
|
autocomplete="off"
|
||||||
|
readonly={field.readonly && !isCreateMode}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if errorMessage}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user