removed lodash and axios

This commit is contained in:
2026-05-07 22:50:02 +03:00
parent daa4b268a6
commit a04cdd753d
24 changed files with 2191 additions and 4844 deletions
+71
View File
@@ -83,3 +83,74 @@ export function apiGet(url, options = {}) {
}, },
}).then((r) => r.json()); }).then((r) => r.json());
} }
export function isEqual(db, ed) {
let isObject = (x) =>
typeof x === "object" && !Array.isArray(x) && x !== null;
let isArray = (x) => x?.constructor === Array;
let isEmpty = (x) => x === null || x === undefined || x == [];
const db_value = db ?? null;
const ed_value = ed ?? null;
if (isObject(db_value)) {
let keys = Object.keys(db_value);
return keys.reduce((acc, k) => {
if (acc === false) {
return false;
}
return isEqual(db_value?.[k], ed_value?.[k]);
}, true);
}
if (isArray(db_value)) {
return db_value.reduce((c, v, i) => {
if (c === false) {
return false;
}
return isEqual(v, ed_value?.[i]);
}, true);
}
if (isEmpty(db_value) && isEmpty(ed_value)) {
return true;
}
if (db_value == ed_value) {
return true;
}
return false;
// const ok = Object.keys,
// tx = typeof x,
// ty = typeof y;
// return x && y && tx === "object" && tx === ty
// ? ok(x).length === ok(y).length &&
// ok(x).every((key) => isEqual(x[key], y[key]))
// : x === y;
}
export function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
export function arrayUnique(array) {
return array.filter((value, index) => array.indexOf(value) === index);
}
export function arrayUniqueBy(items, uniqueBy) {
const ids = new Set(items.map((item) => item[uniqueBy]));
return [...ids].map((id) => items.find((i) => i[uniqueBy] === id));
}
export function arrayUniqueCb(items, aFilter) {
const cache = new Set();
return items.filter((item) => {
const cacheValue = aFilter(item);
if (cache.has(cacheValue)) return false;
cache.add(cacheValue);
return true;
});
}
-2
View File
@@ -25,10 +25,8 @@
// export let layout; // export let layout;
export let channel; export let channel;
export let axios;
export let readableSchemas; export let readableSchemas;
setContext("axios", axios);
setContext("channel", channel); setContext("channel", channel);
setContext( setContext(
"readableSchemas", "readableSchemas",
+28 -28
View File
@@ -2,8 +2,9 @@
import ErrorAlert from "../common/ErrorAlert.svelte"; import ErrorAlert from "../common/ErrorAlert.svelte";
import SpinnerButton from "../common/SpinnerButton.svelte"; import SpinnerButton from "../common/SpinnerButton.svelte";
import Avatar from "./Avatar.svelte"; import Avatar from "./Avatar.svelte";
import {getContext} from "svelte"; import { getContext } from "svelte";
import SuccessAlert from "../common/SuccessAlert.svelte"; import SuccessAlert from "../common/SuccessAlert.svelte";
import { apiPost } from "../../helpers";
const user = getContext("user"); const user = getContext("user");
const channel = getContext("channel"); const channel = getContext("channel");
@@ -16,16 +17,15 @@
e.preventDefault(); e.preventDefault();
errorMessage = ""; errorMessage = "";
axios apiPost(channel.lucentUrl + "/account/update-name", {
.post(channel.lucentUrl + "/account/update-name", { name: name,
name: name, })
})
.then((response) => { .then((response) => {
successAlert.show(); successAlert.show();
}) })
.catch((error) => { .catch((error) => {
errorMessage = error.response?.data.error; errorMessage = error.response?.data.error;
console.log({errorMessage}); console.log({ errorMessage });
}); });
} }
@@ -33,55 +33,55 @@
e.preventDefault(); e.preventDefault();
errorMessage = ""; errorMessage = "";
axios apiPost(channel.lucentUrl + "/account/update-email", {
.post(channel.lucentUrl + "/account/update-email", { email: email,
email: email, })
})
.then((response) => { .then((response) => {
successAlert.show(); successAlert.show();
}) })
.catch((error) => { .catch((error) => {
errorMessage = error.response?.data.error; errorMessage = error.response?.data.error;
console.log({errorMessage}); console.log({ errorMessage });
}); });
} }
</script> </script>
<div class="wrapper-tiny"> <div class="wrapper-tiny">
<ErrorAlert message={errorMessage}/> <ErrorAlert message={errorMessage} />
<SuccessAlert bind:this={successAlert} /> <SuccessAlert bind:this={successAlert} />
<h3 class="header-small mb-5"> <h3 class="header-small mb-5">
<Avatar name={user.name}/> <Avatar name={user.name} />
</h3> </h3>
<form on:submit={saveName}> <form on:submit={saveName}>
<div class="input-group mb-5"> <div class="input-group mb-5">
<input <input
type="text" type="text"
bind:value={name} bind:value={name}
class="form-control mb-3" class="form-control mb-3"
placeholder="Name" placeholder="Name"
required required
/> />
<SpinnerButton label="Update Name"/> <SpinnerButton label="Update Name" />
</div> </div>
</form> </form>
<form on:submit={saveEmail}> <form on:submit={saveEmail}>
<div class="input-group mb-5"> <div class="input-group mb-5">
<input <input
type="email" type="email"
bind:value={email} bind:value={email}
class="form-control mb-3" class="form-control mb-3"
placeholder="Email" placeholder="Email"
required required
/> />
<SpinnerButton label="Update Email"/> <SpinnerButton label="Update Email" />
</div> </div>
</form> </form>
<div class="list-group"> <div class="list-group">
<a class="list-group-item list-group-item-action" href="{ channel.lucentUrl }/logout">Logout from this <a
device</a> class="list-group-item list-group-item-action"
href="{channel.lucentUrl}/logout">Logout from this device</a
>
</div> </div>
</div> </div>
+17 -19
View File
@@ -1,7 +1,8 @@
<script> <script>
import { apiPost } from "../../helpers";
import ErrorAlert from "../common/ErrorAlert.svelte"; import ErrorAlert from "../common/ErrorAlert.svelte";
import SpinnerButton from "../common/SpinnerButton.svelte"; import SpinnerButton from "../common/SpinnerButton.svelte";
import {getContext} from "svelte"; import { getContext } from "svelte";
const channel = getContext("channel"); const channel = getContext("channel");
let name = ""; let name = "";
@@ -12,48 +13,45 @@
e.preventDefault(); e.preventDefault();
errorMessage = ""; errorMessage = "";
axios apiPost(channel.lucentUrl + "/register", {
.post(channel.lucentUrl + "/register", { name: name,
name: name, email: email,
email: email, })
})
.then(() => { .then(() => {
window.location = channel.lucentUrl + "/login"; window.location = channel.lucentUrl + "/login";
}) })
.catch((error) => { .catch((error) => {
errorMessage = error.response?.data.error; errorMessage = error.response?.data.error;
console.log({errorMessage}); console.log({ errorMessage });
}); });
} }
</script> </script>
<div class="wrapper-tiny"> <div class="wrapper-tiny">
<ErrorAlert message={errorMessage}/> <ErrorAlert message={errorMessage} />
<form on:submit={register}> <form on:submit={register}>
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Name</label> <label for="name" class="form-label">Name</label>
<input <input
type="text" type="text"
bind:value={name} bind:value={name}
class="form-control" class="form-control"
id="name" id="name"
/> />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="email" class="form-label">Email address</label> <label for="email" class="form-label">Email address</label>
<input <input
type="email" type="email"
bind:value={email} bind:value={email}
class="form-control" class="form-control"
id="email" id="email"
/> />
</div> </div>
<div class="text-center mt-5 d-block"> <div class="text-center mt-5 d-block">
<SpinnerButton label="Register"/> <SpinnerButton label="Register" />
</div> </div>
</form> </form>
</div> </div>
+139
View File
@@ -0,0 +1,139 @@
/* eslint-disable no-undefined,no-param-reassign,no-shadow */
/**
* Throttle execution of a function. Especially useful for rate limiting
* execution of handlers on events like resize and scroll.
*
* @param {number} delay - A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher)
* are most useful.
* @param {Function} callback - A function to be executed after delay milliseconds. The `this` context and all arguments are passed through,
* as-is, to `callback` when the throttled-function is executed.
* @param {object} [options] - An object to configure options.
* @param {boolean} [options.noTrailing] - Optional, defaults to false. If noTrailing is true, callback will only execute every `delay` milliseconds
* while the throttled-function is being called. If noTrailing is false or unspecified, callback will be executed
* one final time after the last throttled-function call. (After the throttled-function has not been called for
* `delay` milliseconds, the internal counter is reset).
* @param {boolean} [options.noLeading] - Optional, defaults to false. If noLeading is false, the first throttled-function call will execute callback
* immediately. If noLeading is true, the first the callback execution will be skipped. It should be noted that
* callback will never executed if both noLeading = true and noTrailing = true.
* @param {boolean} [options.debounceMode] - If `debounceMode` is true (at begin), schedule `clear` to execute after `delay` ms. If `debounceMode` is
* false (at end), schedule `callback` to execute after `delay` ms.
*
* @returns {Function} A new, throttled, function.
*/
export function throttle(delay, callback, options) {
const {
noTrailing = false,
noLeading = false,
debounceMode = undefined,
} = options || {};
/*
* After wrapper has stopped being called, this timeout ensures that
* `callback` is executed at the proper times in `throttle` and `end`
* debounce modes.
*/
let timeoutID;
let cancelled = false;
// Keep track of the last time `callback` was executed.
let lastExec = 0;
// Function to clear existing timeout
function clearExistingTimeout() {
if (timeoutID) {
clearTimeout(timeoutID);
}
}
// Function to cancel next exec
function cancel(options) {
const { upcomingOnly = false } = options || {};
clearExistingTimeout();
cancelled = !upcomingOnly;
}
/*
* The `wrapper` function encapsulates all of the throttling / debouncing
* functionality and when executed will limit the rate at which `callback`
* is executed.
*/
function wrapper(...arguments_) {
let self = this;
let elapsed = Date.now() - lastExec;
if (cancelled) {
return;
}
// Execute `callback` and update the `lastExec` timestamp.
function exec() {
lastExec = Date.now();
callback.apply(self, arguments_);
}
/*
* If `debounceMode` is true (at begin) this is used to clear the flag
* to allow future `callback` executions.
*/
function clear() {
timeoutID = undefined;
}
if (!noLeading && debounceMode && !timeoutID) {
/*
* Since `wrapper` is being called for the first time and
* `debounceMode` is true (at begin), execute `callback`
* and noLeading != true.
*/
exec();
}
clearExistingTimeout();
if (debounceMode === undefined && elapsed > delay) {
if (noLeading) {
/*
* In throttle mode with noLeading, if `delay` time has
* been exceeded, update `lastExec` and schedule `callback`
* to execute after `delay` ms.
*/
lastExec = Date.now();
if (!noTrailing) {
timeoutID = setTimeout(debounceMode ? clear : exec, delay);
}
} else {
/*
* In throttle mode without noLeading, if `delay` time has been exceeded, execute
* `callback`.
*/
exec();
}
} else if (noTrailing !== true) {
/*
* In trailing throttle mode, since `delay` time has not been
* exceeded, schedule `callback` to execute `delay` ms after most
* recent execution.
*
* If `debounceMode` is true (at begin), schedule `clear` to execute
* after `delay` ms.
*
* If `debounceMode` is false (at end), schedule `callback` to
* execute after `delay` ms.
*/
timeoutID = setTimeout(
debounceMode ? clear : exec,
debounceMode === undefined ? delay - elapsed : delay,
);
}
}
wrapper.cancel = cancel;
// Return the wrapper function.
return wrapper;
}
export function debounce(delay, callback, options) {
const { atBegin = false } = options || {};
return throttle(delay, callback, { debounceMode: atBegin !== false });
}
+26 -28
View File
@@ -1,7 +1,6 @@
<script> <script>
import {getContext, onMount} from "svelte"; import { getContext, onMount } from "svelte";
import axios from "axios"; import { apiPost } from "../../helpers";
const channel = getContext("channel"); const channel = getContext("channel");
export let title; export let title;
export let command; export let command;
@@ -12,59 +11,57 @@
let inProgress = false; let inProgress = false;
function connect() { function connect() {
const eventSource = new EventSource(channel.lucentUrl + "/command-report-source/" + command.signature ); const eventSource = new EventSource(
channel.lucentUrl + "/command-report-source/" + command.signature,
);
eventSource.onmessage = function (event) { eventSource.onmessage = function (event) {
inProgress = true; inProgress = true;
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
date = data.date; date = data.date;
logs = data.logs; logs = data.logs;
anchorEl.scrollIntoView() anchorEl.scrollIntoView();
} };
eventSource.onerror = (e) => { eventSource.onerror = (e) => {
console.log(e) console.log(e);
eventSource.close(); eventSource.close();
inProgress = false; inProgress = false;
} };
} }
function buildWebsite(e) { function buildWebsite(e) {
e.preventDefault(); e.preventDefault();
inProgress = true; inProgress = true;
axios.post(channel.lucentUrl + "/command/" + command.signature).then(response => { apiPost(channel.lucentUrl + "/command/" + command.signature).then(
connect() (response) => {
}) connect();
},
);
} }
onMount(() => { onMount(() => {
connect() connect();
}) });
</script> </script>
<div class="common-wrapper"> <div class="common-wrapper">
<div class="lx-card mt-5"> <div class="lx-card mt-5">
<h3 class="header-small mb-5">{title}</h3> <h3 class="header-small mb-5">{title}</h3>
<button on:click={buildWebsite} class="button primary mb-3" disabled={inProgress}>Start <button
on:click={buildWebsite}
class="button primary mb-3"
disabled={inProgress}
>Start
</button> </button>
<div class="mb-3"> <div class="mb-3">
{#if inProgress} {#if inProgress}
<span class="badge text-bg-warning"> <span class="badge text-bg-warning"> Action in progress </span>
Action in progress
</span>
{/if} {/if}
{#if !inProgress && logs} {#if !inProgress && logs}
<span class="badge text-bg-info"> <span class="badge text-bg-info"> Action completed </span>
Action completed
</span>
{/if} {/if}
</div> </div>
<pre class="logs">{logs} <pre class="logs">{logs}
@@ -72,12 +69,13 @@
</pre> </pre>
</div> </div>
</div> </div>
<style> <style>
.logs{ .logs {
max-height: 70vh; max-height: 70vh;
overflow: scroll; overflow: scroll;
background: var(--p90); background: var(--p90);
color: var(--p10); color: var(--p10);
padding: 10px; padding: 10px;
} }
</style> </style>
@@ -1,7 +1,7 @@
<script> <script>
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { range } from "lodash";
import NavItem from "./NavItem.svelte"; import NavItem from "./NavItem.svelte";
export let inModal; export let inModal;
export let modalUrl; export let modalUrl;
@@ -11,7 +11,11 @@
$: totalPages = Math.ceil(total / limit); $: totalPages = Math.ceil(total / limit);
$: currentPage = Math.ceil((skip - 1) / limit) + 1; $: currentPage = Math.ceil((skip - 1) / limit) + 1;
const range = (start, end, step = 1) =>
Array.from(
{ length: Math.ceil((end - start) / step) },
(_, i) => start + i * step,
);
$: pageRange = range(currentPage - 3, currentPage + 4).filter((i) => { $: pageRange = range(currentPage - 3, currentPage + 4).filter((i) => {
return i > 0 && i <= totalPages; return i > 0 && i <= totalPages;
}); });
@@ -1,6 +1,6 @@
<script> <script>
import { createEventDispatcher, getContext } from "svelte"; import { createEventDispatcher, getContext } from "svelte";
import { debounce } from "lodash"; import { apiGet, debounce } from "../../../helpers";
const channel = getContext("channel"); const channel = getContext("channel");
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -12,17 +12,16 @@
$: searchOptions = []; $: searchOptions = [];
const updateResults = debounce((e) => { const updateResults = debounce((e) => {
axios apiGet(channel.lucentUrl + "/records/suggestions", {
.get(channel.lucentUrl + "/records/suggestions", { params: {
params: { schema: field.collections[0],
schema: field.collections[0], field: "search",
field: "search", value: search,
value: search, ui: "search",
ui: "search", },
}, })
})
.then((response) => { .then((response) => {
searchOptions = response.data; searchOptions = response;
}) })
.catch((error) => { .catch((error) => {
searchOptions = []; searchOptions = [];
-129
View File
@@ -1,129 +0,0 @@
<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?inline";
import customcss from "./tinymce.css?inline";
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() + customcss.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,
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});
});
export function insertMedia(info){
activeEditor.execCommand('InsertHTML', false, info.html);
}
</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>
-37
View File
@@ -1,37 +0,0 @@
.mce-content-body .img {
max-width: 100%;
height: auto;
}
.mce-content-body{
font-size: 16px;
line-height: 20px;
}
.mce-content-body p{
margin-bottom: 14px;
&:last-child{
margin-bottom: 0;
}
}
.mce-content-body ul {
padding: 0 0 0 16px;
list-style: none outside none;
}
.mce-content-body li::before {
content: "—";
opacity: .5;
font-size: 12px;
padding-right: 6px;
vertical-align: 10%;
}
.mce-content-body li {
list-style: none;
padding: 0;
}
+35 -43
View File
@@ -3,9 +3,8 @@
import SuccessAlert from "../common/SuccessAlert.svelte"; import SuccessAlert from "../common/SuccessAlert.svelte";
import SpinnerButton from "../common/SpinnerButton.svelte"; import SpinnerButton from "../common/SpinnerButton.svelte";
import MemberSettingsCard from "./MemberSettingsCard.svelte"; import MemberSettingsCard from "./MemberSettingsCard.svelte";
import {getContext} from "svelte"; import { getContext } from "svelte";
import axios from "axios"; import { apiPost } from "../../helpers";
const channel = getContext("channel"); const channel = getContext("channel");
export let users; export let users;
@@ -23,15 +22,14 @@
function invite(newName, newEmail, newRole) { function invite(newName, newEmail, newRole) {
errorMessage = ""; errorMessage = "";
axios apiPost(channel.lucentUrl + "/members/invite", {
.post(channel.lucentUrl + "/members/invite", { name: newName,
name: newName, email: newEmail,
email: newEmail, roles: [newRole],
roles: [newRole], })
})
.then((response) => { .then((response) => {
successAlert.show("User was invited"); successAlert.show("User was invited");
users = [...users, response.data.user]; users = [...users, response.user];
name = null; name = null;
email = null; email = null;
role = null; role = null;
@@ -45,14 +43,13 @@
e.preventDefault(); e.preventDefault();
errorMessage = ""; errorMessage = "";
axios apiPost(channel.lucentUrl + "/members/update", {
.post(channel.lucentUrl + "/members/update", { id: e.detail.user,
id: e.detail.user, roles: e.detail.roles,
roles: e.detail.roles, })
})
.then((response) => { .then((response) => {
successAlert.show("Users updated"); successAlert.show("Users updated");
users = response.data.users; users = response.users;
}) })
.catch((error) => { .catch((error) => {
errorMessage = error.response?.data?.error ?? ""; errorMessage = error.response?.data?.error ?? "";
@@ -63,50 +60,45 @@
<div class="common-wrapper"> <div class="common-wrapper">
<div class="lx-card mt-5"> <div class="lx-card mt-5">
<h3 class="header-small mb-5">Invite people</h3> <h3 class="header-small mb-5">Invite people</h3>
<ErrorAlert message={errorMessage}/> <ErrorAlert message={errorMessage} />
<SuccessAlert bind:this={successAlert}/> <SuccessAlert bind:this={successAlert} />
<form on:submit={submitInvite}> <form on:submit={submitInvite}>
<div class="mb-3"> <div class="mb-3">
<label for="inviteeName" class="form-label" <label for="inviteeName" class="form-label">Invitee Name</label>
>Invitee Name</label
>
<input <input
type="text" type="text"
bind:value={name} bind:value={name}
class="form-control" class="form-control"
id="inviteeName" id="inviteeName"
placeholder="Member name" placeholder="Member name"
required required
/> />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="inviteeEmail" class="form-label" <label for="inviteeEmail" class="form-label"
>Invitee Email Address</label >Invitee Email Address</label
> >
<input <input
type="email" type="email"
bind:value={email} bind:value={email}
class="form-control" class="form-control"
id="inviteeEmail" id="inviteeEmail"
placeholder="Member email" placeholder="Member email"
required required
/> />
</div> </div>
<div class="me-3"> <div class="me-3">
<select bind:value={role}> <select bind:value={role}>
{#each channel.roles.filter((r) => r !== "removed") as arole} {#each channel.roles.filter((r) => r !== "removed") as arole}
<option <option value={arole}>{arole}</option>
value={arole}
>{arole}</option>
{/each} {/each}
</select> </select>
</div> </div>
<div class="mt-5 d-block text-center"> <div class="mt-5 d-block text-center">
<SpinnerButton label="Invite"/> <SpinnerButton label="Invite" />
</div> </div>
</form> </form>
</div> </div>
@@ -115,10 +107,10 @@
<h3 class="header-small mb-5 mt-5">Members</h3> <h3 class="header-small mb-5 mt-5">Members</h3>
{#each users as user} {#each users as user}
<MemberSettingsCard <MemberSettingsCard
member={user} member={user}
roles={channel.roles} roles={channel.roles}
on:update={update} on:update={update}
on:reinvite={(e) => invite(e.detail.email, e.detail.role)} on:reinvite={(e) => invite(e.detail.email, e.detail.role)}
/> />
{/each} {/each}
</div> </div>
+8 -10
View File
@@ -1,7 +1,5 @@
<script> <script>
import { afterUpdate, getContext, onMount } from "svelte"; import { afterUpdate, getContext, onMount } from "svelte";
import { isEqual } from "lodash";
import axios from "axios";
import EditHeader from "./header/EditHeader.svelte"; import EditHeader from "./header/EditHeader.svelte";
import ContentTabs from "./header/ContentTabs.svelte"; import ContentTabs from "./header/ContentTabs.svelte";
import FormField from "./FormField.svelte"; import FormField from "./FormField.svelte";
@@ -9,6 +7,7 @@
import Info from "./Info.svelte"; import Info from "./Info.svelte";
import ErrorAlert from "../common/ErrorAlert.svelte"; import ErrorAlert from "../common/ErrorAlert.svelte";
import Title from "./header/Title.svelte"; import Title from "./header/Title.svelte";
import { apiPost, isEqual } from "../../helpers";
const channel = getContext("channel"); const channel = getContext("channel");
@@ -97,12 +96,11 @@
graph.edges = graph.edges?.filter( graph.edges = graph.edges?.filter(
(edge) => !edge._isTrashed && edge.source === record.id, (edge) => !edge._isTrashed && edge.source === record.id,
); );
axios apiPost(channel.lucentUrl + "/records", {
.post(channel.lucentUrl + "/records", { record: record,
record: record, edges: graph.edges,
edges: graph.edges, isCreateMode: isCreateMode,
isCreateMode: isCreateMode, })
})
.then(function (response) { .then(function (response) {
console.log("SAVE: SAVED"); console.log("SAVE: SAVED");
@@ -110,14 +108,14 @@
window.location = window.location =
channel.lucentUrl + "/records/" + record.id; channel.lucentUrl + "/records/" + record.id;
} else { } else {
record = response.data.records[0] ?? null; record = response.records[0] ?? null;
if (!record) { if (!record) {
// means trashed // means trashed
hasUnsavedData = false; hasUnsavedData = false;
window.location = channel.lucentUrl; window.location = channel.lucentUrl;
return; return;
} }
graph = response.data; graph = response;
setOriginalContent(); setOriginalContent();
} }
+2 -2
View File
@@ -1,8 +1,8 @@
<script> <script>
import { friendlyDate } from "../../helpers"; import { friendlyDate, isEqual } from "../../helpers";
import Avatar from "../account/Avatar.svelte"; import Avatar from "../account/Avatar.svelte";
import { usernameById } from "../account/users"; import { usernameById } from "../account/users";
import { isEqual } from "lodash";
import Icon from "../common/Icon.svelte"; import Icon from "../common/Icon.svelte";
import RevisionCell from "./revisions/RevisionCell.svelte"; import RevisionCell from "./revisions/RevisionCell.svelte";
import { getContext } from "svelte"; import { getContext } from "svelte";
+8 -10
View File
@@ -6,14 +6,13 @@
onMount, onMount,
} from "svelte"; } from "svelte";
import { isEqual } from "lodash";
import FormField from "./FormField.svelte"; import FormField from "./FormField.svelte";
import FilePreview from "./FilePreview.svelte"; import FilePreview from "./FilePreview.svelte";
import ContentTabs from "./header/ContentTabs.svelte"; import ContentTabs from "./header/ContentTabs.svelte";
import ErrorAlert from "../common/ErrorAlert.svelte"; import ErrorAlert from "../common/ErrorAlert.svelte";
import EditHeader from "./header/EditHeader.svelte"; import EditHeader from "./header/EditHeader.svelte";
import axios from "axios";
import Title from "./header/Title.svelte"; import Title from "./header/Title.svelte";
import { apiPost, isEqual } from "../../helpers";
const channel = getContext("channel"); const channel = getContext("channel");
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -118,17 +117,16 @@
(edge) => !edge._isTrashed && edge.source === record.id, (edge) => !edge._isTrashed && edge.source === record.id,
) ?? []; ) ?? [];
axios apiPost(channel.lucentUrl + "/records", {
.post(channel.lucentUrl + "/records", { record: record,
record: record, edges: graph.edges,
edges: graph.edges, isCreateMode: isCreateMode,
isCreateMode: isCreateMode, })
})
.then(function (response) { .then(function (response) {
console.log("SAVE: SAVED INLINE"); console.log("SAVE: SAVED INLINE");
record = response.data.records[0]; record = response.records[0];
graph = response.data; graph = response;
if (!isCreateMode) { if (!isCreateMode) {
setOriginalContent(); setOriginalContent();
} }
@@ -1,12 +1,11 @@
<script> <script>
import {getContext} from "svelte"; import { getContext } from "svelte";
import {insertEdges} from "./reference"; import { insertEdges } from "./reference";
import {getErrorMessage} from "./errorMessage"; import { getErrorMessage } from "./errorMessage";
import {sortByField} from "../../edges/sortEdges"; import { sortByField } from "../../edges/sortEdges";
import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte"; import ReferenceInlineButtons from "./ReferenceInlineButtons.svelte";
import Sortable from "../../libs/Sortable.svelte"; import Sortable from "../../libs/Sortable.svelte";
import PreviewReference from "../previews/PreviewReference.svelte"; import PreviewReference from "../previews/PreviewReference.svelte";
import axios from "axios";
const channel = getContext("channel"); const channel = getContext("channel");
export let record; export let record;
@@ -15,27 +14,36 @@
export let validationErrors; export let validationErrors;
$: errorMessage = getErrorMessage(validationErrors, field.name); $: errorMessage = getErrorMessage(validationErrors, field.name);
$: references =
$: references = graph.edges graph.edges
.filter((edge) => edge.field === field.name) .filter((edge) => edge.field === field.name)
.map((edge) => { .map((edge) => {
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source); return graph.records.find(
}).filter((rec) => (rec?.id ? true : false)) ?? []; (increc) =>
increc.id === edge.target && record.id === edge.source,
);
})
.filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) => let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name) field.collections.includes(aschema.name),
); );
function removeReference(e) { function removeReference(e) {
e.preventDefault(); e.preventDefault();
graph.edges = graph.edges.filter( graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === field.name) (edge) => !(edge.target === e.detail && edge.field === field.name),
); );
} }
function reorder(e) { function reorder(e) {
graph.edges = sortByField(
graph.edges = sortByField(e.detail.source, e.detail.target, graph.edges, field.name, references); e.detail.source,
e.detail.target,
graph.edges,
field.name,
references,
);
} }
function insert(e) { function insert(e) {
@@ -49,9 +57,14 @@
// }).then(function (response) { // }).then(function (response) {
// graph = response.data.graph; // graph = response.data.graph;
// }) // })
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action); graph = insertEdges(
graph,
record,
e.detail.records,
field.name,
e.detail.action,
);
} }
</script> </script>
{#if errorMessage} {#if errorMessage}
@@ -61,10 +74,10 @@
{/if} {/if}
<div class="inline-card-wrapper"> <div class="inline-card-wrapper">
<ReferenceInlineButtons <ReferenceInlineButtons
recordId={null} recordId={null}
schemas={collections} schemas={collections}
on:insert={insert} on:insert={insert}
on:save={insert} on:save={insert}
/> />
</div> </div>
{#if references.length > 0} {#if references.length > 0}
@@ -72,10 +85,10 @@
{#each references as reference (reference.id)} {#each references as reference (reference.id)}
<div> <div>
<PreviewReference <PreviewReference
{graph} {graph}
record={reference} record={reference}
hasDelete={true} hasDelete={true}
on:remove={removeReference} on:remove={removeReference}
/> />
</div> </div>
{/each} {/each}
@@ -1,11 +1,11 @@
<script> <script>
import {createEventDispatcher, getContext} from "svelte"; import { createEventDispatcher, getContext } from "svelte";
import Icon from "../../common/Icon.svelte"; import Icon from "../../common/Icon.svelte";
import InlineEdit from "../InlineEdit.svelte"; import InlineEdit from "../InlineEdit.svelte";
import Dialog from "../../dialog/Dialog.svelte"; import Dialog from "../../dialog/Dialog.svelte";
import DialogRecord from "../../dialog/DialogRecord.svelte"; import DialogRecord from "../../dialog/DialogRecord.svelte";
import axios from "axios";
import Dropdown from "../../common/Dropdown.svelte"; import Dropdown from "../../common/Dropdown.svelte";
import { apiGet } from "../../../helpers";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const channel = getContext("channel"); const channel = getContext("channel");
@@ -24,7 +24,7 @@
e.preventDefault(); e.preventDefault();
console.log("Save inline"); console.log("Save inline");
inLineCreateRecord = null; inLineCreateRecord = null;
dialogRecord.close() dialogRecord.close();
dispatch("save", { dispatch("save", {
records: e.detail.records, records: e.detail.records,
after: recordId, after: recordId,
@@ -44,11 +44,10 @@
function createInlineReference(e, schemaUId) { function createInlineReference(e, schemaUId) {
e.preventDefault(); e.preventDefault();
inLineCreateRecord = null; inLineCreateRecord = null;
axios apiGet(channel.lucentUrl + "/records/newInline?schema=" + schemaUId)
.get(channel.lucentUrl + "/records/newInline?schema=" + schemaUId)
.then((response) => { .then((response) => {
inLineCreateRecord = response.data; inLineCreateRecord = response;
dialogRecord.open() dialogRecord.open();
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
@@ -57,60 +56,53 @@
</script> </script>
{#if schemas.length > 1} {#if schemas.length > 1}
<div <div style="display: flex;align-items: center;gap:4px">
style="display: flex;align-items: center;gap:4px"
>
<Dropdown> <Dropdown>
<div slot="button">New</div> <div slot="button">New</div>
{#each schemas as schema} {#each schemas as schema}
<button <button
class=" button" class=" button"
on:click={(e) => on:click={(e) => createInlineReference(e, schema.name)}
createInlineReference(e, schema.name)} >{schema.label}
>{schema.label}
</button> </button>
{/each} {/each}
</Dropdown> </Dropdown>
<Dropdown> <Dropdown>
<div slot="button"> <Icon icon="magnifying-glass"/></div> <div slot="button"><Icon icon="magnifying-glass" /></div>
{#each schemas as schema} {#each schemas as schema}
<button <button
class="button" class="button"
on:click={(e) => openBrowseModal(e, schema.name)} on:click={(e) => openBrowseModal(e, schema.name)}
>{schema.label} >{schema.label}
</button> </button>
{/each} {/each}
</Dropdown> </Dropdown>
</div> </div>
{:else} {:else}
<div style="display:flex;align-items: center;gap: 4px"> <div style="display:flex;align-items: center;gap: 4px">
<button <button
class="button" class="button"
on:click={(e) => createInlineReference(e, schemas[0].name)} on:click={(e) => createInlineReference(e, schemas[0].name)}
>New >New
</button> </button>
<button <button
class="button" class="button"
on:click={(e) => openBrowseModal(e, schemas[0].name)} on:click={(e) => openBrowseModal(e, schemas[0].name)}
>
<Icon icon="magnifying-glass"/>
</button
> >
<Icon icon="magnifying-glass" />
</button>
</div> </div>
{/if} {/if}
<DialogRecord bind:this={dialogRecord}> <DialogRecord bind:this={dialogRecord}>
{#if inLineCreateRecord} {#if inLineCreateRecord}
<InlineEdit <InlineEdit
{...inLineCreateRecord} {...inLineCreateRecord}
isCreateMode={true} isCreateMode={true}
on:cancel={(e) => (inLineCreateRecord = null)} on:cancel={(e) => (inLineCreateRecord = null)}
on:inlinesaved={save} on:inlinesaved={save}
/> />
{/if} {/if}
</DialogRecord> </DialogRecord>
<Dialog bind:this={browseModal} on:insert={insert}/> <Dialog bind:this={browseModal} on:insert={insert} />
@@ -1,6 +1,5 @@
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import { debounce } from "lodash";
import { getErrorMessage } from "./errorMessage"; import { getErrorMessage } from "./errorMessage";
import { insertEdges } from "./reference.js"; import { insertEdges } from "./reference.js";
import Icon from "../../common/Icon.svelte"; import Icon from "../../common/Icon.svelte";
@@ -1,7 +1,6 @@
<script> <script>
import Tinymce from "../../libs/Tinymce.svelte";
import RichEditorFiles from "./RichEditorFiles.svelte"; import RichEditorFiles from "./RichEditorFiles.svelte";
import {getErrorMessage} from "./errorMessage"; import { getErrorMessage } from "./errorMessage";
import Trix from "../../libs/Trix.svelte"; import Trix from "../../libs/Trix.svelte";
export let value; export let value;
@@ -18,29 +17,24 @@
readonly: field.readonly && !isCreateMode, readonly: field.readonly && !isCreateMode,
}; };
function insertMedia(e){ function insertMedia(e) {
editor.insertMedia(e.detail) editor.insertMedia(e.detail);
} }
</script> </script>
<div class="mb-0"> <div class="mb-0">
<Trix {field} bind:this={editor} bind:value></Trix> <Trix {field} bind:this={editor} bind:value></Trix>
<!-- <Tinymce bind:this={editor} bind:value {additionalConfig}/>--> <!-- <Tinymce bind:this={editor} bind:value {additionalConfig}/>-->
{#if field.collections.length > 0} {#if field.collections.length > 0}
<RichEditorFiles <RichEditorFiles
bind:graph bind:graph
{record} {record}
{field} {field}
{validationErrors} {validationErrors}
on:editor-insert={insertMedia} on:editor-insert={insertMedia}
> ></RichEditorFiles>
</RichEditorFiles>
{/if} {/if}
{#if errorMessage} {#if errorMessage}
<div class="invalid-feedback d-block"> <div class="invalid-feedback d-block">
{errorMessage} {errorMessage}
+8 -10
View File
@@ -1,5 +1,4 @@
<script> <script>
import { v4 as uuidv4 } from "uuid";
import { getContext } from "svelte"; import { getContext } from "svelte";
import Icon from "../../common/Icon.svelte"; import Icon from "../../common/Icon.svelte";
import { getErrorMessage } from "./errorMessage"; import { getErrorMessage } from "./errorMessage";
@@ -14,12 +13,11 @@
function generateId(e) { function generateId(e) {
e.preventDefault(); e.preventDefault();
value = uuidv4(); value = self.crypto.randomUUID();
} }
</script> </script>
<div class="mb-0"> <div class="mb-0">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<input <input
type="text" type="text"
@@ -31,13 +29,13 @@
{readonly} {readonly}
/> />
{#if !readonly} {#if !readonly}
<button <button
class="btn btn-primary ms-2" class="btn btn-primary ms-2"
title="Generate a new UUIDv4" title="Generate a new UUIDv4"
on:click={generateId} on:click={generateId}
> >
<Icon icon="dice" /> <Icon icon="dice" />
</button> </button>
{/if} {/if}
</div> </div>
+2 -3
View File
@@ -1,11 +1,10 @@
<script> <script>
import { uniqueId } from "lodash";
import { getContext } from "svelte"; import { getContext } from "svelte";
const channelurl = getContext("channelurl"); const channelurl = getContext("channelurl");
export let field; export let field;
export let value; export let value;
export let schema; export let schema;
let id = uniqueId(); let id = self.crypto.randomUUID();
</script> </script>
<div class="mb-0"> <div class="mb-0">
@@ -25,6 +24,6 @@
placeholder="https://www.example.com" placeholder="https://www.example.com"
/> />
{#if field.help} {#if field.help}
<small class=" text-primary opacity-50">{field.help}</small> <small class=" text-primary opacity-50">{field.help}</small>
{/if} {/if}
</div> </div>
+32 -20
View File
@@ -1,24 +1,36 @@
import {uniqBy} from "lodash"; import { arrayUniqueCb } from "../../../helpers";
export function insertEdges(graph, sourceRecord, targetRecords, fieldName, action = "") { export function insertEdges(
let newEdges = targetRecords.map((r) => { graph,
return { sourceRecord,
target: r.id, targetRecords,
source: sourceRecord.id, fieldName,
sourceSchema: sourceRecord.schema, action = "",
targetSchema: r.schema, ) {
field: fieldName, let newEdges = targetRecords.map((r) => {
depth: 1, return {
rank: "" target: r.id,
}; source: sourceRecord.id,
}); sourceSchema: sourceRecord.schema,
targetSchema: r.schema,
field: fieldName,
depth: 1,
rank: "",
};
});
let replacedEdges = graph.edges; let replacedEdges = graph.edges;
if (action === "replace") { if (action === "replace") {
replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name); replacedEdges = replacedEdges.filter((edge) => edge.field !== field.name);
} }
graph.records = uniqBy([...graph.records, ...targetRecords], (r) => r.id); graph.records = arrayUniqueCb(
graph.edges = uniqBy([...replacedEdges, ...newEdges], (edge) => edge.source + edge.target + edge.field + edge.depth); [...graph.records, ...targetRecords],
return graph; (r) => r.id,
);
graph.edges = arrayUniqueCb(
[...replacedEdges, ...newEdges],
(edge) => edge.source + edge.target + edge.field + edge.depth,
);
return graph;
} }
+1695 -4376
View File
File diff suppressed because it is too large Load Diff
+27 -32
View File
@@ -1,34 +1,29 @@
{ {
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build" "build": "vite build"
}, },
"devDependencies": { "devDependencies": {
"@codemirror/commands": "^6.6.0", "@codemirror/commands": "^6.6.0",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.2.5", "@codemirror/lang-markdown": "^6.2.5",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.4.1",
"@sveltejs/vite-plugin-svelte": "^3.1.1", "@sveltejs/vite-plugin-svelte": "^3.1.1",
"axios": "^1.7.4", "codemirror": "^6.0.1",
"codemirror": "^6.0.1", "date-fns": "^3.6.0",
"date-fns": "^3.6.0", "flatpickr": "^4.6.13",
"flatpickr": "^4.6.13", "fuse.js": "^7.0.0",
"fuse.js": "^7.0.0", "htmx.org": "^2.0.1",
"htmx.org": "^2.0.1", "install": "^0.13.0",
"install": "^0.13.0", "laravel-vite-plugin": "^1.0.5",
"laravel-vite-plugin": "^1.0.5", "mustache": "^4.2.0",
"lodash": "^4.17.21", "postcss": "8.4.31",
"mustache": "^4.2.0", "sass": "^1.77.8",
"npm": "^10.8.2", "sortablejs": "^1.15.2",
"postcss": "8.4.31", "svelte": "^4.2.18",
"sass": "^1.77.8", "trix": "^2.1.5",
"sortablejs": "^1.15.2", "vite": "5.2.6"
"svelte": "^4.2.18", }
"tinymce": "^6.8.4",
"trix": "^2.1.5",
"uuid": "^10.0.0",
"vite": "5.2.6"
}
} }
+3 -6
View File
@@ -7,11 +7,8 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Lucent\Channel\ChannelService; use Lucent\Channel\ChannelService;
use Lucent\File\FileService; use Lucent\File\FileService;
use Lucent\File\ImageService;
use Lucent\Query\Query; use Lucent\Query\Query;
use Lucent\Record\InputData\RecordInputData;
use Lucent\Record\RecordService; use Lucent\Record\RecordService;
use Lucent\Record\Status;
use function Lucent\Response\fail; use function Lucent\Response\fail;
use function Lucent\Response\ok; use function Lucent\Response\ok;
@@ -27,20 +24,20 @@ class FileController extends Controller
public function fromDisk(Request $request, string $disk) public function fromDisk(Request $request, string $disk)
{ {
$imagePath = $request->route("any"); $imagePath = $request->route("any");
$disk = $this->fileService->loadDisk($disk); $disk = $this->fileService->loadPublicDisk($disk);
return response()->file($disk->path($imagePath)); return response()->file($disk->path($imagePath));
} }
public function thumb(Request $request, string $disk) public function thumb(Request $request, string $disk)
{ {
$imagePath = "thumbs/" . $request->route("any"); $imagePath = "thumbs/" . $request->route("any");
$disk = $this->fileService->loadDisk($disk); $disk = $this->fileService->loadPublicDisk($disk);
return response()->file($disk->path($imagePath)); return response()->file($disk->path($imagePath));
} }
public function download(Request $request) public function download(Request $request)
{ {
$disk = $this->fileService->loadDisk(); $disk = $this->fileService->loadPublicDisk();
return $disk->download($request->input("path")); return $disk->download($request->input("path"));
} }