refactor(dashboard): revamp how lists work entirely
This commit is contained in:
parent
a62b6d53e2
commit
39e18704a6
@ -7,8 +7,8 @@
|
||||
import Public from "./routes/Public.svelte";
|
||||
import Main from "./routes/dash/Profile.svelte";
|
||||
import Status from './routes/status/Status.svelte';
|
||||
import Member from './routes/dash/Member/Member.svelte';
|
||||
import Group from './routes/dash/Group/Group.svelte';
|
||||
import Member from './routes/dash/member/Member.svelte';
|
||||
import Group from './routes/dash/group/Group.svelte';
|
||||
import PageNotFound from './routes/PageNotFound.svelte';
|
||||
import { Alert } from 'sveltestrap';
|
||||
import DiscordLogin from "./routes/DiscordLogin.svelte";
|
||||
|
@ -15,7 +15,7 @@
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
$: if (item.name) {
|
||||
if ((searchBy === "display name" || sortBy === "display name") && item.display_name) htmlName = toHTML(item.display_name);
|
||||
if ((searchBy === "display_name" || sortBy === "display_name") && item.display_name) htmlName = toHTML(item.display_name);
|
||||
else htmlName = toHTML(item.name);
|
||||
} else htmlName = "";
|
||||
|
||||
|
@ -95,8 +95,7 @@
|
||||
update({...group, ...res});
|
||||
group = {...group, ...res};
|
||||
err = [];
|
||||
success = true;
|
||||
loading = false;
|
||||
view = 'card';
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err.push(error.message);
|
||||
@ -106,7 +105,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card class="mb-4 pb-3" style={`height: 27.5rem; ${group.color && `border-bottom: 4px solid #${group.color}`}`}>
|
||||
<div class="mb-4 pb-3 card" style={`height: 27.5rem; ${group.color && `border-bottom: 4px solid #${group.color}`}`}>
|
||||
<CardHeader>
|
||||
<CardTitle class="d-flex justify-content-center align-middle w-100 mb-0">
|
||||
<div class="icon d-inline-block">
|
||||
@ -133,13 +132,13 @@
|
||||
<hr style="min-height: 1px;"/>
|
||||
<Row>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
{#if !isPublic}<Button color="link" class="mt-2" on:click={() => {view = "edit"}} id={`group-${group.uuid}-edit-button-card`}><FaEdit/></Button>{/if}
|
||||
{#if !isPublic}<Button color="link" class="mt-2" style="height: 3.5rem;" on:click={() => {view = "edit"}} id={`group-${group.uuid}-edit-button-card`}><FaEdit/></Button>{/if}
|
||||
</Col>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
{#if !isPublic && isDash}<Button color="link" class="mt-2 text-reset" on:click={() => {view = "groups"}} id={`group-${group.uuid}-groups-button-card`}><FaUserAlt/></Button>{/if}
|
||||
{#if !isPublic && isDash}<Button color="link" class="mt-2 text-reset" on:click={() => {view = "groups"}} style="height: 3.5rem;" id={`group-${group.uuid}-groups-button-card`}><FaUserAlt/></Button>{/if}
|
||||
</Col>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
<Link tabindex={-1} to={pageLink} class="text-reset"><Button color="link" class="mt-2 w-100 text-reset" id={`group-${group.uuid}-view-button-card`}><FaInfoCircle/></Button></Link>
|
||||
<Link tabindex={-1} to={pageLink} class="text-reset"><Button color="link" class="mt-2 w-100 text-reset" style="height: 3.5rem;" id={`group-${group.uuid}-view-button-card`}><FaInfoCircle/></Button></Link>
|
||||
</Col>
|
||||
{#if !isPublic}<Tooltip target={`group-${group.uuid}-edit-button-card`} placement="bottom">Edit group</Tooltip>{/if}
|
||||
{#if !isPublic && isDash}<Tooltip target={`group-${group.uuid}-groups-button-card`} placement="bottom">View members</Tooltip>{/if}
|
||||
@ -160,13 +159,13 @@
|
||||
<hr style="min-height: 1px"/>
|
||||
<Row>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
<Button color="link" class="mt-2" on:click={() => {view = "edit"}} id={`group-${group.uuid}-edit-button-groups`}><FaEdit/></Button>
|
||||
<Button color="link" class="mt-2" style="height: 3.5rem;" on:click={() => {view = "edit"}} id={`group-${group.uuid}-edit-button-groups`}><FaEdit/></Button>
|
||||
</Col>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
<Button color="link" class="mt-2 text-reset" on:click={() => {view = "card"}} id={`group-${group.uuid}-back-button-groups`}><FaTimes/></Button>
|
||||
<Button color="link" class="mt-2 text-reset" style="height: 3.5rem;" on:click={() => {view = "card"}} id={`group-${group.uuid}-back-button-groups`}><FaTimes/></Button>
|
||||
</Col>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
<Link tabindex={-1} to={`./m/${group.id}`} class="text-reset"><Button color="link" class="mt-2 w-100 text-reset" id={`group-${group.uuid}-view-button-groups`}><FaInfoCircle/></Button></Link>
|
||||
<Link tabindex={-1} to={`./m/${group.id}`} class="text-reset"><Button color="link" class="mt-2 w-100 text-reset" style="height: 3.5rem;" id={`group-${group.uuid}-view-button-groups`}><FaInfoCircle/></Button></Link>
|
||||
</Col>
|
||||
<Tooltip target={`group-${group.uuid}-edit-button-groups`} placement="bottom">Edit group</Tooltip>
|
||||
<Tooltip target={`group-${group.uuid}-back-button-groups`} placement="bottom">Back to info</Tooltip>
|
||||
@ -194,9 +193,6 @@
|
||||
</Col>
|
||||
</Row>
|
||||
<hr style="min-height: 1px" />
|
||||
{#if success}
|
||||
<Alert class="m-0 mb-2" color="success">group edited!</Alert>
|
||||
{/if}
|
||||
{#if err}
|
||||
{#each err as errorMessage}
|
||||
<Alert class="m-0 mb-2" color="danger">{@html errorMessage}</Alert>
|
||||
@ -209,14 +205,14 @@
|
||||
<Col xs={4}>
|
||||
</Col>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
<Button disabled={loading} color="link" class="mt-2 text-success" on:click={submit} id={`group-${group.uuid}-submit-button-edit`}><FaCheck/></Button>
|
||||
<Button disabled={loading} color="link" class="mt-2 text-success" style="height: 3.5rem;" on:click={submit} id={`group-${group.uuid}-submit-button-edit`}><FaCheck/></Button>
|
||||
</Col>
|
||||
<Tooltip target={`group-${group.uuid}-back-button-edit`} placement="bottom">Go back</Tooltip>
|
||||
<Tooltip target={`group-${group.uuid}-submit-button-edit`} placement="bottom">Submit edit</Tooltip>
|
||||
</Row>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
@ -227,4 +223,13 @@
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.item {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.item {
|
||||
height: 30rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -51,8 +51,8 @@
|
||||
let res = await api().groups(group.id).patch({data});
|
||||
update({...group, ...res});
|
||||
err = [];
|
||||
success = true;
|
||||
loading = false;
|
||||
editMode = false;
|
||||
return;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err.push(error.message);
|
||||
|
@ -7,25 +7,19 @@
|
||||
import type { Member, Group } from '../../api/types';
|
||||
import MemberCard from '../member/CardView.svelte';
|
||||
import GroupCard from '../group/CardView.svelte';
|
||||
import type { PageOptions, List } from './types';
|
||||
|
||||
export let list: Member[]|Group[];
|
||||
export let groups: Group[] = [];
|
||||
export let members: Group[] = [];
|
||||
|
||||
export let itemType: string;
|
||||
|
||||
export let searchBy = "name";
|
||||
export let sortBy = "name";
|
||||
export let isPublic = false;
|
||||
export let isDash = false;
|
||||
export let lists: List<Member|Group>;
|
||||
export let pageOptions: PageOptions;
|
||||
export let otherList: List<Member|Group>;
|
||||
|
||||
let copiedItems = {};
|
||||
|
||||
function getShortLink(id: string) {
|
||||
let url = "https://pk.mt"
|
||||
|
||||
if (itemType === "member") url += "/m/"
|
||||
else if (itemType === "group") url += "/g/"
|
||||
if (pageOptions.type === "member") url += "/m/"
|
||||
else if (pageOptions.type === "group") url += "/g/"
|
||||
|
||||
url += id;
|
||||
|
||||
@ -53,12 +47,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
{#if itemType === "member"}
|
||||
{#each list as item (item.uuid)}
|
||||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 mx-auto mx-sm-0 dont-squish">
|
||||
<MemberCard on:update member={item} {searchBy} {sortBy} {groups} {isPublic} {isDash}>
|
||||
<div slot="icon" style="width: auto; height: 1em; cursor: pointer;" id={`${itemType}-copy-${item.uuid}`} on:click|stopPropagation={() => copyShortLink(item.uuid, item.id)} on:keydown={(e) => copyShortLink(item.uuid, item.id, e)} tabindex={0} >
|
||||
<Row class="mx-4 mx-sm-5 mx-md-0">
|
||||
{#if pageOptions.type === "member"}
|
||||
{#each lists.currentPage as item (item.uuid)}
|
||||
<div class="col-12 col-md-6 col-lg-4 col-xxl-3 mx-auto mx-sm-0 dont-squish">
|
||||
<MemberCard on:update member={item} searchBy="name" sortBy="name" groups={otherList.rawList} isPublic={pageOptions.isPublic} isDash={pageOptions.isMain}>
|
||||
<div slot="icon" style="width: auto; height: 1em; cursor: pointer;" id={`${pageOptions.type}-copy-${item.uuid}`} on:click|stopPropagation={() => copyShortLink(item.uuid, item.id)} on:keydown={(e) => copyShortLink(item.uuid, item.id, e)} tabindex={0} >
|
||||
{#if item.privacy && item.privacy.visibility === "private"}
|
||||
<FaLock />
|
||||
{:else}
|
||||
@ -66,14 +60,14 @@
|
||||
{/if}
|
||||
</div>
|
||||
</MemberCard>
|
||||
<Tooltip placement="top" target={`${itemType}-copy-${item.uuid}`}>{copiedItems[item.uuid] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
<Tooltip placement="top" target={`${pageOptions.type}-copy-${item.uuid}`}>{copiedItems[item.uuid] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</div>
|
||||
{/each}
|
||||
{:else if itemType === "group"}
|
||||
{#each list as item (item.uuid)}
|
||||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 mx-auto mx-sm-0 dont-squish">
|
||||
<GroupCard group={item} {searchBy} {sortBy} {members} {isPublic} {isDash}>
|
||||
<div slot="icon" style="width: auto; height: 1em; cursor: pointer;" id={`${itemType}-copy-${item.uuid}`} on:click|stopPropagation={() => copyShortLink(item.uuid, item.id)} on:keydown={(e) => copyShortLink(item.uuid, item.id, e)} tabindex={0} >
|
||||
{:else if pageOptions.type === "group"}
|
||||
{#each lists.currentPage as item (item.uuid)}
|
||||
<div class="col-12 col-md-6 col-lg-4 col-xxl-3 mx-auto mx-sm-0 dont-squish">
|
||||
<GroupCard group={item} searchBy="name" sortBy="name" members={otherList.rawList} isPublic={pageOptions.isPublic} isDash={pageOptions.isMain}>
|
||||
<div slot="icon" style="width: auto; height: 1em; cursor: pointer;" id={`${pageOptions.type}-copy-${item.uuid}`} on:click|stopPropagation={() => copyShortLink(item.uuid, item.id)} on:keydown={(e) => copyShortLink(item.uuid, item.id, e)} tabindex={0} >
|
||||
{#if item.privacy && item.privacy.visibility === "private"}
|
||||
<FaLock />
|
||||
{:else}
|
||||
@ -81,7 +75,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</GroupCard>
|
||||
<Tooltip placement="top" target={`${itemType}-copy-${item.uuid}`}>{copiedItems[item.uuid] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
<Tooltip placement="top" target={`${pageOptions.type}-copy-${item.uuid}`}>{copiedItems[item.uuid] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
@ -7,80 +7,50 @@
|
||||
import NewGroup from '../group/NewGroup.svelte';
|
||||
import ListPagination from '../common/ListPagination.svelte';
|
||||
import ListControl from './ListControl.svelte';
|
||||
import ListSearch from './ListSearch.svelte';
|
||||
import ListView from './ListView.svelte';
|
||||
import CardView from './CardView.svelte';
|
||||
|
||||
import type { Member, Group } from '../../api/types';
|
||||
import api from '../../api';
|
||||
import type { ListOptions, List, PageOptions } from './types';
|
||||
import { createShortList, filterList, getPageAmount, paginateList } from './functions';
|
||||
|
||||
export let members: Member[] = [];
|
||||
export let groups: Group[] = [];
|
||||
|
||||
export let view: string = "list";
|
||||
|
||||
export let isDash = false;
|
||||
|
||||
let list: Member[] | Group[] = [];
|
||||
let processedList: Member[] | Group[] = [];
|
||||
|
||||
$: groupList = groups && groups.map(function(group) { return {name: group.name, shortid: group.id, id: group.uuid, members: group.members, display_name: group.display_name}; }).sort((a, b) => a.name.localeCompare(b.name));
|
||||
$: memberList = members && members.map(function(member) { return {name: member.name, shortid: member.id, id: member.uuid, display_name: member.display_name}; }).sort((a, b) => a.name.localeCompare(b.name));
|
||||
export let options: ListOptions;
|
||||
export let pageOptions: PageOptions;
|
||||
export let lists: List<Member|Group>;
|
||||
export let otherList: List<Member|Group>;
|
||||
|
||||
let token = localStorage.getItem("pk-token");
|
||||
let listLoading = true;
|
||||
let err: string;
|
||||
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
let pageAmount: number;
|
||||
let currentPage: number = 1;
|
||||
let pageAmount = 1;
|
||||
|
||||
let itemsPerPageValue;
|
||||
$: {
|
||||
if (view === "card") itemsPerPageValue = "24";
|
||||
|
||||
else if (settings && settings.accessibility && settings.accessibility.expandedcards) itemsPerPageValue = "10";
|
||||
else itemsPerPageValue = "25";
|
||||
}
|
||||
|
||||
$: itemsPerPage = parseInt(itemsPerPageValue);
|
||||
|
||||
$: indexOfLastItem = currentPage * itemsPerPage;
|
||||
$: indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||
$: pageAmount = Math.ceil(processedList.length / itemsPerPage);
|
||||
|
||||
$: slicedList = processedList.slice(indexOfFirstItem, indexOfLastItem);
|
||||
|
||||
export let isPublic: boolean;
|
||||
export let itemType: string;
|
||||
|
||||
let searchValue: string = "";
|
||||
let searchBy: string = "name";
|
||||
let sortBy: string = "name";
|
||||
if (pageOptions.view === "card") pageOptions.itemsPerPage = 24;
|
||||
else if (settings && settings.accessibility && settings.accessibility.expandedcards) pageOptions.itemsPerPage = 10;
|
||||
else pageOptions.itemsPerPage = 25;
|
||||
|
||||
let params = useParams();
|
||||
$: id = $params.id;
|
||||
|
||||
onMount(() => {
|
||||
if (token || isPublic) fetchList();
|
||||
if (token || pageOptions.isPublic) fetchList();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
err = "";
|
||||
listLoading = true;
|
||||
try {
|
||||
if (itemType === "member") {
|
||||
const res: Member[] = await api().systems(isPublic ? id : "@me").members.get({ auth: !isPublic });
|
||||
members = res;
|
||||
list = res;
|
||||
if (pageOptions.type === "member") {
|
||||
const res: Member[] = await api().systems(pageOptions.isPublic ? id : "@me").members.get({ auth: !pageOptions.isPublic });
|
||||
lists.rawList = res;
|
||||
}
|
||||
else if (itemType === "group") {
|
||||
const res: Group[] = await api().systems(isPublic ? id : "@me").groups.get({ auth: !isPublic, query: { with_members: !isPublic } });
|
||||
groups = res;
|
||||
list = res;
|
||||
else if (pageOptions.type === "group") {
|
||||
const res: Group[] = await api().systems(pageOptions.isPublic ? id : "@me").groups.get({ auth: !pageOptions.isPublic, query: { with_members: !pageOptions.isPublic } });
|
||||
lists.rawList = res;
|
||||
}
|
||||
else throw new Error(`Unknown list type ${itemType}`);
|
||||
else throw new Error(`Unknown list type ${pageOptions.type}`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err = error.message;
|
||||
@ -88,39 +58,28 @@
|
||||
listLoading = false;
|
||||
}
|
||||
|
||||
$: lists.shortGroups = createShortList(pageOptions.type === 'group' ? lists.rawList : otherList.rawList);
|
||||
$: lists.shortMembers = createShortList(pageOptions.type === 'member' ? lists.rawList : otherList.rawList);
|
||||
$: lists.processedList = filterList(lists.rawList, options, pageOptions.type);
|
||||
$: lists.currentPage = paginateList(lists.processedList, pageOptions);
|
||||
$: pageAmount = getPageAmount(lists.processedList, pageOptions);
|
||||
|
||||
function addItemToList(event: any) {
|
||||
if (itemType === "member") {
|
||||
members.push(event.detail);
|
||||
list = members;
|
||||
} else if (itemType === "group") {
|
||||
groups.push(event.detail);
|
||||
list = groups;
|
||||
}
|
||||
lists.rawList.push(event.detail);
|
||||
lists.rawList = lists.rawList;
|
||||
}
|
||||
|
||||
function updateDelete(event: any) {
|
||||
if (itemType === "member") {
|
||||
members = members.filter(m => m.id !== event.detail);
|
||||
list = members;
|
||||
} else if (itemType === "group") {
|
||||
groups = groups.filter(g => g.id !== event.detail);
|
||||
list = groups;
|
||||
}
|
||||
lists.rawList = lists.rawList.filter(m => m.id !== event.detail);
|
||||
}
|
||||
|
||||
function update(event: any) {
|
||||
if (itemType === "member") {
|
||||
members = members.map(m => m.id === event.detail.id ? m = event.detail : m);
|
||||
list = members;
|
||||
} else if (itemType === "group") {
|
||||
groups = groups.map(g => g.id === event.detail.id ? g = event.detail : g);
|
||||
list = groups;
|
||||
}
|
||||
lists.rawList = lists.rawList.map(m => m.id === event.detail.id ? m = event.detail : m);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<ListControl on:viewChange {itemType} {isPublic} {memberList} {groups} {groupList} {list} bind:finalList={processedList} bind:searchValue bind:searchBy bind:sortBy bind:itemsPerPageValue bind:currentPage bind:view />
|
||||
<ListControl on:viewChange bind:options bind:lists bind:pageOptions />
|
||||
|
||||
{#if listLoading && !err}
|
||||
<div class="mx-auto text-center">
|
||||
@ -136,26 +95,25 @@
|
||||
</Col>
|
||||
</Row>
|
||||
{:else}
|
||||
<span class="itemcounter">{processedList.length} {itemType}s ({slicedList.length} shown)</span>
|
||||
<ListSearch bind:searchBy bind:searchValue on:refresh={fetchList} />
|
||||
<span class="itemcounter">{lists.processedList.length} {pageOptions.type}s ({lists.currentPage.length} shown) <a href="#!" on:click={(e) => {e.preventDefault(); fetchList()}}>Refresh list</a></span>
|
||||
|
||||
<ListPagination bind:currentPage {pageAmount} />
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
|
||||
{#if !err && !isPublic}
|
||||
{#if itemType === "member"}
|
||||
{#if !err && !pageOptions.isPublic}
|
||||
{#if pageOptions.type === "member"}
|
||||
<NewMember on:create={addItemToList} />
|
||||
{:else if itemType === "group"}
|
||||
{:else if pageOptions.type === "group"}
|
||||
<NewGroup on:create={addItemToList} />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if view === "card"}
|
||||
<CardView on:update={update} list={slicedList} {groups} {members} {itemType} {sortBy} {searchBy} {isPublic} {isDash} />
|
||||
{:else if view === "tiny"}
|
||||
{#if pageOptions.view === "card"}
|
||||
<CardView on:update={update} {otherList} {pageOptions} {lists} />
|
||||
{:else if pageOptions.view === "tiny"}
|
||||
tiny!
|
||||
{:else}
|
||||
<ListView on:update={update} on:deletion={updateDelete} list={slicedList} {groups} {members} {isPublic} {itemType} {itemsPerPage} {currentPage} {sortBy} {searchBy} fullLength={list.length} />
|
||||
<ListView on:update={update} on:deletion={updateDelete} {otherList} {lists} {pageOptions} {options} />
|
||||
{/if}
|
||||
<ListPagination bind:currentPage {pageAmount} />
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
@ -1,40 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { Card, CardHeader, CardBody, CardTitle, Alert, Accordion, AccordionItem, InputGroupText, InputGroup, Input, Row, Col, Spinner, Button, Tooltip, Label } from 'sveltestrap';
|
||||
import { Card, CardHeader, CardBody, CardTitle, InputGroupText, InputGroup, Input, Row, Col, Button, Tooltip, Label } from 'sveltestrap';
|
||||
import FaSearch from 'svelte-icons/fa/FaSearch.svelte'
|
||||
import FaQuestionCircle from 'svelte-icons/fa/FaQuestionCircle.svelte'
|
||||
import Svelecte, { addFormatter } from 'svelecte';
|
||||
import type { Member, Group } from '../../api/types';
|
||||
import { Link, useParams } from 'svelte-navigator';
|
||||
import moment from 'moment';
|
||||
|
||||
export let list: Member[] | Group[] = [];
|
||||
import type { ListOptions, List, PageOptions, ShortList } from './types';
|
||||
|
||||
export let itemType: string;
|
||||
export let memberList: any = [];
|
||||
export let groups: Group[] = [];
|
||||
export let groupList: any = [];
|
||||
export let options: ListOptions;
|
||||
export let lists: List<Member|Group>;
|
||||
export let pageOptions: PageOptions;
|
||||
|
||||
export let searchBy = "name";
|
||||
export let searchValue: string;
|
||||
export let itemsPerPageValue: string;
|
||||
export let view = "list";
|
||||
let advanced = false;
|
||||
|
||||
let itemsPerPageSelection = {
|
||||
small: "10",
|
||||
default: "25",
|
||||
large: "50"
|
||||
small: 10,
|
||||
default: 25,
|
||||
large: 50
|
||||
}
|
||||
|
||||
$: { if (view === "card") itemsPerPageSelection = {
|
||||
small: "12",
|
||||
default: "24",
|
||||
large: "60"
|
||||
$: { if (pageOptions.view === "card") itemsPerPageSelection = {
|
||||
small: 12,
|
||||
default: 24,
|
||||
large: 60
|
||||
}
|
||||
else {
|
||||
itemsPerPageSelection = {
|
||||
small: "10",
|
||||
default: "25",
|
||||
large: "50"
|
||||
small: 10,
|
||||
default: 25,
|
||||
large: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -42,180 +38,13 @@ $: { if (view === "card") itemsPerPageSelection = {
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function onViewChange(e: any) {
|
||||
resetPage();
|
||||
dispatch("viewChange", e.target.value);
|
||||
}
|
||||
|
||||
export let sortBy = "name";
|
||||
let sortOrder = "ascending";
|
||||
let privacyFilter = "all";
|
||||
let groupSearchMode = "include";
|
||||
let selectedGroups = [];
|
||||
|
||||
export let currentPage: number;
|
||||
export let isPublic: boolean;
|
||||
|
||||
let params = useParams();
|
||||
$: systemId = $params.id;
|
||||
|
||||
$: {searchValue; privacyFilter; currentPage = 1};
|
||||
|
||||
// converting list to any[] avoids a "this expression is not calleable" error
|
||||
$: searchedList = (list as any[]).filter((item: Member | Group) => {
|
||||
if (!searchValue && searchBy !== "description" && searchBy !== "display name") return true;
|
||||
|
||||
switch (searchBy) {
|
||||
case "name": if (item.name.toLowerCase().includes(searchValue.toLowerCase())) return true;
|
||||
break;
|
||||
case "display name": if (!searchValue) {
|
||||
if (!item.display_name) return true;
|
||||
else return false;
|
||||
}
|
||||
if (item.display_name && item.display_name.toLowerCase().includes(searchValue.toLowerCase())) return true;
|
||||
break;
|
||||
case "description": if (!searchValue) {
|
||||
if (!item.description) return true;
|
||||
else return false;
|
||||
}
|
||||
else if (item.description && item.description.toLowerCase().includes(searchValue.toLowerCase())) return true;
|
||||
break;
|
||||
case "ID": if (item.id.toLowerCase().includes(searchValue.toLowerCase())) return true;
|
||||
break;
|
||||
default: if (item.name.toLowerCase().includes(searchValue.toLowerCase())) return true;
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
|
||||
$: filteredList = searchedList.filter((item) => {
|
||||
if (privacyFilter === "all") return true;
|
||||
if (privacyFilter === "public" && item.privacy.visibility === "public") return true;
|
||||
if (privacyFilter === "private" && item.privacy.visibility === "private") return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
let sortedList = [];
|
||||
|
||||
$: if (filteredList) {
|
||||
let alphabeticalList = filteredList.sort((a, b) => a.name.localeCompare(b.name));
|
||||
switch (sortBy) {
|
||||
case "name": sortedList = alphabeticalList;
|
||||
break;
|
||||
case "display name": sortedList = filteredList.sort((a, b) => {
|
||||
if (a.display_name && b.display_name) return a.display_name.localeCompare(b.display_name);
|
||||
else if (a.display_name && !b.display_name) return a.display_name.localeCompare(b.name);
|
||||
else if (!a.display_name && b.display_name) return a.name.localeCompare(b.display_name);
|
||||
else return a.name.localeCompare(b.name);
|
||||
});
|
||||
break;
|
||||
case "creation date": sortedList = filteredList.sort((a, b) => {
|
||||
if (a.created && b.created) return a.created.localeCompare(b.created);
|
||||
});
|
||||
break;
|
||||
case "ID": sortedList = filteredList.sort((a, b) => a.id.localeCompare(b.id));
|
||||
break;
|
||||
case "avatar": sortedList = alphabeticalList.sort((a, b) => {
|
||||
if (a.icon === null || a.avatar_url === null) {
|
||||
return 1;
|
||||
};
|
||||
if (b.icon === null || b.avatar_url === null) {
|
||||
return -1;
|
||||
};
|
||||
if (a.icon === b.icon || a.avatar_url === b.avatar_url) {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "color": sortedList = alphabeticalList.sort((a, b) => {
|
||||
if (a.color === null) {
|
||||
return 1;
|
||||
};
|
||||
if (b.color === null) {
|
||||
return -1;
|
||||
};
|
||||
if (a.color === b.color) {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "birthday": sortedList = alphabeticalList.sort((a, b) => {
|
||||
if (a.birthday === null) {
|
||||
return 1;
|
||||
}
|
||||
if (b.birthday === null) {
|
||||
return -1;
|
||||
}
|
||||
let aBirthday = moment(a.birthday.slice(5, a.birthday.length), "MM-DD", true);
|
||||
let bBirthday = moment(b.birthday.slice(5, b.birthday.length), "MM-DD", true);
|
||||
if (aBirthday.isBefore(bBirthday)) {
|
||||
return -1;
|
||||
}
|
||||
if (aBirthday.isAfter(bBirthday)) {
|
||||
return 1;
|
||||
}
|
||||
if (aBirthday === bBirthday) {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "pronouns": sortedList = alphabeticalList.sort((a, b) => {
|
||||
if (a.pronouns === null) {
|
||||
return 1;
|
||||
};
|
||||
if (b.pronouns === null) {
|
||||
return -1;
|
||||
};
|
||||
return 0;
|
||||
})
|
||||
break;
|
||||
default: sortedList = filteredList.sort((a, b) => a.name.localeCompare(b.name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let memberFilteredList = [];
|
||||
$: memberFilteredList = sortedList.filter((item: Member | Group): boolean => {
|
||||
if (itemType === "member") {
|
||||
if (groupSearchMode === "none") {
|
||||
if (groups.some(group => group.members && group.members.includes(item.uuid))) return false;
|
||||
}
|
||||
|
||||
if (selectedGroups.length < 1) return true;
|
||||
|
||||
switch (groupSearchMode) {
|
||||
case "include": if (selectedGroups.some(group => group.members && group.members.includes(item.uuid))) return true;
|
||||
break;
|
||||
case "exclude": if (selectedGroups.every(group => group.members && !group.members.includes(item.uuid))) return true;
|
||||
break;
|
||||
case "match": if (selectedGroups.every(group => group.members && group.members.includes(item.uuid))) return true;
|
||||
break;
|
||||
default: return true;
|
||||
}
|
||||
} else if (itemType === "group") {
|
||||
let group = (item as Group);
|
||||
|
||||
if (groupSearchMode === "none") {
|
||||
if (group.members && group.members.length > 0) return false;
|
||||
}
|
||||
|
||||
if (selectedGroups.length < 1) return true;
|
||||
|
||||
switch (groupSearchMode) {
|
||||
case "include": if (group.members && selectedGroups.some(member => group.members.includes(member.id))) return true;
|
||||
break;
|
||||
case "exclude": if (group.members && selectedGroups.every(member => !group.members.includes(member.id))) return true;
|
||||
break;
|
||||
case "match": if (group.members && selectedGroups.every(member => group.members.includes(member.id))) return true;
|
||||
break;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
|
||||
export let finalList = [];
|
||||
$:{sortOrder; if (sortOrder === "descending") finalList = memberFilteredList.reverse(); else finalList = memberFilteredList;}
|
||||
|
||||
function groupListRenderer(item: any) {
|
||||
return `${item.name} (<code>${item.shortid}</code>)`;
|
||||
}
|
||||
@ -234,132 +63,303 @@ addFormatter({
|
||||
|
||||
function getRandomizerUrl(): string {
|
||||
let str: string;
|
||||
if (isPublic) str = `/profile/s/${systemId}/random`
|
||||
if (pageOptions.isPublic) str = `/profile/s/${systemId}/random`
|
||||
else str = "/dash/random";
|
||||
|
||||
if (itemType === "group") str += "/g";
|
||||
if (pageOptions.type === "group") str += "/g";
|
||||
return str;
|
||||
}
|
||||
|
||||
function capitalizeFirstLetter(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
function resetPage() {
|
||||
pageOptions.currentPage = 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card class="mb-3">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<CardTitle class="d-flex justify-content-between align-items-center mb-0">
|
||||
<div>
|
||||
<div class="icon d-inline-block">
|
||||
<FaSearch />
|
||||
</div> Search {itemType === "member" ? "members" : "groups"}
|
||||
</CardTitle>
|
||||
</div> Control {pageOptions.type} list
|
||||
</div>
|
||||
<Button color="primary" on:click={() => advanced = !advanced}>Toggle advanced mode</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Row>
|
||||
<Col xs={12} lg={3} class="mb-2">
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Page length</InputGroupText>
|
||||
<Input bind:value={itemsPerPageValue} type="select" aria-label="page length">
|
||||
<Input bind:value={pageOptions.itemsPerPage} type="select" aria-label="page length" on:change={() => resetPage()}>
|
||||
<option>{itemsPerPageSelection.small}</option>
|
||||
<option>{itemsPerPageSelection.default}</option>
|
||||
<option>{itemsPerPageSelection.large}</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs={12} lg={3} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Search by</InputGroupText>
|
||||
<Input bind:value={searchBy} type="select" aria-label="search by">
|
||||
<option>name</option>
|
||||
<option>display name</option>
|
||||
<option>description</option>
|
||||
<option>ID</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs={12} lg={3} class="mb-2">
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Sort by</InputGroupText>
|
||||
<Input bind:value={sortBy} type="select" aria-label="sort by">
|
||||
<option>name</option>
|
||||
<option>display name</option>
|
||||
{#if !isPublic}<option>creation date</option>{/if}
|
||||
<option>ID</option>
|
||||
<option>avatar</option>
|
||||
<option>color</option>
|
||||
{#if itemType === "member"}<option>birthday</option>{/if}
|
||||
{#if itemType === "member"}<option>pronouns</option>{/if}
|
||||
<Input bind:value={options.sort} type="select">
|
||||
<option value="name">Name</option>
|
||||
<option value="display_name">Display name</option>
|
||||
<option value="id">ID</option>
|
||||
{#if pageOptions.type === 'member'}
|
||||
<option value="pronouns">Pronouns</option>
|
||||
{/if}
|
||||
<option value="none">API response order</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs={12} lg={3} class="mb-2">
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Sort order</InputGroupText>
|
||||
<Input bind:value={sortOrder} type="select" aria-label="sort order">
|
||||
<option>ascending</option>
|
||||
<option>descending</option>
|
||||
<InputGroupText>Order</InputGroupText>
|
||||
<Input bind:value={options.order} type="select">
|
||||
<option value="ascending">Ascending</option>
|
||||
<option value="descending">Descending</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
{#if !isPublic}
|
||||
<Col xs={12} lg={3} class="mb-2">
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Only show</InputGroupText>
|
||||
<Input bind:value={privacyFilter} type="select" aria-label="only show">
|
||||
<option>all</option>
|
||||
<option>public</option>
|
||||
<option>private</option>
|
||||
<Input bind:value={options.show} type="select" aria-label="view mode" on:change={() => resetPage()}>
|
||||
<option value="all">All {pageOptions.type}s</option>
|
||||
<option value="public">Public {pageOptions.type}s</option>
|
||||
<option value="private">Private {pageOptions.type}s</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
{/if}
|
||||
<Col xs={12} lg={3} class="mb-2">
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>View mode</InputGroupText>
|
||||
<Input bind:value={view} type="select" aria-label="view mode" on:change={(e) => onViewChange(e)} >
|
||||
<option>list</option>
|
||||
<option value="card">cards</option>
|
||||
<Input bind:value={pageOptions.view} type="select" aria-label="view mode" on:change={(e) => onViewChange(e)} >
|
||||
<option value="list">List</option>
|
||||
<option value="card">Cards</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs={12} lg={3} class="mb-2">
|
||||
<Link to={getRandomizerUrl()}><Button class="w-100" color="secondary" tabindex={-1} aria-label={`randomize ${itemType}s`}>Random {capitalizeFirstLetter(itemType)}</Button></Link>
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<Link to={getRandomizerUrl()}><Button class="w-100" color="secondary" tabindex={-1} aria-label={`randomize ${pageOptions.type}s`}>Random {capitalizeFirstLetter(pageOptions.type)}</Button></Link>
|
||||
</Col>
|
||||
</Row>
|
||||
{#if !isPublic}
|
||||
<hr/>
|
||||
<Label>Filter {itemType === "member" ? "member" : "group"} by {itemType === "member" ? "group" : "member"}</Label>
|
||||
{#if itemType === "member"}
|
||||
<Svelecte disableHighlight renderer="group-list" valueAsObject bind:value={selectedGroups} options={groupList} multiple style="margin-bottom: 0.5rem" />
|
||||
{:else if itemType === "group"}
|
||||
<Svelecte disableHighlight renderer="member-list" valueAsObject bind:value={selectedGroups} options={memberList} multiple style="margin-bottom: 0.5rem" />
|
||||
{/if}
|
||||
<CardTitle class="d-flex justify-content-between my-3 h4">
|
||||
<div>
|
||||
Search {pageOptions.type === "member" ? "members" : "groups"}
|
||||
</div>
|
||||
<div class="icon d-inline-block" id={`${pageOptions.type}-search-help`}>
|
||||
<FaQuestionCircle />
|
||||
</div>
|
||||
<Tooltip target={`${pageOptions.type}-search-help`} placement="left" >You can search by multiple fields at the same time.<br/>The toggle controls whether to <b>exclude</b> or <b>include</b> the search term.</Tooltip>
|
||||
</CardTitle>
|
||||
<Row>
|
||||
<Col xs={12} class="mb-2">
|
||||
<InputGroup class="mb-2">
|
||||
<InputGroupText>Name</InputGroupText>
|
||||
<Input
|
||||
style="resize: none; overflow: hidden;"
|
||||
rows={1} type="textarea"
|
||||
bind:value={options.search.name}
|
||||
on:keydown={() => resetPage()}
|
||||
placeholder="Search by name..."/>
|
||||
<InputGroupText>
|
||||
<Input bind:checked={options.searchMode.name} type="switch"/>
|
||||
</InputGroupText>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<details>
|
||||
<summary><b>Toggle extra search fields</b></summary>
|
||||
<Row class="mt-3">
|
||||
<Col xs={12} lg={6} class="mb-2">
|
||||
<InputGroup class="mb-2">
|
||||
<InputGroupText>Display Name</InputGroupText>
|
||||
<Input
|
||||
style="resize: none; overflow: hidden;"
|
||||
rows={1} type="textarea"
|
||||
bind:value={options.search.display_name}
|
||||
on:keydown={() => resetPage()}
|
||||
placeholder="Search by display name..."/>
|
||||
<InputGroupText>
|
||||
<Input bind:checked={options.searchMode.display_name} type="switch"/>
|
||||
</InputGroupText>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs={12} lg={6} class="mb-2">
|
||||
<InputGroup class="mb-2">
|
||||
<InputGroupText>ID</InputGroupText>
|
||||
<Input
|
||||
style="resize: none; overflow: hidden;"
|
||||
rows={1} type="textarea"
|
||||
bind:value={options.search.id}
|
||||
on:keydown={() => resetPage()}
|
||||
placeholder="Search by ID..."/>
|
||||
<InputGroupText>
|
||||
<Input bind:checked={options.searchMode.id} type="switch"/>
|
||||
</InputGroupText>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
{#if pageOptions.type === 'member'}
|
||||
<Col xs={12} lg={6} class="mb-2">
|
||||
<InputGroup class="mb-2">
|
||||
<InputGroupText>Pronouns</InputGroupText>
|
||||
<Input
|
||||
style="resize: none; overflow: hidden;"
|
||||
rows={1} type="textarea"
|
||||
bind:value={options.search.pronouns}
|
||||
on:keydown={() => resetPage()}
|
||||
placeholder="Search by pronouns..."/>
|
||||
<InputGroupText>
|
||||
<Input bind:checked={options.searchMode.pronouns} type="switch"/>
|
||||
</InputGroupText>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
{/if}
|
||||
<Col xs={12} lg={6} class="mb-2">
|
||||
<InputGroup class="mb-2">
|
||||
<InputGroupText>Description</InputGroupText>
|
||||
<Input
|
||||
style="resize: none;"
|
||||
rows={1} type="textarea"
|
||||
bind:value={options.search.description}
|
||||
on:keydown={() => resetPage()}
|
||||
placeholder="Search by description..."/>
|
||||
<InputGroupText>
|
||||
<Input bind:checked={options.searchMode.description} type="switch"/>
|
||||
</InputGroupText>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</details>
|
||||
|
||||
<div class="filter-mode-group">
|
||||
<span class="filter-mode-label" id="m-include" on:click={() => groupSearchMode = "include"} on:keyup={e => e.key === "Enter" ? groupSearchMode = "include" : ""} tabindex={0}>{@html groupSearchMode === "include" ? "<b>include</b>" : "include"}</span>
|
||||
| <span class="filter-mode-label" id="m-exclude" on:click={() => groupSearchMode = "exclude"} on:keyup={e => e.key === "Enter" ? groupSearchMode = "exclude" : ""} tabindex={0}>{@html groupSearchMode === "exclude" ? "<b>exclude</b>" : "exclude"}</span>
|
||||
| <span class="filter-mode-label" id="m-match" on:click={() => groupSearchMode = "match"} on:keyup={e => e.key === "Enter" ? groupSearchMode = "match" : ""} tabindex={0}>{@html groupSearchMode === "match" ? "<b>exact match</b>" : "exact match"}</span>
|
||||
| <span class="filter-mode-label" id="m-none" on:click={() => groupSearchMode = "none"} on:keyup={e => e.key === "Enter" ? groupSearchMode = "none" : ""} tabindex={0}>{@html groupSearchMode === "none" ? "<b>none</b>" : "none"}</span>
|
||||
</div>
|
||||
<Tooltip placement="bottom" target="m-include">Includes every member who's a part of any of the groups.</Tooltip>
|
||||
<Tooltip placement="bottom" target="m-exclude">Excludes every member who's a part of any of the groups, the opposite of include.</Tooltip>
|
||||
<Tooltip placement="bottom" target="m-match">Only includes members who are a part of every group.</Tooltip>
|
||||
<Tooltip placement="bottom" target="m-none">Only includes members that are in no groups.</Tooltip>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<style>
|
||||
.filter-mode-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-mode-group {
|
||||
line-height: 1.5em;
|
||||
padding:0.375rem 0;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
</style>
|
||||
{#if advanced}
|
||||
<hr/>
|
||||
<CardTitle class="d-flex justify-content-between my-3 h4">
|
||||
<div>
|
||||
Filter by {pageOptions.type === "member" ? "groups" : "members"}
|
||||
</div>
|
||||
<div class="icon d-inline-block" id={`${pageOptions.type}-groups-help`}>
|
||||
<FaQuestionCircle />
|
||||
</div>
|
||||
<Tooltip target={`${pageOptions.type}-groups-help`} placement="left" >Augh</Tooltip>
|
||||
</CardTitle>
|
||||
<Row>
|
||||
<p><b>Include</b> {pageOptions.type === 'group' ? "groups with the following members" : "members in the following groups"}</p>
|
||||
<Col xs={12} md={7} lg={9} class="mb-2">
|
||||
{#if pageOptions.type === "member"}
|
||||
<Svelecte disableHighlight renderer="group-list" valueAsObject bind:value={options.groups.include.list} options={lists.shortGroups} multiple style="margin-bottom: 0.5rem" placeholder="Include..." />
|
||||
{:else if pageOptions.type === "group"}
|
||||
<Svelecte disableHighlight renderer="member-list" valueAsObject bind:value={options.groups.include.list} options={lists.shortMembers} multiple style="margin-bottom: 0.5rem" placeholder="Include..." />
|
||||
{/if}
|
||||
</Col>
|
||||
<Col xs={12} md={5} lg={3} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText class="w-75">Exact?</InputGroupText>
|
||||
<InputGroupText class="w-25 bg-body d-flex justify-content-center">
|
||||
<Input bind:checked={options.groups.include.exact} type="switch"/>
|
||||
</InputGroupText>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<p><b>Exclude</b> {pageOptions.type === 'group' ? "groups with the following members" : "members in the following groups"}</p>
|
||||
<Col xs={12} md={7} lg={9} class="mb-2">
|
||||
{#if pageOptions.type === "member"}
|
||||
<Svelecte disableHighlight renderer="group-list" valueAsObject bind:value={options.groups.exclude.list} options={lists.shortGroups} multiple style="margin-bottom: 0.5rem" placeholder="Exclude..." />
|
||||
{:else if pageOptions.type === "group"}
|
||||
<Svelecte disableHighlight renderer="member-list" valueAsObject bind:value={options.groups.exclude.list} options={lists.shortMembers} multiple style="margin-bottom: 0.5rem" placeholder="Exclude..." />
|
||||
{/if}
|
||||
</Col>
|
||||
<Col xs={12} md={5} lg={3} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText class="w-75">Exact?</InputGroupText>
|
||||
<InputGroupText class="w-25 bg-body d-flex justify-content-center">
|
||||
<Input bind:checked={options.groups.exclude.exact} type="switch"/>
|
||||
</InputGroupText>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr/>
|
||||
<CardTitle class="d-flex justify-content-between my-3 h4">
|
||||
<div>
|
||||
Filter by fields
|
||||
</div>
|
||||
<div class="icon d-inline-block" id={`${pageOptions.type}-filters-help`}>
|
||||
<FaQuestionCircle />
|
||||
</div>
|
||||
<Tooltip target={`${pageOptions.type}-filters-help`} placement="left" >I am so tired</Tooltip>
|
||||
</CardTitle>
|
||||
<Row class="mt-3">
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Display name</InputGroupText>
|
||||
<Input type="select" bind:value={options.filter.display_name} on:change={() => resetPage()}>
|
||||
<option value="all">All</option>
|
||||
<option value="include">With display name</option>
|
||||
<option value="exclude">Without display name</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Description</InputGroupText>
|
||||
<Input type="select" bind:value={options.filter.description} on:change={() => resetPage()}>
|
||||
<option value="all">All</option>
|
||||
<option value="include">With description</option>
|
||||
<option value="exclude">Without description</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
{#if pageOptions.type === 'member'}
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Avatar</InputGroupText>
|
||||
<Input type="select" bind:value={options.filter.avatar_url} on:change={() => resetPage()}>
|
||||
<option value="all">All</option>
|
||||
<option value="include">With avatar</option>
|
||||
<option value="exclude">Without avatar</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Birthday</InputGroupText>
|
||||
<Input type="select" bind:value={options.filter.birthday} on:change={() => resetPage()}>
|
||||
<option value="all">All</option>
|
||||
<option value="include">With birthday</option>
|
||||
<option value="exclude">Without birthday</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Pronouns</InputGroupText>
|
||||
<Input type="select" bind:value={options.filter.pronouns} on:change={() => resetPage()}>
|
||||
<option value="all">All</option>
|
||||
<option value="include">With pronouns</option>
|
||||
<option value="exclude">Without pronouns</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
{:else}
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Icon</InputGroupText>
|
||||
<Input type="select" bind:value={options.filter.icon} on:change={() => resetPage()}>
|
||||
<option value="all">All</option>
|
||||
<option value="include">With icon</option>
|
||||
<option value="exclude">Without icon</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
@ -10,52 +10,43 @@
|
||||
import MemberBody from '../member/Body.svelte';
|
||||
import GroupBody from '../group/Body.svelte';
|
||||
import CardsHeader from '../common/CardsHeader.svelte';
|
||||
import { defaultListOptions, type List, type ListOptions, type PageOptions } from './types';
|
||||
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
export let list: Member[]|Group[];
|
||||
export let members: Member[] = [];
|
||||
export let groups: Group[] = [];
|
||||
|
||||
export let isPublic: boolean;
|
||||
export let itemType: string;
|
||||
export let itemsPerPage: number;
|
||||
export let currentPage: number;
|
||||
export let fullLength: number;
|
||||
|
||||
export let openByDefault = false;
|
||||
|
||||
export let searchBy = "name";
|
||||
export let sortBy = "name";
|
||||
|
||||
$: indexStart = itemsPerPage * (currentPage - 1);
|
||||
export let options: ListOptions = JSON.parse(JSON.stringify(defaultListOptions));
|
||||
export let otherList: List <Member|Group>;
|
||||
export let lists: List<Member|Group>;
|
||||
export let pageOptions: PageOptions;
|
||||
|
||||
function getItemLink(item: Member | Group): string {
|
||||
let url: string;
|
||||
|
||||
if (!isPublic) url = "/dash/";
|
||||
if (!pageOptions.isPublic) url = "/dash/";
|
||||
else url = "/profile/";
|
||||
|
||||
if (itemType === "member") url += "m/";
|
||||
else if (itemType === "group") url += "g/";
|
||||
if (pageOptions.type === "member") url += "m/";
|
||||
else if (pageOptions.type === "group") url += "g/";
|
||||
|
||||
url += item.id;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
$: indexStart = pageOptions.itemsPerPage * (pageOptions.currentPage - 1);
|
||||
|
||||
function skipToNextItem(event, index: number) {
|
||||
let el;
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
if (index + 1 < indexStart + itemsPerPage && index + 1 < fullLength) el = document.getElementById(`${itemType}-card-${index + 1}`);
|
||||
else el = document.getElementById(`${itemType}-card-${indexStart}`);
|
||||
if (index + 1 < indexStart + pageOptions.itemsPerPage && index + 1 < lists.processedList.length) el = document.getElementById(`${pageOptions.type}-card-${index + 1}`);
|
||||
else el = document.getElementById(`${pageOptions.type}-card-${indexStart}`);
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
if (index - 1 >= indexStart) el = document.getElementById(`${itemType}-card-${index - 1}`);
|
||||
else if (fullLength <= indexStart + itemsPerPage) el = document.getElementById(`${itemType}-card-${fullLength - 1}`);
|
||||
else el = document.getElementById(`${itemType}-card-${indexStart + itemsPerPage - 1}`);
|
||||
if (index - 1 >= indexStart) el = document.getElementById(`${pageOptions.type}-card-${index - 1}`);
|
||||
else if (lists.processedList.length <= indexStart + pageOptions.itemsPerPage) el = document.getElementById(`${pageOptions.type}-card-${lists.processedList.length - 1}`);
|
||||
else el = document.getElementById(`${pageOptions.type}-card-${indexStart + pageOptions.itemsPerPage - 1}`);
|
||||
}
|
||||
|
||||
if (el) {
|
||||
@ -66,7 +57,20 @@
|
||||
|
||||
let isOpen = {};
|
||||
|
||||
function toggleCard(index: string) {
|
||||
function toggleCard(index: string, count: number) {
|
||||
|
||||
if (pageOptions.randomized) {
|
||||
let newIndex = index + '-' + pageOptions.currentPage + '-' + count;
|
||||
|
||||
isOpen[newIndex] = isOpen[newIndex] || {};
|
||||
if (isOpen[newIndex] === true) {
|
||||
isOpen[newIndex] = false;
|
||||
} else {
|
||||
isOpen[newIndex] = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
isOpen[index] = isOpen[index] || {};
|
||||
if (isOpen[index] === true) {
|
||||
isOpen[index] = false;
|
||||
@ -78,8 +82,8 @@
|
||||
function getShortLink(id: string) {
|
||||
let url = "https://pk.mt"
|
||||
|
||||
if (itemType === "member") url += "/m/"
|
||||
else if (itemType === "group") url += "/g/"
|
||||
if (pageOptions.type === "member") url += "/m/"
|
||||
else if (pageOptions.type === "group") url += "/g/"
|
||||
|
||||
url += id;
|
||||
|
||||
@ -108,18 +112,18 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !openByDefault && (settings && settings.accessibility ? (!settings.accessibility.expandedcards && !settings.accessibility.pagelinks) : true)}
|
||||
{#if (settings && settings.accessibility ? (!settings.accessibility.expandedcards && !settings.accessibility.pagelinks) : true)}
|
||||
<div class="mb-3 accordion">
|
||||
{#each list as item, index (item.uuid)}
|
||||
{#each lists.currentPage as item, index (pageOptions.randomized ? item.uuid + '-' + index : item.uuid)}
|
||||
<Card style="border-radius: 0;">
|
||||
<h2 class="accordion-header">
|
||||
<button class="w-100 accordion-button collapsed bg-transparent" id={`${itemType}-card-${indexStart + index}`} on:click={() => toggleCard(item.uuid)} on:keydown={(e) => skipToNextItem(e, indexStart + index)}>
|
||||
<CardsHeader {item} {sortBy} {searchBy}>
|
||||
<div slot="icon" style="cursor: pointer;" id={`${itemType}-copy-${item.id}-${indexStart + index}`} on:click|stopPropagation={() => copyShortLink(indexStart + index, item.id)} on:keydown={(e) => copyShortLink(indexStart + index, item.id, e)} tabindex={0} >
|
||||
{#if isPublic || item.privacy.visibility === "public"}
|
||||
{#if itemType === "member"}
|
||||
<button class="w-100 accordion-button collapsed bg-transparent" id={`${pageOptions.type}-card-${indexStart + index}`} on:click={() => toggleCard(item.uuid, index)} on:keydown={(e) => skipToNextItem(e, indexStart + index)}>
|
||||
<CardsHeader {item} sortBy={options.sort}>
|
||||
<div slot="icon" style="cursor: pointer;" id={`${pageOptions.type}-copy-${item.id}-${indexStart + index}`} on:click|stopPropagation={() => copyShortLink(indexStart + index, item.id)} on:keydown={(e) => copyShortLink(indexStart + index, item.id, e)} tabindex={0} >
|
||||
{#if item.privacy && item.privacy.visibility === "public"}
|
||||
{#if pageOptions.type === "member"}
|
||||
<FaUserCircle />
|
||||
{:else if itemType === "group"}
|
||||
{:else if pageOptions.type === "group"}
|
||||
<FaUsers />
|
||||
{/if}
|
||||
{:else}
|
||||
@ -127,32 +131,32 @@
|
||||
{/if}
|
||||
</div>
|
||||
</CardsHeader>
|
||||
<Tooltip placement="top" target={`${itemType}-copy-${item.id}-${indexStart + index}`}>{copiedArray[indexStart + index] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
<Tooltip placement="top" target={`${pageOptions.type}-copy-${item.id}-${indexStart + index}`}>{copiedArray[indexStart + index] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</button>
|
||||
</h2>
|
||||
<Collapse isOpen={isOpen[item.uuid]}>
|
||||
<Collapse isOpen={pageOptions.randomized ? isOpen[item.uuid + '-' + pageOptions.currentPage + '-' + index] : isOpen[item.uuid]}>
|
||||
<CardBody class="border-top">
|
||||
{#if itemType === "member"}
|
||||
<MemberBody on:update on:deletion bind:isPublic groups={groups} member={item} />
|
||||
{:else if itemType === "group"}
|
||||
<GroupBody on:update on:deletion bind:isPublic {members} group={item} />
|
||||
{#if pageOptions.type === "member"}
|
||||
<MemberBody on:update on:deletion bind:isPublic={pageOptions.isPublic} groups={otherList.rawList} member={item} />
|
||||
{:else if pageOptions.type === "group"}
|
||||
<GroupBody on:update on:deletion bind:isPublic={pageOptions.isPublic} members={otherList.rawList} group={item} />
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Collapse>
|
||||
</Card>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if openByDefault || settings.accessibility.expandedcards}
|
||||
{#each list as item, index (item.id + index)}
|
||||
{:else if settings.accessibility.expandedcards}
|
||||
{#each lists.currentPage as item, index (pageOptions.randomized ? item.uuid + '-' + index : item.uuid)}
|
||||
<Card class="mb-3">
|
||||
<div class="accordion-button collapsed p-0" id={`${itemType}-card-${indexStart + index}`} on:keydown={(e) => skipToNextItem(e, indexStart + index)} tabindex={0}>
|
||||
<div class="accordion-button collapsed p-0" id={`${pageOptions.type}-card-${indexStart + index}`} on:keydown={(e) => skipToNextItem(e, indexStart + index)} tabindex={0}>
|
||||
<CardHeader class="w-100">
|
||||
<CardsHeader {item} {sortBy} {searchBy}>
|
||||
<div slot="icon" style="cursor: pointer;" id={`${itemType}-copy-${item.id}-${indexStart + index}`} on:click|stopPropagation={() => copyShortLink(indexStart + index, item.id)} on:keydown|stopPropagation={(e) => copyShortLink(indexStart + index, item.id, e)} tabindex={0} >
|
||||
{#if isPublic || item.privacy.visibility === "public"}
|
||||
{#if itemType === "member"}
|
||||
<CardsHeader {item} sortBy={options.sort}>
|
||||
<div slot="icon" style="cursor: pointer;" id={`${pageOptions.type}-copy-${item.id}-${indexStart + index}`} on:click|stopPropagation={() => copyShortLink(indexStart + index, item.id)} on:keydown|stopPropagation={(e) => copyShortLink(indexStart + index, item.id, e)} tabindex={0} >
|
||||
{#if item.privacy && item.privacy.visibility === "public"}
|
||||
{#if pageOptions.type === "member"}
|
||||
<FaUserCircle />
|
||||
{:else if itemType === "group"}
|
||||
{:else if pageOptions.type === "group"}
|
||||
<FaUsers />
|
||||
{/if}
|
||||
{:else}
|
||||
@ -160,29 +164,29 @@
|
||||
{/if}
|
||||
</div>
|
||||
</CardsHeader>
|
||||
<Tooltip placement="top" target={`${itemType}-copy-${item.id}-${indexStart + index}`}>{copiedArray[indexStart + index] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
<Tooltip placement="top" target={`${pageOptions.type}-copy-${item.id}-${indexStart + index}`}>{copiedArray[indexStart + index] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</CardHeader>
|
||||
</div>
|
||||
<CardBody>
|
||||
{#if itemType === "member"}
|
||||
<MemberBody on:update on:deletion bind:isPublic groups={groups} member={item} />
|
||||
{:else if itemType === "group"}
|
||||
<GroupBody on:update on:deletion bind:isPublic {members} group={item} />
|
||||
{#if pageOptions.type === "member"}
|
||||
<MemberBody on:update on:deletion bind:isPublic={pageOptions.isPublic} groups={otherList.rawList} member={item} />
|
||||
{:else if pageOptions.type === "group"}
|
||||
<GroupBody on:update on:deletion bind:isPublic={pageOptions.isPublic} members={otherList.rawList} group={item} />
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="my-3">
|
||||
{#each list as item, index (item.id + index)}
|
||||
{#each lists.currentPage as item, index(pageOptions.randomized ? item.uuid + '-' + index : item.uuid)}
|
||||
<Card style="border-radius: 0;">
|
||||
<a class="accordion-button collapsed bg-transparent" style="text-decoration: none;" href={getItemLink(item)} id={`${itemType}-card-${indexStart + index}`} on:keydown={(e) => skipToNextItem(e, indexStart + index)} use:link >
|
||||
<CardsHeader {item}>
|
||||
<div slot="icon" style="cursor: pointer;" id={`${itemType}-copy-${item.id}-${indexStart + index}`} on:click|stopPropagation={() => copyShortLink(indexStart + index, item.id)} on:keydown|stopPropagation={(e) => copyShortLink(indexStart + index, item.id, e)} tabindex={0} >
|
||||
{#if isPublic || item.privacy.visibility === "public"}
|
||||
{#if itemType === "member"}
|
||||
<a class="accordion-button collapsed bg-transparent" style="text-decoration: none;" href={getItemLink(item)} id={`${pageOptions.type}-card-${indexStart + index}`} on:keydown={(e) => skipToNextItem(e, indexStart + index)} use:link >
|
||||
<CardsHeader {item} sortBy={options.sort}>
|
||||
<div slot="icon" style="cursor: pointer;" id={`${pageOptions.type}-copy-${item.id}-${indexStart + index}`} on:click|stopPropagation={() => copyShortLink(indexStart + index, item.id)} on:keydown|stopPropagation={(e) => copyShortLink(indexStart + index, item.id, e)} tabindex={0} >
|
||||
{#if item.privacy && item.privacy.visibility === "public"}
|
||||
{#if pageOptions.type === "member"}
|
||||
<FaUserCircle />
|
||||
{:else if itemType === "group"}
|
||||
{:else if pageOptions.type === "group"}
|
||||
<FaUsers />
|
||||
{/if}
|
||||
{:else}
|
||||
@ -190,7 +194,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</CardsHeader>
|
||||
<Tooltip placement="top" target={`${itemType}-copy-${item.id}-${indexStart + index}`}>{copiedArray[indexStart + index] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
<Tooltip placement="top" target={`${pageOptions.type}-copy-${item.id}-${indexStart + index}`}>{copiedArray[indexStart + index] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</a>
|
||||
</Card>
|
||||
{/each}
|
||||
|
114
dashboard/src/components/list/PageControl.svelte
Normal file
114
dashboard/src/components/list/PageControl.svelte
Normal file
@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import { Card, CardHeader, CardBody, CardTitle, InputGroupText, InputGroup, Input, Row, Col, Button, Tooltip } from 'sveltestrap';
|
||||
import FaList from 'svelte-icons/fa/FaList.svelte'
|
||||
|
||||
import type { ListOptions, PageOptions } from './types';
|
||||
|
||||
export let options: ListOptions;
|
||||
export let pageOptions: PageOptions;
|
||||
|
||||
let itemsPerPageSelection = {
|
||||
small: 10,
|
||||
default: 25,
|
||||
large: 50
|
||||
}
|
||||
|
||||
$: { if (pageOptions.view === "card") itemsPerPageSelection = {
|
||||
small: 12,
|
||||
default: 24,
|
||||
large: 60
|
||||
}
|
||||
else {
|
||||
itemsPerPageSelection = {
|
||||
small: 10,
|
||||
default: 25,
|
||||
large: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetPage() {
|
||||
pageOptions.currentPage = 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card class="mb-3">
|
||||
<CardHeader>
|
||||
<CardTitle class="d-flex justify-content-between align-items-center mb-0 py-2">
|
||||
<div>
|
||||
<div class="icon d-inline-block">
|
||||
<FaList />
|
||||
</div> {pageOptions.type === 'group' ? 'Member groups' : 'Group members'}
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Row class="mb-3">
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Page length</InputGroupText>
|
||||
<Input bind:value={pageOptions.itemsPerPage} type="select" aria-label="page length" on:change={() => resetPage()}>
|
||||
<option>{itemsPerPageSelection.small}</option>
|
||||
<option>{itemsPerPageSelection.default}</option>
|
||||
<option>{itemsPerPageSelection.large}</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<!-- <Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Sort by</InputGroupText>
|
||||
<Input bind:value={options.sort} type="select">
|
||||
<option value="name">Name</option>
|
||||
<option value="display_name">Display name</option>
|
||||
<option value="id">ID</option>
|
||||
{#if pageOptions.type === 'member'}
|
||||
<option value="pronouns">Pronouns</option>
|
||||
{/if}
|
||||
<option value="none">API response order</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col> -->
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Order</InputGroupText>
|
||||
<Input bind:value={options.order} type="select">
|
||||
<option value="ascending">Ascending</option>
|
||||
<option value="descending">Descending</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<!-- <Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>Only show</InputGroupText>
|
||||
<Input bind:value={options.show} type="select" aria-label="view mode" on:change={() => resetPage()}>
|
||||
<option value="all">All {pageOptions.type}s</option>
|
||||
<option value="public">Public {pageOptions.type}s</option>
|
||||
<option value="private">Private {pageOptions.type}s</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col> -->
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<InputGroup>
|
||||
<InputGroupText>View mode</InputGroupText>
|
||||
<Input bind:value={pageOptions.view} type="select" aria-label="view mode">
|
||||
<option value="list">List</option>
|
||||
<option value="card">Cards</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={12} class="mb-2">
|
||||
<InputGroup class="mb-2">
|
||||
<InputGroupText>Name</InputGroupText>
|
||||
<Input
|
||||
style="resize: none; overflow: hidden;"
|
||||
rows={1} type="textarea"
|
||||
bind:value={options.search.name}
|
||||
on:keydown={() => resetPage()}
|
||||
placeholder="Search by name..."/>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBody>
|
||||
</Card>
|
162
dashboard/src/components/list/functions.ts
Normal file
162
dashboard/src/components/list/functions.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import type { Group, Member } from '../../api/types';
|
||||
import type { ListOptions, PageOptions } from './types';
|
||||
|
||||
export function filterList<T extends Member|Group>(list: T[], options: ListOptions, type?: string): T[] {
|
||||
let searchedList = search(list, options);
|
||||
let groupedList = [...searchedList];
|
||||
if (type)
|
||||
groupedList = group(searchedList, options, type);
|
||||
let filteredList = filter(groupedList, options);
|
||||
let sortedList = sort(filteredList, options);
|
||||
let orderedList = reorder(sortedList, options);
|
||||
|
||||
return orderedList;
|
||||
}
|
||||
|
||||
export function paginateList<T extends Member|Group>(list: T[], options: PageOptions): T[] {
|
||||
let indexLast = options.currentPage * options.itemsPerPage;
|
||||
let indexFirst = indexLast - options.itemsPerPage;
|
||||
return list.slice(indexFirst, indexLast);
|
||||
}
|
||||
|
||||
export function getPageAmount<T extends Member|Group>(list: T[], options: PageOptions): number {
|
||||
return Math.ceil(list.length / options.itemsPerPage);
|
||||
}
|
||||
|
||||
export function createShortList<T extends Member|Group>(list: T[]) {
|
||||
return list.map(function(item) {
|
||||
return {
|
||||
name: item.name,
|
||||
shortid: item.id,
|
||||
id: item.uuid,
|
||||
members: (item as Group).members,
|
||||
display_name: item.display_name};
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
function search<T extends Member|Group>(list: T[], options: ListOptions): T[] {
|
||||
Object.keys(options.search).forEach(x => {
|
||||
if (options.search[x]) {
|
||||
if (options.searchMode[x] !== false)
|
||||
list = list.filter(item => item[x] ? item[x].toLowerCase().includes(options.search[x].toLowerCase()) : false);
|
||||
else list = list.filter(item => item[x] ? !item[x].toLowerCase().includes(options.search[x].toLowerCase()) : true);
|
||||
}
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function filter<T extends Member|Group>(list: T[], options: ListOptions): T[] {
|
||||
let newList = [...list];
|
||||
Object.keys(options.filter).forEach(x => {
|
||||
if (options.filter[x] === 'include') {
|
||||
newList = [...list].filter(item => item[x] && true);
|
||||
} else if (options.filter[x] === 'exclude') {
|
||||
newList = [...list].filter(item => !item[x] && true)
|
||||
}
|
||||
});
|
||||
|
||||
let anotherList = [...newList];
|
||||
|
||||
if (options.show === 'private') {
|
||||
anotherList = [...newList].filter(item => item.privacy && item.privacy.visibility === 'private');
|
||||
} else if (options.show === 'public') {
|
||||
anotherList = [...newList].filter(item => item.privacy && item.privacy.visibility === 'public');
|
||||
}
|
||||
|
||||
return anotherList;
|
||||
}
|
||||
|
||||
function sort<T extends Member|Group>(list: T[], options: ListOptions): T[] {
|
||||
if (options.sort === 'none')
|
||||
return list;
|
||||
|
||||
let newList: T[] = [];
|
||||
if (options.sort && options.sort === 'display_name' || options.sort === 'name' || options.sort === 'id') {
|
||||
newList = [...list].sort((a, b) => {
|
||||
let aa = a[options.sort] || a.name;
|
||||
let bb = b[options.sort] || b.name;
|
||||
return aa.localeCompare(bb);
|
||||
});
|
||||
} else if (options.sort === 'pronouns') {
|
||||
newList = [...list].sort((a, b) => {
|
||||
let aa = (a as Member).pronouns;
|
||||
let bb = (b as Member).pronouns;
|
||||
if (aa === bb) return a.name.localeCompare(b.name);
|
||||
if (aa === null) return 1;
|
||||
if (bb === null) return -1;
|
||||
return aa.localeCompare(bb);
|
||||
});
|
||||
}
|
||||
return newList;
|
||||
}
|
||||
|
||||
function group<T extends Member|Group>(list: T[], options: ListOptions, type?: string): T[] {
|
||||
let groupIncludedList = [...list];
|
||||
|
||||
if (options.groups.include.list.length > 0) {
|
||||
// include has items! check the type and whether to match exactly
|
||||
if (type === 'member')
|
||||
if (options.groups.include.exact === true)
|
||||
// match exact, include only members in EVERY group
|
||||
groupIncludedList = [...list].filter(m =>
|
||||
(options.groups.include.list as Group[]).every(g => g.members?.includes(m.uuid))
|
||||
);
|
||||
else
|
||||
// just include any member in at least one group
|
||||
groupIncludedList = [...list].filter(m =>
|
||||
(options.groups.include.list as Group[]).some(g => g.members?.includes(m.uuid))
|
||||
);
|
||||
|
||||
else if (type === 'group')
|
||||
if (options.groups.include.exact === true)
|
||||
groupIncludedList = [...list].filter(g =>
|
||||
(g as Group).members && (options.groups.include.list as Member[])
|
||||
.every(m => (g as Group).members.includes(m.id))
|
||||
);
|
||||
else
|
||||
groupIncludedList = [...list].filter(g =>
|
||||
(g as Group).members && (options.groups.include.list as Member[])
|
||||
.some(m => (g as Group).members.includes(m.id))
|
||||
);
|
||||
}
|
||||
|
||||
let groupExcludedList = [...groupIncludedList];
|
||||
|
||||
if (options.groups.exclude.list.length > 0) {
|
||||
if (type === 'member')
|
||||
if (options.groups.exclude.exact === true)
|
||||
groupExcludedList = [...groupIncludedList].filter(m =>
|
||||
!(options.groups.exclude.list as Group[]).every(g => g.members?.includes(m.uuid))
|
||||
);
|
||||
else
|
||||
groupExcludedList = [...groupIncludedList].filter(m =>
|
||||
!(options.groups.exclude.list as Group[]).some(g => g.members?.includes(m.uuid))
|
||||
);
|
||||
|
||||
else if (type === 'group')
|
||||
if (options.groups.exclude.exact === true)
|
||||
groupExcludedList = [...groupIncludedList].filter(g =>
|
||||
(g as Group).members && (options.groups.exclude.list as Member[])
|
||||
.some(m => !(g as Group).members.includes(m.id))
|
||||
);
|
||||
else
|
||||
groupExcludedList = [...groupIncludedList].filter(g =>
|
||||
(g as Group).members && (options.groups.exclude.list as Member[])
|
||||
.every(m => !(g as Group).members.includes(m.id))
|
||||
);
|
||||
}
|
||||
|
||||
return groupExcludedList;
|
||||
}
|
||||
|
||||
function reorder<T extends Member|Group>(list: T[], options: ListOptions): T[] {
|
||||
if (options.order === 'descending') {
|
||||
return [...list].reverse();
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
130
dashboard/src/components/list/types.ts
Normal file
130
dashboard/src/components/list/types.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import type { Group, Member } from '../../api/types';
|
||||
|
||||
export interface ListOptions {
|
||||
// search and filter based on different things
|
||||
// TODO: allow for multiple kinds of searches together
|
||||
search: {
|
||||
name?: string,
|
||||
description?: string,
|
||||
id?: string,
|
||||
pronouns?: string,
|
||||
display_name?: string,
|
||||
},
|
||||
|
||||
// true: include only items with this string
|
||||
// false: exclude all items with this string
|
||||
searchMode: {
|
||||
name?: boolean,
|
||||
description?: boolean,
|
||||
id?: boolean,
|
||||
pronouns?: boolean,
|
||||
display_name?: boolean,
|
||||
}
|
||||
// filtering members based on what group they're in (and vice versa)
|
||||
groups: {
|
||||
// arrays so we can really fine-tune the combinations of groups
|
||||
include: {
|
||||
exact: boolean, // only include members who are in ALL groups
|
||||
list: [],
|
||||
},
|
||||
exclude: {
|
||||
exact: boolean, // only exclude members who are in ALL groups
|
||||
list: []
|
||||
},
|
||||
none: boolean
|
||||
},
|
||||
// filter members based on whether fields have a value set or not
|
||||
// if set to true: only include items with a value
|
||||
// if set to false: exclude anything with a value
|
||||
// if null: don't filter
|
||||
filter: {
|
||||
description: "all"|"include"|"exclude",
|
||||
birthday: "all"|"include"|"exclude",
|
||||
pronouns: "all"|"include"|"exclude",
|
||||
display_name: "all"|"include"|"exclude",
|
||||
avatar_url: "all"|"include"|"exclude",
|
||||
icon: "all"|"include"|"exclude",
|
||||
|
||||
}
|
||||
// what it says on the tin
|
||||
sort: 'name'|'description'|'birthday'|'pronouns'|'display_name'|'id'|'none',
|
||||
order: "ascending"|"descending",
|
||||
show: "all"|"private"|"public",
|
||||
// so we can change the key for duplicate members on the randomize page
|
||||
}
|
||||
|
||||
export interface PageOptions {
|
||||
// changes availability of certain buttons
|
||||
isPublic: boolean, // is this list on the public section?
|
||||
isMain: boolean, // is this list on the main dasbhoard, or on another page?
|
||||
currentPage: number,
|
||||
itemsPerPage: number,
|
||||
pageAmount: number,
|
||||
view: string,
|
||||
randomized: boolean,
|
||||
type: 'member'|'group'
|
||||
}
|
||||
|
||||
export interface ShortList {
|
||||
name: string,
|
||||
shortid: string,
|
||||
id: string,
|
||||
members?: string[],
|
||||
display_name: string,
|
||||
}
|
||||
|
||||
export interface List<T extends Member|Group> {
|
||||
rawList: T[], // the raw list from the API
|
||||
processedList: T[], // the list after sorting and filtering
|
||||
currentPage: T[], // the slice that represents the current page,
|
||||
|
||||
// for svelecte specifically (member/group selection)
|
||||
shortGroups: ShortList[],
|
||||
shortMembers: ShortList[],
|
||||
}
|
||||
|
||||
export const defaultListOptions: ListOptions = {
|
||||
search: {},
|
||||
groups: {
|
||||
none: false,
|
||||
include: {
|
||||
exact: false,
|
||||
list: []
|
||||
},
|
||||
exclude: {
|
||||
exact: false,
|
||||
list: []
|
||||
}
|
||||
},
|
||||
searchMode: {
|
||||
name: true,
|
||||
display_name: true,
|
||||
description: true,
|
||||
pronouns: true,
|
||||
id: true
|
||||
},
|
||||
filter: {
|
||||
display_name: 'all',
|
||||
birthday: 'all',
|
||||
description: 'all',
|
||||
pronouns: 'all',
|
||||
avatar_url: 'all',
|
||||
icon: 'all'
|
||||
|
||||
},
|
||||
sort: 'name',
|
||||
order: 'ascending',
|
||||
show: 'all'
|
||||
}
|
||||
|
||||
export const defaultPageOptions: PageOptions = {
|
||||
isPublic: true,
|
||||
isMain: true,
|
||||
currentPage: 1,
|
||||
itemsPerPage: 25,
|
||||
pageAmount: 1,
|
||||
view: "list",
|
||||
randomized: false,
|
||||
type: 'member'
|
||||
}
|
||||
|
@ -93,7 +93,7 @@
|
||||
try {
|
||||
let res = await api().members(member.id).patch({data});
|
||||
update({...member, ...res});
|
||||
success = true;
|
||||
view = 'card';
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err.push(error.message);
|
||||
@ -111,7 +111,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card class="mb-4 pb-3" style={`height: 27.5rem; ${member.color && `border-bottom: 4px solid #${member.color}`}`}>
|
||||
<div class="mb-4 pb-3 card item" style={`${member.color && `border-bottom: 4px solid #${member.color}`}`}>
|
||||
<CardHeader>
|
||||
<CardTitle class="d-flex justify-content-center align-middle w-100 mb-0">
|
||||
<div class="icon d-inline-block">
|
||||
@ -141,13 +141,13 @@
|
||||
<hr style="min-height: 1px;"/>
|
||||
<Row>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
{#if !isPublic}<Button color="link" class="mt-2" on:click={() => {view = "edit"}} id={`member-${member.uuid}-edit-button-card`}><FaEdit/></Button>{/if}
|
||||
{#if !isPublic}<Button color="link" class="mt-2" style="height: 3.5rem;" on:click={() => {view = "edit"}} id={`member-${member.uuid}-edit-button-card`}><FaEdit/></Button>{/if}
|
||||
</Col>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
{#if !isPublic && isDash}<Button color="link" class="mt-2 text-reset" on:click={() => {view = "groups"}} id={`member-${member.uuid}-groups-button-card`}><FaUsers/></Button>{/if}
|
||||
{#if !isPublic && isDash}<Button color="link" class="mt-2 text-reset" style="height: 3.5rem;" on:click={() => {view = "groups"}} id={`member-${member.uuid}-groups-button-card`}><FaUsers/></Button>{/if}
|
||||
</Col>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
<Link tabindex={-1} to={pageLink} class="text-reset"><Button color="link" class="mt-2 w-100 text-reset" id={`member-${member.uuid}-view-button-card`}><FaInfoCircle/></Button></Link>
|
||||
<Link tabindex={-1} to={pageLink} class="text-reset"><Button color="link" class="mt-2 w-100 text-reset" style="height: 3.5rem;" id={`member-${member.uuid}-view-button-card`}><FaInfoCircle/></Button></Link>
|
||||
</Col>
|
||||
{#if !isPublic}<Tooltip target={`member-${member.uuid}-edit-button-card`} placement="bottom">Edit member</Tooltip>{/if}
|
||||
{#if !isPublic && isDash}<Tooltip target={`member-${member.uuid}-groups-button-card`} placement="bottom">View groups</Tooltip>{/if}
|
||||
@ -168,13 +168,13 @@
|
||||
<hr style="min-height: 1px"/>
|
||||
<Row>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
<Button color="link" class="mt-2" on:click={() => {view = "edit"}} id={`member-${member.uuid}-edit-button-groups`}><FaEdit/></Button>
|
||||
<Button color="link" class="mt-2" style="height: 3.5rem;" on:click={() => {view = "edit"}} id={`member-${member.uuid}-edit-button-groups`}><FaEdit/></Button>
|
||||
</Col>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
<Button color="link" class="mt-2 text-reset" on:click={() => {view = "card"}} id={`member-${member.uuid}-back-button-groups`}><FaTimes/></Button>
|
||||
<Button color="link" class="mt-2 text-reset" style="height: 3.5rem;" on:click={() => {view = "card"}} id={`member-${member.uuid}-back-button-groups`}><FaTimes/></Button>
|
||||
</Col>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
<Link tabindex={-1} to={`./m/${member.id}`} class="text-reset"><Button color="link" class="mt-2 w-100 text-reset" id={`member-${member.uuid}-view-button-groups`}><FaInfoCircle/></Button></Link>
|
||||
<Link tabindex={-1} to={`./m/${member.id}`} class="text-reset"><Button color="link" class="mt-2 w-100 text-reset" style="height: 3.5rem;" id={`member-${member.uuid}-view-button-groups`}><FaInfoCircle/></Button></Link>
|
||||
</Col>
|
||||
<Tooltip target={`member-${member.uuid}-edit-button-groups`} placement="bottom">Edit member</Tooltip>
|
||||
<Tooltip target={`member-${member.uuid}-back-button-groups`} placement="bottom">Back to info</Tooltip>
|
||||
@ -204,9 +204,6 @@
|
||||
</Col>
|
||||
</Row>
|
||||
<hr style="min-height: 1px" />
|
||||
{#if success}
|
||||
<Alert class="m-0 mb-2" color="success">Member edited!</Alert>
|
||||
{/if}
|
||||
{#if err}
|
||||
{#each err as errorMessage}
|
||||
<Alert class="m-0 mb-2" color="danger">{@html errorMessage}</Alert>
|
||||
@ -214,19 +211,19 @@
|
||||
{/if}
|
||||
<Row>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
<Button disabled={loading} color="link" class="mt-2 text-danger" style="height: 3rem;" on:click={() => {view = "card"}} id={`member-${member.uuid}-back-button-edit`}><FaTimes/></Button>
|
||||
<Button disabled={loading} color="link" class="mt-2 text-danger" style="height: 3.5rem;" on:click={() => {view = "card"}} id={`member-${member.uuid}-back-button-edit`}><FaTimes/></Button>
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
</Col>
|
||||
<Col xs={4} class="align-items-center justify-content-center">
|
||||
<Button disabled={loading} color="link" class="mt-2 text-success" on:click={submit} id={`member-${member.uuid}-submit-button-edit`}><FaCheck/></Button>
|
||||
<Button disabled={loading} color="link" class="mt-2 text-success" style="height: 3.5rem;" on:click={submit} id={`member-${member.uuid}-submit-button-edit`}><FaCheck/></Button>
|
||||
</Col>
|
||||
<Tooltip target={`member-${member.uuid}-back-button-edit`} placement="bottom">Go back</Tooltip>
|
||||
<Tooltip target={`member-${member.uuid}-submit-button-edit`} placement="bottom">Submit edit</Tooltip>
|
||||
</Row>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
@ -237,4 +234,13 @@
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.item {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.item {
|
||||
height: 30rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -74,7 +74,8 @@
|
||||
try {
|
||||
let res = await api().members(member.id).patch({data});
|
||||
update({...member, ...res});
|
||||
success = true;
|
||||
editMode = false;
|
||||
return;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err.push(error.message);
|
||||
|
@ -6,8 +6,9 @@
|
||||
import SystemMain from '../../components/system/Main.svelte';
|
||||
import List from '../../components/list/List.svelte';
|
||||
|
||||
import type { System } from '../../api/types';
|
||||
import type { System, Member, Group } from '../../api/types';
|
||||
import api from '../../api';
|
||||
import { defaultListOptions, defaultPageOptions, type List as Lists, type ListOptions, type PageOptions } from '../../components/list/types';
|
||||
|
||||
// get the state from the navigator so that we know which tab to start on
|
||||
let location = useLocation();
|
||||
@ -69,9 +70,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
// some values that get passed from the member to the group components and vice versa
|
||||
let members = [];
|
||||
let groups = [];
|
||||
let memberList: Lists<Member> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
let groupList: Lists<Group> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
let groupListOptions: ListOptions = JSON.parse(JSON.stringify(defaultListOptions));
|
||||
let memberListOptions: ListOptions = JSON.parse(JSON.stringify(defaultListOptions));
|
||||
|
||||
let memberListPageOptions: PageOptions = {...defaultPageOptions,
|
||||
view: listView,
|
||||
isPublic: false,
|
||||
type: 'member'
|
||||
};
|
||||
|
||||
let groupListPageOptions: PageOptions = {...defaultPageOptions,
|
||||
view: listView,
|
||||
isPublic: false,
|
||||
type: 'group'
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@ -89,11 +119,11 @@
|
||||
<SystemMain bind:user={user} isPublic={false} />
|
||||
</TabPane>
|
||||
<TabPane tabId="members" tab="Members" active={tabPane === "members"}>
|
||||
<List on:viewChange={(e) => navigateTo("members", e.detail)} bind:groups={groups} bind:members={members} isPublic={false} itemType={"member"} bind:view={listView} isDash={true} />
|
||||
<List on:viewChange={(e) => navigateTo("members", e.detail)} bind:otherList={groupList} bind:lists={memberList} bind:pageOptions={memberListPageOptions} bind:options={memberListOptions} />
|
||||
</TabPane>
|
||||
<TabPane tabId="groups" tab="Groups" active={tabPane === "groups"}>
|
||||
<List on:viewChange={(e) => navigateTo("members", e.detail)} bind:members={members} bind:groups={groups} isPublic={false} itemType={"group"} bind:view={listView} isDash={true} />
|
||||
</TabPane>
|
||||
<List on:viewChange={(e) => navigateTo("members", e.detail)} bind:otherList={memberList} bind:lists={groupList} bind:pageOptions={groupListPageOptions} bind:options={groupListOptions} />
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -7,10 +7,13 @@
|
||||
import type { Member, Group } from "../../../api/types";
|
||||
import CardsHeader from "../../../components/common/CardsHeader.svelte";
|
||||
import FaUsers from 'svelte-icons/fa/FaUsers.svelte';
|
||||
import FaList from 'svelte-icons/fa/FaList.svelte';
|
||||
import ListPagination from '../../../components/common/ListPagination.svelte';
|
||||
import ListView from '../../../components/list/ListView.svelte';
|
||||
import CardView from '../../../components/list/CardView.svelte';
|
||||
import type { List as Lists, ListOptions, PageOptions } from '../../../components/list/types';
|
||||
import { defaultListOptions, defaultPageOptions } from '../../../components/list/types';
|
||||
import { filterList, paginateList, getPageAmount } from '../../../components/list/functions';
|
||||
import PageControl from "../../../components/list/PageControl.svelte";
|
||||
|
||||
// get the state from the navigator so that we know which tab to start on
|
||||
let location = useLocation();
|
||||
@ -24,27 +27,16 @@
|
||||
let err = "";
|
||||
let memberErr = "";
|
||||
let group: Group;
|
||||
let members: Member[] = [];
|
||||
let systemMembers: Group[] = [];
|
||||
let isDeleted = false;
|
||||
let notOwnSystem = false;
|
||||
let copied = false;
|
||||
let pageAmount = 1;
|
||||
|
||||
const isPage = true;
|
||||
export let isPublic = true;
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
let currentPage = 1;
|
||||
let itemsPerPage = listView === "card" ? 12 : settings && settings.accessibility && settings.accessibility.expandedcards ? 5 : 10;
|
||||
|
||||
$: indexOfLastItem = currentPage * itemsPerPage;
|
||||
$: indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||
$: pageAmount = Math.ceil(members.length / itemsPerPage);
|
||||
|
||||
$: orderedMembers = members.sort((a, b) => a.name.localeCompare(b.name));
|
||||
$: slicedMembers = orderedMembers.slice(indexOfFirstItem, indexOfLastItem);
|
||||
|
||||
if (!isPublic && isPage) {
|
||||
if (!isPublic) {
|
||||
let user = localStorage.getItem("pk-user");
|
||||
if (!user) navigate("/");
|
||||
}
|
||||
@ -68,7 +60,6 @@
|
||||
title = isPublic ? group.name : `${group.name} (dash)`;
|
||||
}
|
||||
memberLoading = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
fetchMembers();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@ -79,10 +70,9 @@
|
||||
|
||||
async function fetchMembers() {
|
||||
try {
|
||||
members = await api().groups($params.id).members().get({auth: !isPublic});
|
||||
group.members = members.map(function(member) {return member.uuid});
|
||||
lists.rawList = await api().groups($params.id).members().get({auth: !isPublic});
|
||||
group.members = lists.rawList.map(function(member) {return member.uuid});
|
||||
if (!isPublic) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
systemMembers = await api().systems("@me").members.get({ auth: true });
|
||||
}
|
||||
memberErr = "";
|
||||
@ -94,25 +84,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function updateMembers() {
|
||||
memberLoading = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
fetchMembers();
|
||||
let lists: Lists<Member> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
function updateDelete() {
|
||||
isDeleted = true;
|
||||
let nope: Lists<Group> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
let listOptions: ListOptions = {...defaultListOptions};
|
||||
|
||||
function updateMemberList(event: any) {
|
||||
members = members.map(member => member.id !== event.detail.id ? member : event.detail);
|
||||
systemMembers = systemMembers.map(member => member.id !== event.detail.id ? member : event.detail);
|
||||
}
|
||||
|
||||
function deleteMemberFromList(event: any) {
|
||||
members = members.filter(member => member.id !== event.detail);
|
||||
systemMembers = systemMembers.filter(member => member.id !== event.detail);
|
||||
}
|
||||
let pageOptions: PageOptions = {...defaultPageOptions,
|
||||
view: listView,
|
||||
isPublic: isPublic,
|
||||
type: 'member',
|
||||
isMain: false,
|
||||
itemsPerPage: listView === 'card' ? 24 : 25
|
||||
};
|
||||
|
||||
async function copyShortLink(event?) {
|
||||
if (event) {
|
||||
@ -128,6 +126,19 @@
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
$: lists.processedList = filterList(lists.rawList, listOptions);
|
||||
$: lists.currentPage = paginateList(lists.processedList, pageOptions);
|
||||
$: pageAmount = getPageAmount(lists.processedList, pageOptions);
|
||||
|
||||
|
||||
function updateDelete(event: any) {
|
||||
lists.rawList = lists.rawList.filter(m => m.id !== event.detail);
|
||||
}
|
||||
|
||||
function update(event: any) {
|
||||
lists.rawList = lists.rawList.map(m => m.id === event.detail.id ? m = event.detail : m);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if settings && settings.appearance.color_background && !notOwnSystem}
|
||||
@ -163,7 +174,7 @@
|
||||
<Tooltip placement="top" target={`group-copy-${group.id}`}>{copied ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Body on:deletion={updateDelete} on:updateMembers={updateMembers} bind:members={systemMembers} bind:group={group} isPage={isPage} isPublic={isPublic}/>
|
||||
<Body bind:members={systemMembers} bind:group={group} isPage={true} isPublic={isPublic}/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
@ -171,23 +182,15 @@
|
||||
<Alert color="primary"><Spinner size="sm" /> Fetching members...</Alert>
|
||||
{:else if memberErr}
|
||||
<Alert color="danger">{memberErr}</Alert>
|
||||
{:else if members && members.length > 0}
|
||||
<Card class="mb-2">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaList />
|
||||
</div> Group list
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<ListPagination bind:currentPage bind:pageAmount />
|
||||
{#if listView === "card"}
|
||||
<CardView list={slicedMembers} {isPublic} itemType="member" isDash={false} />
|
||||
{:else if lists.rawList && lists.rawList.length > 0}
|
||||
<PageControl bind:options={listOptions} bind:pageOptions />
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
{#if pageOptions.view === "card"}
|
||||
<CardView {pageOptions} {lists} otherList={nope} on:update={update} />
|
||||
{:else}
|
||||
<ListView on:deletion={(e) => deleteMemberFromList(e)} bind:list={slicedMembers} isPublic={isPublic} itemType="member" itemsPerPage={itemsPerPage} currentPage={currentPage} fullLength={members.length} />
|
||||
<ListPagination bind:currentPage bind:pageAmount />
|
||||
<ListView {pageOptions} {lists} otherList={nope} on:update={update} on:deletion={updateDelete} />
|
||||
{/if}
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
{/if}
|
||||
{/if}
|
||||
</Col>
|
||||
|
@ -8,9 +8,12 @@
|
||||
import type { Member, Group } from "../../../api/types";
|
||||
import CardsHeader from "../../../components/common/CardsHeader.svelte";
|
||||
import FaAddressCard from 'svelte-icons/fa/FaAddressCard.svelte'
|
||||
import FaList from 'svelte-icons/fa/FaList.svelte'
|
||||
import ListPagination from '../../../components/common/ListPagination.svelte';
|
||||
import CardView from '../../../components/list/CardView.svelte';
|
||||
import type { List as Lists, ListOptions, PageOptions } from '../../../components/list/types';
|
||||
import { defaultListOptions, defaultPageOptions } from '../../../components/list/types';
|
||||
import { filterList, getPageAmount, paginateList } from '../../../components/list/functions';
|
||||
import PageControl from "../../../components/list/PageControl.svelte";
|
||||
|
||||
// get the state from the navigator so that we know which tab to start on
|
||||
let location = useLocation();
|
||||
@ -24,28 +27,16 @@
|
||||
let err = "";
|
||||
let groupErr = "";
|
||||
let member: Member;
|
||||
let groups: Group[] = [];
|
||||
let systemGroups: Group[] = [];
|
||||
let systemMembers: Member[] = [];
|
||||
let isDeleted = false;
|
||||
let notOwnSystem = false;
|
||||
let copied = false;
|
||||
let pageAmount = 1;
|
||||
|
||||
const isPage = true;
|
||||
export let isPublic = true;
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
let currentPage = 1;
|
||||
let itemsPerPage = listView === "card" ? 12 : settings && settings.accessibility && settings.accessibility.expandedcards ? 5 : 10;
|
||||
|
||||
$: indexOfLastItem = currentPage * itemsPerPage;
|
||||
$: indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||
$: pageAmount = Math.ceil(groups.length / itemsPerPage);
|
||||
|
||||
$: orderedGroups = groups.sort((a, b) => a.name.localeCompare(b.name));
|
||||
$: slicedGroups = orderedGroups.slice(indexOfFirstItem, indexOfLastItem);
|
||||
|
||||
if (!isPublic && isPage) {
|
||||
if (!isPublic) {
|
||||
let user = localStorage.getItem("pk-user");
|
||||
if (!user) navigate("/");
|
||||
}
|
||||
@ -69,7 +60,6 @@
|
||||
title = isPublic ? member.name : `${member.name} (dash)`;
|
||||
}
|
||||
groupLoading = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
fetchGroups();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@ -80,19 +70,12 @@
|
||||
|
||||
async function fetchGroups() {
|
||||
try {
|
||||
groups = await api().members($params.id).groups().get({auth: !isPublic, query: { with_members: !isPublic } });
|
||||
lists.rawList = await api().members($params.id).groups().get({auth: !isPublic, query: { with_members: !isPublic } });
|
||||
if (!isPublic) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
systemGroups = await api().systems("@me").groups.get({ auth: true, query: { with_members: true } });
|
||||
}
|
||||
groupErr = "";
|
||||
groupLoading = false;
|
||||
// we can't use with_members from a group list from a member endpoint yet, but I'm leaving this in in case we do
|
||||
// (this is needed for editing a group member list from the member page)
|
||||
/* if (!isPublic) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
systemMembers = await api().systems("@me").members.get({auth: true});
|
||||
} */
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
groupErr = error.message;
|
||||
@ -100,20 +83,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function updateGroups() {
|
||||
groupLoading = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
fetchGroups();
|
||||
let lists: Lists<Group> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
function updateDelete() {
|
||||
isDeleted = true;
|
||||
let nope: Lists<Member> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
let listOptions: ListOptions = {...defaultListOptions};
|
||||
|
||||
function deleteGroupFromList(event: any) {
|
||||
groups = groups.filter(group => group.id !== event.detail);
|
||||
systemGroups = systemGroups.filter(group => group.id !== event.detail);
|
||||
}
|
||||
let pageOptions: PageOptions = {...defaultPageOptions,
|
||||
view: listView,
|
||||
isPublic: isPublic,
|
||||
type: 'group',
|
||||
isMain: false,
|
||||
itemsPerPage: listView === 'card' ? 24 : 25
|
||||
};
|
||||
|
||||
async function copyShortLink(event?) {
|
||||
if (event) {
|
||||
@ -129,6 +125,18 @@
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
$: lists.processedList = filterList(lists.rawList, listOptions);
|
||||
$: lists.currentPage = paginateList(lists.processedList, pageOptions);
|
||||
$: pageAmount = getPageAmount(lists.processedList, pageOptions);
|
||||
|
||||
function updateDelete(event: any) {
|
||||
lists.rawList = lists.rawList.filter(m => m.id !== event.detail);
|
||||
}
|
||||
|
||||
function update(event: any) {
|
||||
lists.rawList = lists.rawList.map(m => m.id === event.detail.id ? m = event.detail : m);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if settings && settings.appearance.color_background && !notOwnSystem}
|
||||
@ -164,7 +172,7 @@
|
||||
<Tooltip placement="top" target={`member-copy-${member.id}`}>{copied ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Body on:deletion={updateDelete} on:updateGroups={updateGroups} bind:groups={systemGroups} bind:member={member} isPage={isPage} isPublic={isPublic}/>
|
||||
<Body bind:groups={systemGroups} bind:member={member} isPage={true} isPublic={isPublic}/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
@ -172,23 +180,15 @@
|
||||
<Alert color="primary"><Spinner size="sm" /> Fetching groups...</Alert>
|
||||
{:else if groupErr}
|
||||
<Alert color="danger">{groupErr}</Alert>
|
||||
{:else if groups && groups.length > 0}
|
||||
<Card class="mb-2">
|
||||
<CardHeader>
|
||||
<CardTitle style="margin-top: 8px; outline: none;">
|
||||
<div class="icon d-inline-block">
|
||||
<FaList />
|
||||
</div> Member groups
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<ListPagination bind:currentPage bind:pageAmount />
|
||||
{#if listView === "card"}
|
||||
<CardView list={slicedGroups} {isPublic} itemType="group" isDash={false} />
|
||||
{:else if lists.rawList && lists.rawList.length > 0}
|
||||
<PageControl bind:options={listOptions} bind:pageOptions />
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
{#if pageOptions.view === "card"}
|
||||
<CardView {pageOptions} {lists} otherList={nope} on:update={update} />
|
||||
{:else}
|
||||
<ListView on:deletion={(e) => deleteGroupFromList(e)} bind:list={slicedGroups} isPublic={isPublic} itemType="group" itemsPerPage={itemsPerPage} currentPage={currentPage} fullLength={groups.length} />
|
||||
<ListPagination bind:currentPage bind:pageAmount />
|
||||
<ListView {pageOptions} {lists} otherList={nope} on:update={update} on:deletion={updateDelete} />
|
||||
{/if}
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
{/if}
|
||||
{/if}
|
||||
</Col>
|
||||
|
@ -5,16 +5,14 @@
|
||||
|
||||
import SystemMain from '../../components/system/Main.svelte';
|
||||
import List from '../../components/list/List.svelte';
|
||||
import { defaultListOptions, defaultPageOptions, type List as Lists, type ListOptions, type PageOptions } from '../../components/list/types';
|
||||
|
||||
import type{ System } from '../../api/types';
|
||||
import type{ Group, Member, System } from '../../api/types';
|
||||
import api from '../../api';
|
||||
|
||||
let user: System = {};
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
let members = [];
|
||||
let groups = [];
|
||||
|
||||
let params = useParams();
|
||||
$: id = $params.id;
|
||||
|
||||
@ -55,6 +53,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
let memberList: Lists<Member> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
let groupList: Lists<Group> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
let groupListOptions: ListOptions = defaultListOptions;
|
||||
let memberListOptions: ListOptions = defaultListOptions;
|
||||
|
||||
let pageOptions: PageOptions = defaultPageOptions;
|
||||
let memberListPageOptions: PageOptions = {...pageOptions, ...{
|
||||
view: listView,
|
||||
isPublic: true,
|
||||
type: 'member'
|
||||
}};
|
||||
|
||||
let groupListPageOptions: PageOptions = {...pageOptions, ...{
|
||||
view: listView,
|
||||
isPublic: true,
|
||||
type: 'group'
|
||||
}};
|
||||
|
||||
</script>
|
||||
|
||||
<!-- display the banner if there's a banner set, and if the current settings allow for it-->
|
||||
@ -75,14 +107,14 @@
|
||||
<Alert color="info" aria-hidden>You are currently <b>viewing</b> a system.</Alert>
|
||||
<TabContent class="mt-3" on:tab={(e) => navigateTo(e.detail, listView)}>
|
||||
<TabPane tabId="system" tab="System" active={tabPane === "system"}>
|
||||
<SystemMain bind:user isPublic={true} />
|
||||
<SystemMain bind:user={user} isPublic={false} />
|
||||
</TabPane>
|
||||
<TabPane tabId="members" tab="Members" active={tabPane === "members"}>
|
||||
<List on:viewChange={(e) => navigateTo("members", e.detail)} members={members} groups={groups} isPublic={true} itemType={"member"} bind:view={listView} isDash={false}/>
|
||||
<List on:viewChange={(e) => navigateTo("members", e.detail)} bind:otherList={groupList} bind:lists={memberList} bind:pageOptions={memberListPageOptions} bind:options={memberListOptions} />
|
||||
</TabPane>
|
||||
<TabPane tabId="groups" tab="Groups" active={tabPane === "groups"}>
|
||||
<List on:viewChange={(e) => navigateTo("groups", e.detail)} members={members} groups={groups} isPublic={true} itemType={"group"} bind:view={listView} isDash={false} />
|
||||
</TabPane>
|
||||
<List on:viewChange={(e) => navigateTo("members", e.detail)} bind:otherList={memberList} bind:lists={groupList} bind:pageOptions={groupListPageOptions} bind:options={groupListOptions} />
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
{/if}
|
||||
</Col>
|
||||
|
@ -4,16 +4,19 @@
|
||||
import { Alert, Col, Container, Row, Card, CardBody, CardHeader, CardTitle, Input, Label, Button, Accordion, AccordionHeader, AccordionItem } from 'sveltestrap';
|
||||
import FaRandom from 'svelte-icons/fa/FaRandom.svelte'
|
||||
|
||||
import CardsList from '../../components/list/ListView.svelte';
|
||||
import ListView from '../../components/list/ListView.svelte';
|
||||
import api from '../../api';
|
||||
import type { Group, Member } from '../../api/types';
|
||||
import { defaultListOptions, type List as Lists, type ListOptions, type PageOptions } from '../../components/list/types';
|
||||
import { defaultPageOptions } from '../../components/list/types';
|
||||
import { filterList, paginateList } from '../../components/list/functions';
|
||||
import CardView from '../../components/list/CardView.svelte';
|
||||
|
||||
export let isPublic: boolean = false;
|
||||
export let type: string = "member";
|
||||
export let pickFromGroup: boolean = false;
|
||||
|
||||
let list: Member[]|Group[] = [];
|
||||
let randomList: Member[]|Group[] = [];
|
||||
|
||||
let loading = true;
|
||||
let err: string;
|
||||
@ -21,7 +24,7 @@
|
||||
let params = useParams();
|
||||
$: id = $params.id;
|
||||
$: groupId = $params.groupId;
|
||||
|
||||
|
||||
let location = useLocation();
|
||||
let searchParams = $location.search && new URLSearchParams($location.search);
|
||||
|
||||
@ -41,10 +44,10 @@
|
||||
|
||||
let allowDoubles = false;
|
||||
if (searchParams && searchParams.get("doubles") === "true") allowDoubles = true;
|
||||
|
||||
let listView = 'list';
|
||||
if (searchParams && searchParams.get('view')) listView = searchParams.get('view');
|
||||
|
||||
// just a hidden option to expand the cards by default regardless of your global settings
|
||||
let openByDefault = false;
|
||||
if (searchParams && searchParams.get("open") === "true") openByDefault = true;
|
||||
|
||||
let rollCounter = 1;
|
||||
|
||||
@ -70,7 +73,7 @@
|
||||
list = res;
|
||||
}
|
||||
else throw new Error(`Unknown list type ${type}`);
|
||||
randomList = randomizeList(amount, usePrivateMembers, allowDoubles);
|
||||
lists.rawList = randomizeList(amount, usePrivateMembers, allowDoubles);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err = error.message;
|
||||
@ -103,14 +106,15 @@
|
||||
function rerollList() {
|
||||
let amount = parseInt(optionAmount);
|
||||
let paramArray = [];
|
||||
if (amount > 1) paramArray.push(`view=${pageOptions.view}`);
|
||||
if (amount > 1) paramArray.push(`amount=${amount}`);
|
||||
if (optionAllowDoubles === "true") paramArray.push("doubles=true");
|
||||
if (optionUsePrivateItems === "true") paramArray.push("all=true");
|
||||
if (openByDefault === true) paramArray.push("open=true");
|
||||
|
||||
randomList = randomizeList(parseInt(optionAmount), optionUsePrivateItems, optionAllowDoubles);
|
||||
lists.rawList = randomizeList(parseInt(optionAmount), optionUsePrivateItems, optionAllowDoubles);
|
||||
navigate(`${path}${paramArray.length > 0 ? `?${paramArray.join('&')}` : ""}`);
|
||||
rollCounter ++;
|
||||
pageOptions.currentPage = rollCounter;
|
||||
}
|
||||
|
||||
function capitalizeFirstLetter(string: string) {
|
||||
@ -125,20 +129,6 @@
|
||||
let optionAllowDoubles = "false";
|
||||
if (allowDoubles === true) optionAllowDoubles = "true";
|
||||
|
||||
function getItemLink(item: Member | Group): string {
|
||||
let url: string;
|
||||
|
||||
if (isPublic) url = "/dash/";
|
||||
else url = "/profile/";
|
||||
|
||||
if (type === "member") url += "m/";
|
||||
else if (type === "group") url += "g/";
|
||||
|
||||
url += item.id;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function getBackUrl() {
|
||||
let str: string;
|
||||
if (isPublic) {
|
||||
@ -150,6 +140,39 @@
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
let lists: Lists<Group> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
let nope: Lists<Member> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
let pageOptions: PageOptions = {...defaultPageOptions,
|
||||
isPublic: true,
|
||||
type: type === 'group' ? 'group' : 'member',
|
||||
isMain: false,
|
||||
itemsPerPage: 5,
|
||||
randomized: true,
|
||||
view: listView === 'card' ? 'card' : 'list'
|
||||
};
|
||||
|
||||
let listOptions: ListOptions = {...defaultListOptions,
|
||||
sort: 'none',
|
||||
};
|
||||
|
||||
$: lists.currentPage = lists.rawList;
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
@ -163,8 +186,8 @@
|
||||
</div>Randomize {capitalizeFirstLetter(type)}s {isPublic && id ? `(${id})` : pickFromGroup ? `(${groupId})` : ""}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Row>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<Row class="mb-3">
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<Label>Amount:</Label>
|
||||
<Input bind:value={optionAmount} type="select" aria-label="amount">
|
||||
<option default={amount === 1}>1</option>
|
||||
@ -174,15 +197,22 @@
|
||||
<option default={amount === 5}>5</option>
|
||||
</Input>
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<Label>Allow duplicates:</Label>
|
||||
<Input bind:value={optionAllowDoubles} type="select" aria-label="allow duplicates">
|
||||
<option value="false" default={allowDoubles === false}>no</option>
|
||||
<option value="true" default={allowDoubles === true}>yes</option>
|
||||
</Input>
|
||||
</Col>
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<Label>View:</Label>
|
||||
<Input bind:value={pageOptions.view} type="select" aria-label="amount">
|
||||
<option value="list">List</option>
|
||||
<option value="card">Cards</option>
|
||||
</Input>
|
||||
</Col>
|
||||
{#if !isPublic}
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
<Label>Use all {type}s:</Label>
|
||||
<Input bind:value={optionUsePrivateItems} type="select" aria-label="include private members">
|
||||
<option value="false" default={usePrivateItems === false}>no (only public {type}s)</option>
|
||||
@ -206,7 +236,11 @@
|
||||
{:else if err}
|
||||
<Alert color="danger">{err}</Alert>
|
||||
{:else}
|
||||
<CardsList openByDefault={openByDefault} bind:list={randomList} isPublic={true} itemType={type} itemsPerPage={5} currentPage={rollCounter} fullLength={5 * rollCounter - (5 - randomList.length)} />
|
||||
{#if pageOptions.view === 'card'}
|
||||
<CardView {lists} {pageOptions} otherList={nope} />
|
||||
{:else}
|
||||
<ListView {lists} {pageOptions} otherList={nope}/>
|
||||
{/if}
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -35,6 +35,10 @@
|
||||
background-color: $light-alt !important;
|
||||
}
|
||||
|
||||
.accordion-item {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
// discord markdown
|
||||
.d-spoiler {
|
||||
color: $dark-alt; //overwrite
|
||||
|
Loading…
Reference in New Issue
Block a user