This commit is contained in:
2023-10-02 23:10:49 +03:00
commit c6cb488379
255 changed files with 18731 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
front/node_modules
/vendor
.env
.env.backup
.phpunit.result.cache
front/npm-debug.log
/.idea
/.vscode
+38
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+14
View File
@@ -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"
}
}
+70
View File
@@ -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'],
// });
+17
View File
@@ -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);
}
+63
View File
@@ -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);
+25
View File
@@ -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}/>
+34
View File
@@ -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}/>
+66
View File
@@ -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>
+16
View File
@@ -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>
+29
View File
@@ -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}
+10
View File
@@ -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>
+46
View File
@@ -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>
+46
View File
@@ -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>
+47
View File
@@ -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>
+81
View File
@@ -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>
+92
View File
@@ -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>
+43
View File
@@ -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>
+44
View File
@@ -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>
`;
}
+6
View File
@@ -0,0 +1,6 @@
export function usernameById(users, id) {
if (users) {
return users.find((u) => u.id === id)?.name ?? id;
}
return id;
}
+25
View File
@@ -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}
+9
View File
@@ -0,0 +1,9 @@
<script>
export let message = "";
</script>
{#if message}
<div class="alert alert-danger" role="alert">
{message}
</div>
{/if}
+136
View File
@@ -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>
+152
View File
@@ -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>
+58
View File
@@ -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}
/>
+141
View File
@@ -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>
+115
View File
@@ -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>
+15
View File
@@ -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];
}
+68
View File
@@ -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}
+67
View File
@@ -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>
+31
View File
@@ -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}`;
}
+20
View File
@@ -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>
+23
View File
@@ -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>
+38
View File
@@ -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"
/>
+26
View File
@@ -0,0 +1,26 @@
<script>
import { uniqueId } from "lodash";
export let label;
export let name;
export let group;
export let value;
export let help;
let id = uniqueId();
</script>
<div class="form-check">
<input
class="form-check-input"
type="radio"
{value}
{name}
bind:group
{id}
/>
<label class="form-check-label" for={id}>
{label}
</label>
{#if help}
<span class="text-muted">{help}</span>
{/if}
</div>
+44
View File
@@ -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>
+50
View File
@@ -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>
+54
View File
@@ -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} />
+58
View File
@@ -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}
+124
View File
@@ -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>
+129
View File
@@ -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}
+237
View File
@@ -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>
+85
View File
@@ -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>
+109
View File
@@ -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>
+126
View File
@@ -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>
+240
View File
@@ -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>
+225
View File
@@ -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>
+34
View File
@@ -0,0 +1,34 @@
<script>
import Icon from "../common/Icon.svelte";
import PreviewCardSmall from "./PreviewCardSmall.svelte";
export let managerRecords;
export let graph;
</script>
{#if managerRecords.length > 0}
<div
class="record-history d-flex justify-content-center align-items-center w-100 mb-4 mt-4"
>
{#each managerRecords.reverse() as arecord, i}
{#if i !== 0}
<Icon icon="angle-right"/>
{/if}
<div class="mx-3 p-0 my-0">
<PreviewCardSmall record={arecord} {graph}/>
</div>
{/each}
</div>
{/if}
<style>
.record-history {
/* background-color: #fff; */
padding: 15px 10px;
border-radius: 32px;
line-height: 12px;
}
</style>
+9
View File
@@ -0,0 +1,9 @@
<script>
</script>
<div class="wrapper-normal ">
<div class="header-normal">
Record Not Found
</div>
</div>
+44
View File
@@ -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>
+10
View File
@@ -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>
+28
View File
@@ -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",
},
};
}
+113
View File
@@ -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