filters and sidebar

This commit is contained in:
2024-08-15 14:44:53 +03:00
parent 1f3ebafe69
commit f9806f60c9
32 changed files with 584 additions and 456 deletions
+20
View File
@@ -30,3 +30,23 @@ export function stripHtml(html = "") {
export function randomId(length = 10) {
return Math.random().toString(36).substring(2, length + 2);
}
export function clickOutside(node) {
const handleClick = event => {
if (node && !node.contains(event.target) && !event.defaultPrevented) {
node.dispatchEvent(
new CustomEvent('click_outside', node)
)
}
}
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
}
}
+12 -3
View File
@@ -4,9 +4,10 @@
import RecordEdit from "./records/Edit.svelte";
import ContentIndex from "./content/Index.svelte";
import {setContext} from "svelte";
import Navbar from "./Navbar.svelte";
import Navbar from "./layout/Navbar.svelte";
import HomeIndex from "./home/Index.svelte";
import BuildReport from "./build/Report.svelte";
import Header from "./layout/Header.svelte";
const components = {
members: Members,
@@ -35,7 +36,15 @@
</script>
<Navbar schema={data.schema}/>
<svelte:component this={components[view]} {title} {...data}/>
<div class="main-wrapper">
<div class="sidebar-content">
<Navbar schema={data.schema}/>
</div>
<div class="main-content">
<Header />
<svelte:component this={components[view]} {title} {...data}/>
</div>
</div>
-126
View File
@@ -1,126 +0,0 @@
<script>
import Avatar from "./account/Avatar.svelte";
import NavbarMenu from "./NavbarMenu.svelte";
import {getContext} from "svelte";
export let schema;
const channel = getContext("channel");
const readableSchemas = getContext("readableSchemas");
const user = getContext("user");
let contentIsOpen = false;
const fileSchemas = readableSchemas.filter((sc) => sc.type === "files");
const otherSchemas = readableSchemas.filter((sc) => !sc.isEntry && sc.type === "collection");
let filesIsActive = false;
let otherIsActive = false;
if(schema){
filesIsActive = fileSchemas.filter(s => s.name === schema.name).length > 0;
otherIsActive = otherSchemas.filter(s => s.name === schema.name).length > 0;
}
</script>
<nav class="lx-nav">
<div>
<button on:click={(e) => contentIsOpen = true} class="btn btn-primary btn-sm d-xxl-none">« Content</button>
</div>
<div class="d-flex align-items-center ">
<a class="nav-item" href="{channel.lucentUrl}">{channel.name}</a>
<a class="nav-item" href="{channel.lucentUrl}/members">Members</a>
{#if channel.generateCommand}
<a href="{channel.lucentUrl}/build-report" class="btn btn-outline-primary btn-sm d-">Build website</a>
{/if}
<!-- <div>-->
<!-- <form method="GET">-->
<!-- <input type="search" name="filter[search_regex]" placeholder="Search"-->
<!-- class="form-control" required/>-->
<!-- </form>-->
<!-- </div>-->
</div>
<div>
<a class="nav-item" href="{channel.lucentUrl}/profile">
<Avatar side="28" name={user.name}/>
</a>
</div>
</nav>
<div class="offcanvas offcanvas-start d-xxl-block show border-0 bg-light-subtle" class:d-none={!contentIsOpen}
style="padding-top:36px " data-bs-scroll="true"
data-bs-backdrop="false"
tabindex="-1" aria-labelledby="offcanvasScrollingLabel">
<!-- <div class="offcanvas-header">-->
<!-- <h5 class="offcanvas-title" id="offcanvasScrollingLabel">Content</h5>-->
<!-- </div>-->
<div class="offcanvas-body">
<button on:click={(e) => contentIsOpen = false} class="btn btn-primary btn-sm d-xxl-none mb-4">« close</button>
<div class="accordion">
<div class="accordion-item">
<h2 class="accordion-header" id="panelsStayOpen-headingMain">
<button class="accordion-button" type="button" data-bs-toggle="collapse"
data-bs-target="#panelsStayOpen-collapseMain" aria-expanded="true"
aria-controls="panelsStayOpen-collapseMain">
Main
</button>
</h2>
<div id="panelsStayOpen-collapseMain" class="accordion-collapse collapse show"
aria-labelledby="panelsStayOpen-headingMain">
<div class="accordion-body">
<NavbarMenu
schemas={ readableSchemas.filter((sc) => sc.isEntry)}
schema={schema}
/>
</div>
</div>
</div>
{#if otherSchemas.length > 0}
<div class="accordion-item">
<h2 class="accordion-header" id="panelsStayOpen-headingOther">
<button class="accordion-button" class:collapsed={!otherIsActive} type="button" data-bs-toggle="collapse"
data-bs-target="#panelsStayOpen-collapseOther" aria-expanded={otherIsActive}
aria-controls="panelsStayOpen-collapseOther">
Other
</button>
</h2>
<div id="panelsStayOpen-collapseOther" class="accordion-collapse collapse"
class:show={otherIsActive}
aria-labelledby="panelsStayOpen-headingOther">
<div class="accordion-body">
<NavbarMenu
schemas={ otherSchemas}
schema={schema}
/>
</div>
</div>
</div>
{/if}
{#if fileSchemas.length > 0}
<div class="accordion-item">
<h2 class="accordion-header" id="panelsStayOpen-headingFS">
<button class="accordion-button " class:collapsed={!filesIsActive} type="button" data-bs-toggle="collapse"
data-bs-target="#panelsStayOpen-collapseFS" aria-expanded={filesIsActive}
aria-controls="panelsStayOpen-collapseFS">
Filesystem
</button>
</h2>
<div id="panelsStayOpen-collapseFS" class="accordion-collapse collapse" class:show={filesIsActive}
aria-labelledby="panelsStayOpen-headingFS">
<div class="accordion-body">
<NavbarMenu
schemas={ fileSchemas}
schema={schema}
/>
</div>
</div>
</div>
{/if}
</div>
</div>
</div>
-16
View File
@@ -1,16 +0,0 @@
<script>
import {getContext} from "svelte";
const channel = getContext("channel");
export let schemas;
export let schema;
</script>
<div class="list-group list-group-flush">
{#each schemas as aschema}
<a class="list-group-item list-group-item-action" class:active={aschema.name === schema?.name}
aria-current="page"
href="{channel.lucentUrl}/content/{aschema.name}">{aschema.label}</a>
{/each}
</div>
+25 -19
View File
@@ -1,23 +1,29 @@
<script>
import {clickOutside} from "../../helpers.js";
let dropdownMenu;
export let orientation = "left";
export function open() {
dropdownMenu.classList.remove("hide")
}
function handleClickOutside() {
dropdownMenu.classList.add("hide")
}
export let width = "300";
let dropdownMenu;
export function hide(){
dropdownMenu.classList.remove("show")
}
</script>
<div class="dropdown">
<button
class="button dropdown-button"
type="button"
on:click={open}
aria-expanded="false"
>
<slot name="button">Dropdown</slot>
</button>
<div bind:this={dropdownMenu} class="dropdown-menu hide orientation-{orientation}" use:clickOutside on:click_outside={handleClickOutside}>
<slot/>
</div>
<button
class="btn btn-sm btn-outline-primary dropdown-toggle d-flex align-items-center"
type="button"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false"
>
<slot name="button">Dropdown</slot>
</button>
<div bind:this={dropdownMenu} class="dropdown-menu" style="width:{width}px;">
<slot/>
</div>
</div>
+2
View File
@@ -125,6 +125,7 @@
role="presentation"
{stroke}
{fill}
>
{@html selectedIcon.path}
</svg>
@@ -132,5 +133,6 @@
<style>
svg {
vertical-align: text-top;
}
</style>
+2 -2
View File
@@ -46,8 +46,8 @@
}
</script>
<div class="wrapper-large transparent ">
<div class="lx-card mb-4 {inModal ? 'mt-0' : 'mt-5'}">
<div class="">
<div class="{inModal ? 'mt-0' : 'mt-5'}">
<h3 class="header-normal mb-5 ">
{schema.label}
</h3>
+1 -1
View File
@@ -31,7 +31,7 @@
</script>
<div class="lx-table rounded">
<div class="table">
<table class="">
<thead class="table-light">
<tr>
+73 -82
View File
@@ -1,6 +1,7 @@
<script>
import Icon from "../../common/Icon.svelte";
import {createEventDispatcher} from "svelte";
import Dropdown from "../../common/Dropdown.svelte";
const dispatch = createEventDispatcher();
export let schema;
@@ -31,101 +32,91 @@
function sortAsc(e, field) {
e.preventDefault();
let prefix = systemFields.map((el) => el.name).includes(field.name) ? "" : "data.";
let prefix = systemFields.map((el) => el.name).includes(field.name) ? "" : "data.";
return triggerSortField(prefix + field.name);
}
function sortDesc(e, field) {
e.preventDefault();
let prefix = systemFields.map((el) => el.name).includes(field.name) ? "" : "data.";
let prefix = systemFields.map((el) => el.name).includes(field.name) ? "" : "data.";
return triggerSortField("-" + prefix + field.name);
}
</script>
<div class=" ">
<button
class="btn btn-sm btn-outline-primary dropdown-toggle d-flex align-items-center"
type="button"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false"
>
<Dropdown>
<div slot="button">
{#if sortParam.startsWith("-")}
<Icon icon="arrow-down-wide-short"/>
{:else}
<Icon icon="arrow-up-short-wide"/>
{/if}
<span class="ms-1">{sortField.label}</span>
</button>
<div class="dropdown-menu" style="width:auto;max-width:800px;">
<div class="row">
{#each sortableFields as field}
<div class="col-4 px-3 py-1 d-flex align-items-center">
<div class="btn-group w-100">
<button
on:click={(e) => sortAsc(e, field)}
title="Sort Ascending"
class="btn btn-sm {field.name == sortField.name && !sortParam.startsWith("-")
? 'btn-primary'
: 'btn-outline-primary'} "
>
<Icon icon="arrow-up-short-wide"/>
</button>
<button
on:click={(e) => sortDesc(e, field)}
title="Sort Descending"
class="btn btn-sm {field.name == sortField.name && sortParam.startsWith("-")
? 'btn-primary'
: 'btn-outline-primary'} "
>
<Icon icon="arrow-down-wide-short"/>
</button>
<button
title="Sort Ascending"
on:click={(e) => sortAsc(e, field)}
class="btn btn-sm btn-outline-primary w-100 text-nowrap"
style="overflow: hidden;"
>
{field.label}
</button>
</div>
</div>
{/each}
</div>
<h6 class="dropdown-header px-0">System</h6>
<div class="row">
{#each systemFieldsFiltered as field}
<div class="col-4 px-3 py-1 d-flex align-items-center">
<div class="btn-group w-100">
<button
on:click={(e) => sortAsc(e, field)}
title="Sort Ascending"
class="btn btn-sm {field.name == sortParam
? 'btn-primary'
: 'btn-outline-primary'} "
>
<Icon icon="arrow-up-short-wide"/>
</button>
<button
on:click={(e) => sortDesc(e, field)}
title="Sort Descending"
class="btn btn-sm {'-' + field.name == sortParam
? 'btn-primary'
: 'btn-outline-primary'} "
>
<Icon icon="arrow-down-wide-short"/>
</button>
<button
title="Sort Ascending"
on:click={(e) => sortAsc(e, field)}
class="btn btn-sm btn-outline-primary w-100 text-nowrap"
style="overflow: hidden;"
>
{field.label}
</button>
</div>
</div>
{/each}
</div>
</div>
</div>
<div>
{#each sortableFields as field}
<div class="dropdown-item">
<button
on:click={(e) => sortAsc(e, field)}
title="Sort Ascending"
class="button button-icon {field.name == sortField.name && !sortParam.startsWith("-")
? 'active'
: ''} "
>
<Icon icon="arrow-up-short-wide"/>
</button>
<button
on:click={(e) => sortDesc(e, field)}
title="Sort Descending"
class="button button-icon {field.name == sortField.name && sortParam.startsWith("-")
? 'active'
: ''} "
>
<Icon icon="arrow-down-wide-short"/>
</button>
<button
title="Sort Ascending"
on:click={(e) => sortAsc(e, field)}
class="button"
>
{field.label}
</button>
</div>
{/each}
<h6 class="dropdown-header">System</h6>
{#each systemFieldsFiltered as field}
<div class="dropdown-item">
<button
on:click={(e) => sortAsc(e, field)}
title="Sort Ascending"
class="button button-icon {field.name == sortParam
? 'active'
: ''} "
>
<Icon icon="arrow-up-short-wide"/>
</button>
<button
on:click={(e) => sortDesc(e, field)}
title="Sort Descending"
class="button button-icon {'-' + field.name == sortParam
? 'active'
: ''} "
>
<Icon icon="arrow-down-wide-short"/>
</button>
<button
title="Sort Ascending"
on:click={(e) => sortAsc(e, field)}
class="button"
>
{field.label}
</button>
</div>
{/each}
</div>
</Dropdown>
+33 -46
View File
@@ -4,7 +4,8 @@
import Icon from "../../common/Icon.svelte";
import SortFields from "./SortFields.svelte";
import AppliedFilter from "./AppliedFilter.svelte";
import {getContext, createEventDispatcher} from "svelte";
import {createEventDispatcher, getContext} from "svelte";
import Dropdown from "../../common/Dropdown.svelte";
const channel = getContext("channel");
@@ -47,8 +48,8 @@
}
</script>
<div class="mb-3 d-flex align-items-center justify-content-between">
<div class=" d-flex align-items-center">
<div class="toolbar">
<div class="toolbar-filters">
<SortFields
{schema}
@@ -72,19 +73,19 @@
/>
<form method="GET" on:submit={search}>
<input type="search" name="filter[search_regex]" placeholder="Search"
class="form-control" required>
<input type="search" name="filter[search_regex]" placeholder="Search"
class="form-control" required>
</form>
</div>
<div class="d-flex align-items-center ">
<div style="display:flex;align-items: center">
{#if schema.type === "collection"}
{#if !inModal && isWritable}
<a
href="{channel.lucentUrl}/records/new?schema={schema.name}"
class="btn btn-sm btn-primary"
class="button"
>
New Record
</a>
@@ -95,48 +96,34 @@
</div>
{/if}
{#if !inModal}
<div class="dropdown d-inline-block">
<button
class="btn btn-link btn-sm"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<Dropdown orientation="right">
<div slot="button">
<Icon icon="ellipsis-vertical"/>
</button>
<ul class="dropdown-menu">
{#if filter["status_in"] === "trashed"}
{#if isWritable}
<li>
<a
class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
>
Empty trash
</a>
</li>
{/if}
{:else}
<li>
<a
class="dropdown-item"
href={csvUrl}
>Export to CSV</a
>
</li>
<li>
<a
class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
>View trashed records</a
>
</li>
</div>
{#if filter["status_in"] === "trashed"}
{#if isWritable}
<a
class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
>
Empty trash
</a>
{/if}
{:else}
<a
class="dropdown-item"
href={csvUrl}
>Export to CSV</a
>
<a
class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
>View trashed records</a
>
{/if}
</Dropdown>
</ul>
</div>
{/if}
</div>
</div>
+52 -35
View File
@@ -28,41 +28,58 @@
fontSize = "13";
}
</script>
<div style="display: flex;align-items: center;gap: 5px;">
{#if record}
{#if record}
{#if record._file.mime.startsWith("image")}
<!-- href={imgurl(record)} -->
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path}
class="d-flex align-items-center justify-content-center "
style="width:{imageSide}px;height:{imageSide}px"
>
<img
class="rounded w-100"
src={imgurl(record)}
alt={record._file.path}
/>
</a>
{:else}
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path}
class="btn btn-outline-primary btn-sm d-flex align-items-center justify-content-center"
style="width:{imageSide}px;height:{imageSide}px"
>
<Icon icon="file" width={fileSide} height={fileSide}/>
<span class="ms-2" style="font-size:{fontSize}px"
>.{record._file.path.split(".").pop()}</span
{#if record._file.mime.startsWith("image")}
<!-- href={imgurl(record)} -->
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path}
style="width:{imageSide}px;height:{imageSide}px"
>
</a>
<img
class="rounded w-100"
src={imgurl(record)}
alt={record._file.path}
/>
</a>
{:else}
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path}
class="file-preview-small"
style="width:{imageSide}px;height:{imageSide}px"
>
<Icon icon="file" width={fileSide} height={fileSide}/>
<span class="ms-2"
>.{record._file.path.split(".").pop()}</span
>
</a>
{/if}
{/if}
{/if}
{#if showFilename}
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path}
class="preview-file-filename lx-small-text text-decoration-none"
>{record._file.path}</a
>
{/if}
{#if showFilename}
<a
href="{channel.lucentUrl}/records/{record.id}"
title={record._file.path}
class="preview-file-filename lx-small-text text-decoration-none"
>{record._file.path}</a
>
{/if}
</div>
<style>
img{
border-radius: 12px;
padding: 4px;
}
.file-preview-small{
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
border-radius: 12px;
padding: 4px;
background: var(--background);
}
</style>
+15 -18
View File
@@ -21,24 +21,21 @@
});
</script>
<div class="wrapper-normal transparent">
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
{#if records.length > 0}
<div class="lx-card mb-4">
<div class="lx-table p-0">
<table class="">
<tbody>
{#each records as record (record.id)}
<tr>
<RecordRow {graph} {record} {users}/>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
<h3 class="header-small mb-4 mt-5">Latest Content changes</h3>
{#if records.length > 0}
{/if}
<div class="table">
<table class="">
<tbody>
{#each records as record (record.id)}
<tr>
<RecordRow {graph} {record} {users}/>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
+11 -11
View File
@@ -1,7 +1,6 @@
<script>
import {formatDistanceToNow, parseJSON} from "date-fns";
import Avatar from "../account/Avatar.svelte";
import Status from "../records/Status.svelte";
import {previewTitle} from "../records/Preview";
import Preview from "../files/Preview.svelte";
import {usernameById} from "../account/users";
@@ -19,29 +18,30 @@
</script>
<td>
<div class="row-name">
{#if record.status === "draft"}
<span class="status">DRAFT</span>
{/if}
{#if schema.type === "files"}
<Preview {record} size="tiny"/>
<Preview {record} size="tiny" showFilename={true}/>
{:else}
<a
href="{channel.lucentUrl}/records/{record.id}"
class="text-decoration-none text-dark d-block"
href="{channel.lucentUrl}/records/{record.id}"
>
{previewTitle(channel.schemas, record, graph)}
</a>
{/if}
</div>
</td>
<td><a
class="text-decoration-none lx-small-text"
href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a
href="{channel.lucentUrl}/content/{schema.name}">{schema.label}</a
>
</td>
<td class="text-center">
<Status status={record.status}/>
</td>
<td>
<div class="d-flex">
<div style="display: flex;gap: 14px">
<Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>
<div class="ms-2">
{frieldlyUpdatedAt}
+26
View File
@@ -0,0 +1,26 @@
<script>
import Avatar from "../account/Avatar.svelte";
import {getContext} from "svelte";
const channel = getContext("channel");
const user = getContext("user");
</script>
<div class="top-nav ">
<a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a>
{#if channel.generateCommand}
<a href="{channel.lucentUrl}/build-report" class="top-nav-item">Build website</a>
{/if}
<!-- <div>-->
<!-- <form method="GET">-->
<!-- <input type="search" name="filter[search_regex]" placeholder="Search"-->
<!-- class="form-control" required/>-->
<!-- </form>-->
<!-- </div>-->
<a href="{channel.lucentUrl}/profile">
<Avatar side="28" name={user.name}/>
</a>
</div>
+42
View File
@@ -0,0 +1,42 @@
<script>
import NavbarMenu from "./NavbarMenu.svelte";
import {getContext} from "svelte";
import Avatar from "../account/Avatar.svelte";
export let schema;
const channel = getContext("channel");
const readableSchemas = getContext("readableSchemas");
const user = getContext("user");
const fileSchemas = readableSchemas.filter((sc) => sc.type === "files");
const otherSchemas = readableSchemas.filter((sc) => !sc.isEntry && sc.type === "collection");
</script>
<div class="sidebar-top">
<a class="logo" href="{channel.lucentUrl}">{channel.name}</a>
<a class="nav-item" href="{channel.lucentUrl}/profile">
<Avatar side="28" name={user.name}/>
</a>
</div>
<div class="sidebar">
<NavbarMenu
title="Content"
schemas={ readableSchemas.filter((sc) => sc.isEntry)}
schema={schema}
expanded={true}
/>
<NavbarMenu
title="Files"
schemas={ fileSchemas}
schema={schema}
/>
<NavbarMenu
title="Other"
schemas={ otherSchemas}
schema={schema}
/>
</div>
+34
View File
@@ -0,0 +1,34 @@
<script>
import {getContext} from "svelte";
import Icon from "../common/Icon.svelte";
const channel = getContext("channel");
export let schemas;
export let title;
export let schema;
export let expanded = false;
if(schemas.find(s => s.name === schema?.name)){
expanded = true;
}
function toggleExpand(){
expanded = !expanded;
}
</script>
<div class="sidebar-header" role="button" tabindex="0" on:click={toggleExpand}>
{title}
{#if expanded}
<Icon icon="circle-chevron-up"></Icon>
{:else}
<Icon icon="circle-chevron-down"></Icon>
{/if}
</div>
{#if expanded}
{#each schemas as aschema}
<a class="sidebar-item" class:active={aschema.name === schema?.name}
aria-current="page"
href="{channel.lucentUrl}/content/{aschema.name}">{aschema.label}</a>
{/each}
{/if}