refactor(dashboard): use svelte context for lists
This commit is contained in:
parent
f2e160c526
commit
a48f2b40c4
@ -142,9 +142,9 @@
|
||||
<Link to={getGroupPageUrl(true)}><button class="link-button button-right btn btn-secondary" style={isPublic ? "float: none !important; margin-left: 0;" : ""} tabindex={-1} aria-label="randomize group members">Randomize group</button></Link>
|
||||
|
||||
{:else if editMode}
|
||||
<Edit on:update on:deletion bind:group bind:editMode />
|
||||
<Edit {group} bind:editMode />
|
||||
{:else if memberMode}
|
||||
<MemberEdit on:updateGroupMembers bind:group bind:memberMode bind:members />
|
||||
<MemberEdit {group} bind:memberMode />
|
||||
{/if}
|
||||
</CardBody>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { Card, CardHeader, CardTitle, Modal, Button, ListGroup, ListGroupItem, Label, Input, Alert, Tooltip, Row, Col } from 'sveltestrap';
|
||||
import { getContext } from 'svelte';
|
||||
import { CardHeader, CardTitle, Modal, Button, ListGroup, ListGroupItem, Label, Input, Alert, Tooltip, Row, Col } from 'sveltestrap';
|
||||
import parseMarkdown from '../../api/parse-markdown';
|
||||
import twemoji from 'twemoji';
|
||||
import { Link } from 'svelte-navigator';
|
||||
@ -12,16 +12,16 @@
|
||||
import FaTimes from 'svelte-icons/fa/FaTimes.svelte'
|
||||
import FaCheck from 'svelte-icons/fa/FaCheck.svelte'
|
||||
|
||||
import type { Group, Member} from '../../api/types';
|
||||
import type { Group, Member } from '../../api/types';
|
||||
import api from '../../api';
|
||||
import default_avatar from '../../assets/default_avatar.png';
|
||||
import resizeMedia from '../../api/resize-media';
|
||||
import AwaitHtml from '../common/AwaitHtml.svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let group: Group;
|
||||
export let searchBy: string;
|
||||
export let sortBy: string;
|
||||
export let members: Member[];
|
||||
export let isPublic = false;
|
||||
export let isDash = false;
|
||||
|
||||
@ -60,17 +60,14 @@
|
||||
|
||||
let altText = `member ${group.name} avatar`;
|
||||
|
||||
$: memberList = members && members.filter(m => group.members && group.members.includes(m.uuid) && true).sort((a, b) => a.name.localeCompare(b.name)) || [];
|
||||
$: members = getContext<Writable<Member[]>>("members")
|
||||
$: memberList = $members && $members.filter(m => group.members && group.members.includes(m.uuid) && true).sort((a, b) => a.name.localeCompare(b.name)) || [];
|
||||
|
||||
$: groups = getContext<Writable<Member[]>>("groups")
|
||||
let listGroupElements = [];
|
||||
|
||||
let pageLink = isPublic ? `/profile/g/${group.id}` : `/dash/g/${group.id}`;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function update(group: Group) {
|
||||
dispatch('update', group);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
let data = input;
|
||||
err = [];
|
||||
@ -93,9 +90,17 @@
|
||||
loading = true;
|
||||
try {
|
||||
let res = await api().groups(group.id).patch({data});
|
||||
update({...group, ...res});
|
||||
group = {...group, ...res};
|
||||
const newgroup = {...group, ...res};
|
||||
const newList = $groups.map((g: Group) => {
|
||||
if (group.uuid === g.uuid) {
|
||||
g = newgroup
|
||||
}
|
||||
return g
|
||||
})
|
||||
groups.set(newList)
|
||||
|
||||
err = [];
|
||||
loading = false;
|
||||
view = 'card';
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@ -179,7 +184,7 @@
|
||||
<Input bind:value={input.icon} maxlength={256} type="url" placeholder={group.icon} aria-label="group avatar url"/>
|
||||
<hr style="min-height: 1px" />
|
||||
<Label>Display name:</Label>
|
||||
<textarea class="form-control mb-2" style="resize: none; height: 1em" bind:value={input.display_name} maxlength={100} type="text" placeholder={group.display_name} aria-label="group display name" />
|
||||
<textarea class="form-control mb-2" style="resize: none; height: 1em" bind:value={input.display_name} maxlength={100} placeholder={group.display_name} aria-label="group display name" />
|
||||
<hr style="min-height: 1px" />
|
||||
<Label>Description:</Label>
|
||||
<textarea class="form-control" style="resize: none; overflow: hidden;" bind:value={input.description} maxlength={1000} use:autoresize placeholder={group.description} aria-label="group description"/>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { Row, Col, Input, Button, Label, Alert, Spinner, Modal, ModalHeader, ModalBody } from 'sveltestrap';
|
||||
import { createEventDispatcher, tick } from 'svelte';
|
||||
import { getContext, tick } from 'svelte';
|
||||
import type { Group } from '../../api/types';
|
||||
import api from '../../api';
|
||||
import { autoresize } from 'svelte-textarea-autoresize';
|
||||
@ -13,19 +14,11 @@
|
||||
export let group: Group;
|
||||
export let editMode: boolean;
|
||||
|
||||
$: groups = getContext<Writable<Group[]>>("groups")
|
||||
|
||||
let err: string[] = [];
|
||||
|
||||
let input: Group = group;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function deletion() {
|
||||
dispatch('deletion', group.id);
|
||||
}
|
||||
|
||||
function update(group: Group) {
|
||||
dispatch('update', group);
|
||||
}
|
||||
let input: Group = JSON.parse(JSON.stringify(group));
|
||||
|
||||
async function submit() {
|
||||
let data = input;
|
||||
@ -49,7 +42,15 @@
|
||||
loading = true;
|
||||
try {
|
||||
let res = await api().groups(group.id).patch({data});
|
||||
update({...group, ...res});
|
||||
const newgroup = {...group, ...res};
|
||||
const newList = $groups.map((g: Group) => {
|
||||
if (group.uuid === g.uuid) {
|
||||
g = newgroup
|
||||
}
|
||||
return g
|
||||
})
|
||||
groups.set(newList);
|
||||
|
||||
err = [];
|
||||
editMode = false;
|
||||
return;
|
||||
@ -85,7 +86,10 @@
|
||||
deleteErr = null;
|
||||
toggleDeleteModal();
|
||||
loading = false;
|
||||
deletion();
|
||||
|
||||
const newList = $groups.filter((g: Group) => group.uuid !== g.uuid)
|
||||
groups.set(newList);
|
||||
|
||||
} catch (error) {
|
||||
loading = false;
|
||||
}
|
||||
@ -110,7 +114,7 @@
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<Label>Display name:</Label>
|
||||
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.display_name} maxlength={100} type="text" placeholder={group.display_name} aria-label="group display name"/>
|
||||
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.display_name} maxlength={100} placeholder={group.display_name} aria-label="group display name"/>
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<Label>Color:</Label>
|
||||
|
@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { Row, Col, Button, Alert, ListGroup, ListGroupItem, Spinner } from 'sveltestrap';
|
||||
import ListPagination from "../common/ListPagination.svelte";
|
||||
import twemoji from "twemoji";
|
||||
@ -14,7 +16,9 @@
|
||||
let err: string;
|
||||
export let group: Group;
|
||||
export let memberMode: boolean = true;
|
||||
export let members: Member[];
|
||||
|
||||
$: groups = getContext<Writable<Group[]>>("groups")
|
||||
$: members = getContext<Writable<Member[]>>("members")
|
||||
|
||||
let membersInGroup: Member[];
|
||||
let membersNotInGroup: Member[];
|
||||
@ -28,18 +32,13 @@
|
||||
|
||||
let smallPages = true;
|
||||
|
||||
updateMemberList();
|
||||
|
||||
function updateMemberList() {
|
||||
membersInGroup = members.filter(member => group.members.includes(member.uuid));
|
||||
membersInGroup = membersInGroup.sort((a, b) => a.name.localeCompare(b.name));
|
||||
$: membersInGroup = $members.filter(member => group.members.includes(member.uuid)).sort((a, b) => a.name.localeCompare(b.name));;
|
||||
|
||||
membersNotInGroup = members.filter(member => !group.members.includes(member.uuid));
|
||||
membersNotInGroup = membersNotInGroup.sort((a, b) => a.name.localeCompare(b.name));
|
||||
$: membersNotInGroup = $members.filter(member => !group.members.includes(member.uuid)).sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
membersInGroupSelection = membersInGroup.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));
|
||||
membersNotInGroupSelection = membersNotInGroup.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));
|
||||
}
|
||||
$: membersInGroupSelection = membersInGroup.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));
|
||||
$: membersNotInGroupSelection = membersNotInGroup.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));
|
||||
|
||||
$: indexOfLastItem = currentPage * 10;
|
||||
$: indexOfFirstItem = indexOfLastItem - 10;
|
||||
@ -69,7 +68,13 @@ function memberListRenderer(item: any) {
|
||||
loading = true;
|
||||
await api().groups(group.id).members.add.post({data});
|
||||
data.forEach(member => group.members.push(member));
|
||||
updateMemberList();
|
||||
$groups.forEach(g => {
|
||||
if (g.uuid === group.uuid) {
|
||||
g.members = group.members
|
||||
}
|
||||
})
|
||||
groups.set($groups)
|
||||
|
||||
err = null;
|
||||
membersToBeAdded = [];
|
||||
loading = false;
|
||||
@ -86,7 +91,13 @@ function memberListRenderer(item: any) {
|
||||
loading = true;
|
||||
await api().groups(group.id).members.remove.post({data});
|
||||
group.members = group.members.filter(m => !data.includes(m));
|
||||
updateMemberList();
|
||||
$groups.forEach(g => {
|
||||
if (g.uuid === group.uuid) {
|
||||
g.members = group.members
|
||||
}
|
||||
})
|
||||
groups.set($groups)
|
||||
|
||||
err = null;
|
||||
membersToBeRemoved = [];
|
||||
loading = false;
|
||||
|
@ -3,8 +3,9 @@
|
||||
import type { Group } from '../../api/types';
|
||||
import api from '../../api';
|
||||
import { autoresize } from 'svelte-textarea-autoresize';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import FaPlus from 'svelte-icons/fa/FaPlus.svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
const descriptions: string[] = JSON.parse(localStorage.getItem("pk-config"))?.description_templates;
|
||||
|
||||
@ -24,11 +25,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function create(data: Group) {
|
||||
dispatch('create', data);
|
||||
}
|
||||
$: groups = getContext<Writable<Group[]>>("groups");
|
||||
|
||||
let input: Group = JSON.parse(JSON.stringify(defaultGroup));
|
||||
|
||||
@ -55,7 +52,10 @@
|
||||
try {
|
||||
let res: Group = await api().groups().post({data});
|
||||
res.members = [];
|
||||
create(res);
|
||||
|
||||
$groups.push(res)
|
||||
groups.set($groups)
|
||||
|
||||
input = JSON.parse(JSON.stringify(defaultGroup));
|
||||
message = `Group ${data.name} successfully created!`
|
||||
err = [];
|
||||
|
@ -1,14 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { tick, createEventDispatcher } from "svelte";
|
||||
import { tick, getContext } from "svelte";
|
||||
import { ModalBody, ModalHeader, Col, Row, Input, Label, ModalFooter, Button, Spinner, Alert } from "sveltestrap";
|
||||
|
||||
import type { Group, GroupPrivacy } from '../../api/types';
|
||||
import api from '../../api';
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
export let privacyOpen: boolean;
|
||||
export let group: Group;
|
||||
const togglePrivacyModal = () => (privacyOpen = !privacyOpen);
|
||||
|
||||
$: groups = getContext<Writable<Group[]>>("groups")
|
||||
|
||||
let err: string;
|
||||
let loading = false;
|
||||
let success = false;
|
||||
@ -18,12 +21,6 @@
|
||||
Object.keys(privacy).forEach(x => privacy[x] = target.value);
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function update(group) {
|
||||
dispatch('update', group);
|
||||
}
|
||||
|
||||
// I can't use the hacked together Required<T> type from the bulk privacy here
|
||||
// that breaks updating the displayed privacy after submitting
|
||||
// but there's not really any way for any privacy fields here to be missing
|
||||
@ -43,7 +40,15 @@
|
||||
const data: Group = {privacy: privacy};
|
||||
try {
|
||||
let res = await api().groups(group.id).patch({data});
|
||||
group = {...group, ...res};
|
||||
const newgroup = {...group, ...res};
|
||||
const newList = $groups.map((g: Group) => {
|
||||
if (group.uuid === g.uuid) {
|
||||
g = newgroup
|
||||
}
|
||||
return g
|
||||
})
|
||||
groups.set(newList);
|
||||
|
||||
success = true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
@ -7,11 +7,10 @@
|
||||
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';
|
||||
import type { PageOptions } from './types';
|
||||
|
||||
export let lists: List<Member|Group>;
|
||||
export let pageOptions: PageOptions;
|
||||
export let otherList: List<Member|Group>;
|
||||
export let currentList: Member[]|Group[];
|
||||
|
||||
let copiedItems = {};
|
||||
|
||||
@ -49,31 +48,31 @@
|
||||
|
||||
<Row class="mx-4 mx-sm-5 mx-md-0">
|
||||
{#if pageOptions.type === "member"}
|
||||
{#each lists.currentPage as item (item.uuid)}
|
||||
{#each currentList 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} >
|
||||
<MemberCard on:update member={item} searchBy="name" sortBy="name" isPublic={pageOptions.isPublic} isDash={pageOptions.isMain}>
|
||||
<button class="button-reset" 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}
|
||||
<FaUserCircle />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</MemberCard>
|
||||
<Tooltip placement="top" target={`${pageOptions.type}-copy-${item.uuid}`}>{copiedItems[item.uuid] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</div>
|
||||
{/each}
|
||||
{:else if pageOptions.type === "group"}
|
||||
{#each lists.currentPage as item (item.uuid)}
|
||||
{#each currentList 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} >
|
||||
<GroupCard group={item} searchBy="name" sortBy="name" isPublic={pageOptions.isPublic} isDash={pageOptions.isMain}>
|
||||
<button class="button-reset" 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}
|
||||
<FaUsers />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</GroupCard>
|
||||
<Tooltip placement="top" target={`${pageOptions.type}-copy-${item.uuid}`}>{copiedItems[item.uuid] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</div>
|
||||
@ -87,4 +86,14 @@
|
||||
max-width: 24rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button-reset {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
</style>
|
103
dashboard/src/components/list/GroupList.svelte
Normal file
103
dashboard/src/components/list/GroupList.svelte
Normal file
@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import { Alert, Row, Col, Spinner, Button } from 'sveltestrap';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
import NewGroup from '../group/NewGroup.svelte';
|
||||
import ListPagination from '../common/ListPagination.svelte';
|
||||
import ListControl from './ListControl.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, PageOptions } from './types';
|
||||
import { createShortList, filterList, getPageAmount, paginateList } from './functions';
|
||||
|
||||
$: memberList = getContext<Writable<Member[]>>("members")
|
||||
$: groupList = getContext<Writable<Group[]>>("groups")
|
||||
|
||||
$: members = $memberList || []
|
||||
$: groups = $groupList || []
|
||||
|
||||
export let options: ListOptions;
|
||||
export let pageOptions: PageOptions;
|
||||
export let systemId: string = "";
|
||||
|
||||
// general state handling
|
||||
export let listLoading = true;
|
||||
export let err: string = "";
|
||||
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
// set the default page amount to 1 so we don't get divisions by 0
|
||||
let pageAmount = 1;
|
||||
|
||||
// set the default items per page based on settings and view
|
||||
// this probably should be moved to it's own function
|
||||
if (pageOptions.view === "card") pageOptions.itemsPerPage = 24;
|
||||
else if (settings && settings.accessibility && settings.accessibility.expandedcards) pageOptions.itemsPerPage = 10;
|
||||
else pageOptions.itemsPerPage = 25;
|
||||
|
||||
async function fetchList() {
|
||||
err = "";
|
||||
listLoading = true;
|
||||
try {
|
||||
const res: Group[] = await api().systems(pageOptions.isPublic ? systemId : "@me").groups.get({ auth: !pageOptions.isPublic, query: { with_members: !pageOptions.isPublic } });
|
||||
groupList.set(res)
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err = error.message;
|
||||
}
|
||||
listLoading = false;
|
||||
}
|
||||
|
||||
$: processedGroups = filterList(groups, options, "group")
|
||||
$: currentGroups = paginateList(processedGroups, pageOptions)
|
||||
$: shortMembers = createShortList(members)
|
||||
$: pageAmount = getPageAmount(processedGroups, pageOptions)
|
||||
$: shortGroups = createShortList(groups)
|
||||
|
||||
</script>
|
||||
|
||||
<ListControl on:viewChange bind:options bind:pageOptions {shortGroups} {shortMembers} />
|
||||
|
||||
{#if listLoading && !err}
|
||||
<div class="mx-auto text-center">
|
||||
<Spinner class="d-inline-block" />
|
||||
</div>
|
||||
{:else if err}
|
||||
<Row>
|
||||
<Col xs={12} lg={10}>
|
||||
<Alert color="danger">{err}</Alert>
|
||||
</Col>
|
||||
<Col xs={12} lg={2}>
|
||||
<Button class="w-100 mb-3" color="primary" on:click={fetchList} aria-label="refresh member list">Refresh</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
{:else}
|
||||
<span class="itemcounter">{members.length} {pageOptions.type}s ({currentGroups.length} shown) <a href="#!" on:click={(e) => {e.preventDefault(); fetchList()}}>Refresh list</a></span>
|
||||
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
|
||||
{#if !err && !pageOptions.isPublic}
|
||||
<NewGroup />
|
||||
{/if}
|
||||
{#if pageOptions.view === "card"}
|
||||
<CardView {pageOptions} currentList={currentGroups} />
|
||||
{:else if pageOptions.view === "tiny"}
|
||||
tiny!
|
||||
{:else}
|
||||
<ListView currentList={currentGroups} {pageOptions} {options} fullListLength={members.length} />
|
||||
{/if}
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.itemcounter {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -1,126 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Alert, Row, Col, Spinner, Button } from 'sveltestrap';
|
||||
import { onMount } from 'svelte';
|
||||
import { useParams } from 'svelte-navigator';
|
||||
|
||||
import NewMember from '../member/NewMember.svelte';
|
||||
import NewGroup from '../group/NewGroup.svelte';
|
||||
import ListPagination from '../common/ListPagination.svelte';
|
||||
import ListControl from './ListControl.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 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 = 1;
|
||||
|
||||
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 || pageOptions.isPublic) fetchList();
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
err = "";
|
||||
listLoading = true;
|
||||
try {
|
||||
if (pageOptions.type === "member") {
|
||||
const res: Member[] = await api().systems(pageOptions.isPublic ? id : "@me").members.get({ auth: !pageOptions.isPublic });
|
||||
lists.rawList = 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 ${pageOptions.type}`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err = error.message;
|
||||
}
|
||||
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) {
|
||||
lists.rawList.push(event.detail);
|
||||
lists.rawList = lists.rawList;
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<ListControl on:viewChange bind:options bind:lists bind:pageOptions />
|
||||
|
||||
{#if listLoading && !err}
|
||||
<div class="mx-auto text-center">
|
||||
<Spinner class="d-inline-block" />
|
||||
</div>
|
||||
{:else if err}
|
||||
<Row>
|
||||
<Col xs={12} lg={10}>
|
||||
<Alert color="danger">{err}</Alert>
|
||||
</Col>
|
||||
<Col xs={12} lg={2}>
|
||||
<Button class="w-100 mb-3" color="primary" on:click={fetchList} aria-label="refresh member list">Refresh</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
{:else}
|
||||
<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={pageOptions.currentPage} {pageAmount} />
|
||||
|
||||
{#if !err && !pageOptions.isPublic}
|
||||
{#if pageOptions.type === "member"}
|
||||
<NewMember on:create={addItemToList} />
|
||||
{:else if pageOptions.type === "group"}
|
||||
<NewGroup on:create={addItemToList} />
|
||||
{/if}
|
||||
{/if}
|
||||
{#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} {otherList} {lists} {pageOptions} {options} />
|
||||
{/if}
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.itemcounter {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -10,8 +10,9 @@ import { Link, useParams } from 'svelte-navigator';
|
||||
import type { ListOptions, List, PageOptions, ShortList } from './types';
|
||||
|
||||
export let options: ListOptions;
|
||||
export let lists: List<Member|Group>;
|
||||
export let pageOptions: PageOptions;
|
||||
export let shortGroups: any[] = []
|
||||
export let shortMembers: any[] = []
|
||||
|
||||
let advanced = false;
|
||||
|
||||
@ -286,9 +287,9 @@ function resetPage() {
|
||||
<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..." />
|
||||
<Svelecte disableHighlight renderer="group-list" valueAsObject bind:value={options.groups.include.list} options={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..." />
|
||||
<Svelecte disableHighlight renderer="member-list" valueAsObject bind:value={options.groups.include.list} options={shortMembers} multiple style="margin-bottom: 0.5rem" placeholder="Include..." />
|
||||
{/if}
|
||||
</Col>
|
||||
<Col xs={12} md={5} lg={3} class="mb-2">
|
||||
@ -304,9 +305,9 @@ function resetPage() {
|
||||
<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..." />
|
||||
<Svelecte disableHighlight renderer="group-list" valueAsObject bind:value={options.groups.exclude.list} options={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..." />
|
||||
<Svelecte disableHighlight renderer="member-list" valueAsObject bind:value={options.groups.exclude.list} options={shortMembers} multiple style="margin-bottom: 0.5rem" placeholder="Exclude..." />
|
||||
{/if}
|
||||
</Col>
|
||||
<Col xs={12} md={5} lg={3} class="mb-2">
|
||||
|
@ -10,13 +10,13 @@
|
||||
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';
|
||||
import { defaultListOptions, type ListOptions, type PageOptions } from './types';
|
||||
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
export let options: ListOptions = JSON.parse(JSON.stringify(defaultListOptions));
|
||||
export let otherList: List <Member|Group>;
|
||||
export let lists: List<Member|Group>;
|
||||
export let fullListLength: number;
|
||||
export let currentList: Member[]|Group[];
|
||||
export let pageOptions: PageOptions;
|
||||
|
||||
function getItemLink(item: Member | Group): string {
|
||||
@ -39,13 +39,13 @@
|
||||
let el;
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
if (index + 1 < indexStart + pageOptions.itemsPerPage && index + 1 < lists.processedList.length) el = document.getElementById(`${pageOptions.type}-card-${index + 1}`);
|
||||
if (index + 1 < indexStart + pageOptions.itemsPerPage && index + 1 < fullListLength) 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(`${pageOptions.type}-card-${index - 1}`);
|
||||
else if (lists.processedList.length <= indexStart + pageOptions.itemsPerPage) el = document.getElementById(`${pageOptions.type}-card-${lists.processedList.length - 1}`);
|
||||
else if (fullListLength <= indexStart + pageOptions.itemsPerPage) el = document.getElementById(`${pageOptions.type}-card-${fullListLength - 1}`);
|
||||
else el = document.getElementById(`${pageOptions.type}-card-${indexStart + pageOptions.itemsPerPage - 1}`);
|
||||
}
|
||||
|
||||
@ -114,12 +114,12 @@
|
||||
|
||||
{#if (settings && settings.accessibility ? (!settings.accessibility.expandedcards && !settings.accessibility.pagelinks) : true)}
|
||||
<div class="mb-3 accordion">
|
||||
{#each lists.currentPage as item, index (pageOptions.randomized ? item.uuid + '-' + index : item.uuid)}
|
||||
{#each currentList 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={`${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} >
|
||||
<button class="button-reset" 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 === "private"}
|
||||
<FaLock />
|
||||
{:else if pageOptions.type === "member"}
|
||||
@ -127,7 +127,7 @@
|
||||
{:else if pageOptions.type === "group"}
|
||||
<FaUsers />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</CardsHeader>
|
||||
<Tooltip placement="top" target={`${pageOptions.type}-copy-${item.id}-${indexStart + index}`}>{copiedArray[indexStart + index] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</button>
|
||||
@ -135,9 +135,9 @@
|
||||
<Collapse isOpen={pageOptions.randomized ? isOpen[item.uuid + '-' + pageOptions.currentPage + '-' + index] : isOpen[item.uuid]}>
|
||||
<CardBody class="border-top">
|
||||
{#if pageOptions.type === "member"}
|
||||
<MemberBody on:update on:deletion bind:isPublic={pageOptions.isPublic} groups={otherList.rawList} member={item} />
|
||||
<MemberBody isPublic={pageOptions.isPublic} member={item} />
|
||||
{:else if pageOptions.type === "group"}
|
||||
<GroupBody on:update on:deletion bind:isPublic={pageOptions.isPublic} members={otherList.rawList} group={item} />
|
||||
<GroupBody isPublic={pageOptions.isPublic} group={item} />
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Collapse>
|
||||
@ -145,12 +145,12 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else if settings.accessibility.expandedcards}
|
||||
{#each lists.currentPage as item, index (pageOptions.randomized ? item.uuid + '-' + index : item.uuid)}
|
||||
{#each currentList as item, index (pageOptions.randomized ? item.uuid + '-' + index : item.uuid)}
|
||||
<Card class="mb-3">
|
||||
<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">
|
||||
<button class="accordion-button collapsed p-0" id={`${pageOptions.type}-card-${indexStart + index}`} on:keydown={(e) => skipToNextItem(e, indexStart + index)} tabindex={0}>
|
||||
<CardHeader>
|
||||
<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} >
|
||||
<button class="button-reset" 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 === "private"}
|
||||
<FaLock />
|
||||
{:else if pageOptions.type === "member"}
|
||||
@ -158,27 +158,27 @@
|
||||
{:else if pageOptions.type === "group"}
|
||||
<FaUsers />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</CardsHeader>
|
||||
<Tooltip placement="top" target={`${pageOptions.type}-copy-${item.id}-${indexStart + index}`}>{copiedArray[indexStart + index] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</CardHeader>
|
||||
</div>
|
||||
</button>
|
||||
<CardBody>
|
||||
{#if pageOptions.type === "member"}
|
||||
<MemberBody on:update on:deletion bind:isPublic={pageOptions.isPublic} groups={otherList.rawList} member={item} />
|
||||
<MemberBody isPublic={pageOptions.isPublic} member={item} />
|
||||
{:else if pageOptions.type === "group"}
|
||||
<GroupBody on:update on:deletion bind:isPublic={pageOptions.isPublic} members={otherList.rawList} group={item} />
|
||||
<GroupBody isPublic={pageOptions.isPublic} group={item} />
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="my-3">
|
||||
{#each lists.currentPage as item, index(pageOptions.randomized ? item.uuid + '-' + index : item.uuid)}
|
||||
{#each currentList 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={`${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} >
|
||||
<button class="button-reset" 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 === "private"}
|
||||
<FaLock />
|
||||
{:else if pageOptions.type === "member"}
|
||||
@ -186,7 +186,7 @@
|
||||
{:else if pageOptions.type === "group"}
|
||||
<FaUsers />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</CardsHeader>
|
||||
<Tooltip placement="top" target={`${pageOptions.type}-copy-${item.id}-${indexStart + index}`}>{copiedArray[indexStart + index] ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</a>
|
||||
@ -194,3 +194,15 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.button-reset {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
</style>
|
103
dashboard/src/components/list/MemberList.svelte
Normal file
103
dashboard/src/components/list/MemberList.svelte
Normal file
@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import { Alert, Row, Col, Spinner, Button } from 'sveltestrap';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
import NewMember from '../member/NewMember.svelte';
|
||||
import ListPagination from '../common/ListPagination.svelte';
|
||||
import ListControl from './ListControl.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';
|
||||
|
||||
$: memberList = getContext<Writable<Member[]>>("members")
|
||||
$: groupList = getContext<Writable<Group[]>>("groups")
|
||||
|
||||
$: members = $memberList || []
|
||||
$: groups = $groupList || []
|
||||
|
||||
export let options: ListOptions;
|
||||
export let pageOptions: PageOptions;
|
||||
export let systemId: string = "";
|
||||
|
||||
// general state handling
|
||||
export let listLoading = true;
|
||||
export let err: string = "";
|
||||
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
// set the default page amount to 1 so we don't get divisions by 0
|
||||
let pageAmount = 1;
|
||||
|
||||
// set the default items per page based on settings and view
|
||||
// this probably should be moved to it's own function
|
||||
if (pageOptions.view === "card") pageOptions.itemsPerPage = 24;
|
||||
else if (settings && settings.accessibility && settings.accessibility.expandedcards) pageOptions.itemsPerPage = 10;
|
||||
else pageOptions.itemsPerPage = 25;
|
||||
|
||||
async function fetchList() {
|
||||
err = "";
|
||||
listLoading = true;
|
||||
try {
|
||||
const res: Member[] = await api().systems(pageOptions.isPublic ? systemId : "@me").members.get({ auth: !pageOptions.isPublic });
|
||||
memberList.set(res)
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
err = error.message;
|
||||
}
|
||||
listLoading = false;
|
||||
}
|
||||
|
||||
$: processedMembers = filterList(members, options, "member")
|
||||
$: currentMembers = paginateList(processedMembers, pageOptions)
|
||||
$: shortMembers = createShortList(members)
|
||||
$: pageAmount = getPageAmount(processedMembers, pageOptions)
|
||||
$: shortGroups = createShortList(groups)
|
||||
|
||||
</script>
|
||||
|
||||
<ListControl on:viewChange bind:options bind:pageOptions {shortGroups} {shortMembers} />
|
||||
|
||||
{#if listLoading && !err}
|
||||
<div class="mx-auto text-center">
|
||||
<Spinner class="d-inline-block" />
|
||||
</div>
|
||||
{:else if err}
|
||||
<Row>
|
||||
<Col xs={12} lg={10}>
|
||||
<Alert color="danger">{err}</Alert>
|
||||
</Col>
|
||||
<Col xs={12} lg={2}>
|
||||
<Button class="w-100 mb-3" color="primary" on:click={fetchList} aria-label="refresh member list">Refresh</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
{:else}
|
||||
<span class="itemcounter">{members.length} {pageOptions.type}s ({currentMembers.length} shown) <a href="#!" on:click={(e) => {e.preventDefault(); fetchList()}}>Refresh list</a></span>
|
||||
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
|
||||
{#if !err && !pageOptions.isPublic}
|
||||
<NewMember />
|
||||
{/if}
|
||||
{#if pageOptions.view === "card"}
|
||||
<CardView {pageOptions} currentList={currentMembers} />
|
||||
{:else if pageOptions.view === "tiny"}
|
||||
tiny!
|
||||
{:else}
|
||||
<ListView currentList={currentMembers} {pageOptions} {options} fullListLength={members.length} />
|
||||
{/if}
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.itemcounter {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -198,8 +198,8 @@
|
||||
<Link to={getSystemPageUrl()}><Button style="flex: 0; {!isPublic && "float: right;"}" color="primary" tabindex={-1} aria-label="view member's system">View system</Button></Link>
|
||||
{/if}
|
||||
{:else if editMode}
|
||||
<Edit on:update on:deletion bind:member bind:editMode />
|
||||
<Edit {member} bind:editMode />
|
||||
{:else if groupMode}
|
||||
<GroupEdit on:updateMemberGroups bind:member bind:groups bind:groupMode />
|
||||
<GroupEdit {member} bind:groupMode />
|
||||
{/if}
|
||||
</CardBody>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { Card, CardHeader, CardTitle, Modal, Button, ListGroup, ListGroupItem, Input, Alert, Label, Spinner, Row, Col, Tooltip } from 'sveltestrap';
|
||||
import AwaitHtml from '../common/AwaitHtml.svelte';
|
||||
import parseMarkdown from '../../api/parse-markdown';
|
||||
@ -17,12 +17,11 @@
|
||||
import api from '../../api';
|
||||
import default_avatar from '../../assets/default_avatar.png';
|
||||
import resizeMedia from '../../api/resize-media';
|
||||
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let member: Member;
|
||||
export let searchBy: string;
|
||||
export let sortBy: string;
|
||||
export let groups: Group[];
|
||||
export let isPublic = false;
|
||||
export let isDash = false;
|
||||
|
||||
@ -66,12 +65,6 @@
|
||||
|
||||
let pageLink = isPublic ? `/profile/m/${member.id}` : `/dash/m/${member.id}`;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function update(member: Member) {
|
||||
dispatch('update', member);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
let data = input;
|
||||
err = [];
|
||||
@ -93,7 +86,16 @@
|
||||
loading = true;
|
||||
try {
|
||||
let res = await api().members(member.id).patch({data});
|
||||
update({...member, ...res});
|
||||
const newMember = {...member, ...res}
|
||||
const newList = $members.map((m: Member) => {
|
||||
if (member.uuid === m.uuid) {
|
||||
m = newMember
|
||||
}
|
||||
return m
|
||||
})
|
||||
|
||||
members.set(newList)
|
||||
|
||||
view = 'card';
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@ -103,7 +105,9 @@
|
||||
loading = false;
|
||||
}
|
||||
|
||||
$: groupList = groups && groups.filter(g => g.members && g.members.includes(member.uuid)).sort((a, b) => a.name.localeCompare(b.name)) || [];
|
||||
$: members = getContext<Writable<Member[]>>("members")
|
||||
$: groups = getContext<Writable<Group[]>>("groups")
|
||||
$: groupList = $groups && $groups.filter(g => g.members && g.members.includes(member.uuid)).sort((a, b) => a.name.localeCompare(b.name)) || [];
|
||||
let listGroupElements = [];
|
||||
|
||||
$: if (view !== "edit") {
|
||||
|
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { Row, Col, Input, Button, Label, Alert, Spinner, Modal, ModalHeader, ModalBody } from 'sveltestrap';
|
||||
import { createEventDispatcher, tick } from 'svelte';
|
||||
import { getContext, tick } from 'svelte';
|
||||
import { autoresize } from 'svelte-textarea-autoresize';
|
||||
import moment from 'moment';
|
||||
|
||||
@ -12,22 +13,13 @@
|
||||
let loading: boolean = false;
|
||||
export let member: Member;
|
||||
export let editMode: boolean;
|
||||
$: members = getContext<Writable<Member[]>>("members")
|
||||
|
||||
let err: string[] = [];
|
||||
let success = false;
|
||||
|
||||
let input: Member = JSON.parse(JSON.stringify(member));
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function deletion() {
|
||||
dispatch('deletion', member.id);
|
||||
}
|
||||
|
||||
function update(member: Member) {
|
||||
dispatch('update', member);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
let data = input;
|
||||
err = [];
|
||||
@ -73,7 +65,15 @@
|
||||
loading = true;
|
||||
try {
|
||||
let res = await api().members(member.id).patch({data});
|
||||
update({...member, ...res});
|
||||
const newMember = {...member, ...res}
|
||||
const newList = $members.map((m: Member) => {
|
||||
if (member.uuid === m.uuid) {
|
||||
m = newMember
|
||||
}
|
||||
return m
|
||||
})
|
||||
|
||||
members.set(newList)
|
||||
editMode = false;
|
||||
return;
|
||||
} catch (error) {
|
||||
@ -108,7 +108,9 @@
|
||||
deleteErr = null;
|
||||
toggleDeleteModal();
|
||||
loading = false;
|
||||
deletion();
|
||||
|
||||
const newList = $members.filter((m: Member) => member.uuid !== m.uuid)
|
||||
members.set(newList);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
deleteErr = error.message;
|
||||
@ -135,11 +137,11 @@
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<Label>Display name:</Label>
|
||||
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.display_name} maxlength={100} type="text" placeholder={member.display_name} aria-label="member display name" />
|
||||
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.display_name} maxlength={100} placeholder={member.display_name} aria-label="member display name" />
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<Label>Pronouns:</Label>
|
||||
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.pronouns} maxlength={100} type="text" placeholder={member.pronouns} aria-label="member pronouns" />
|
||||
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.pronouns} maxlength={100} placeholder={member.pronouns} aria-label="member pronouns" />
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<Label>Birthday:</Label>
|
||||
|
@ -12,13 +12,17 @@
|
||||
|
||||
import type { Member, Group } from '../../api/types';
|
||||
import api from '../../api';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
export let member: Member;
|
||||
export let groups: Group[] = [];
|
||||
let loading: boolean = false;
|
||||
let err: string;
|
||||
export let groupMode: boolean = true;
|
||||
|
||||
$: groups = getContext<Writable<Group[]>>("groups")
|
||||
$: members = getContext<Writable<Member[]>>("members")
|
||||
|
||||
let groupsWithMember: Group[];
|
||||
let groupsWithoutMember: Group[];
|
||||
let groupsWithMemberSelection: any[];
|
||||
@ -30,18 +34,13 @@
|
||||
let currentPage = 1;
|
||||
let smallPages = true;
|
||||
|
||||
updateGroupLists();
|
||||
|
||||
function updateGroupLists() {
|
||||
groupsWithMember = groups.filter(group => group.members && group.members.includes(member.uuid));
|
||||
groupsWithMember.sort((a, b) => a.name.localeCompare(b.name));
|
||||
$: groupsWithMember = $groups.filter(group => group.members && group.members.includes(member.uuid)).sort((a, b) => a.name.localeCompare(b.name));;
|
||||
|
||||
groupsWithoutMember = groups.filter(group => group.members && !group.members.includes(member.uuid));
|
||||
groupsWithoutMember.sort((a, b) => a.name.localeCompare(b.name));
|
||||
$: groupsWithoutMember = $groups.filter(group => group.members && !group.members.includes(member.uuid)).sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
groupsWithMemberSelection = groupsWithMember.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));
|
||||
groupsWithoutMemberSelection = groupsWithoutMember.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));
|
||||
}
|
||||
$: groupsWithMemberSelection = groupsWithMember.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));
|
||||
$: groupsWithoutMemberSelection = groupsWithoutMember.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));
|
||||
|
||||
$: indexOfLastItem = currentPage * 10;
|
||||
$: indexOfFirstItem = indexOfLastItem - 10;
|
||||
@ -70,8 +69,9 @@
|
||||
try {
|
||||
loading = true;
|
||||
await api().members(member.id).groups.add.post({data});
|
||||
groups.forEach(group => data.includes(group.uuid) && group.members.push(member.uuid));
|
||||
updateGroupLists();
|
||||
$groups.forEach(group => data.includes(group.uuid) && group.members.push(member.uuid));
|
||||
groups.set($groups)
|
||||
|
||||
err = null;
|
||||
groupsToBeAdded = [];
|
||||
loading = false;
|
||||
@ -87,8 +87,9 @@
|
||||
try {
|
||||
loading = true;
|
||||
await api().members(member.id).groups.remove.post({data});
|
||||
groups.forEach(group => {if (data.includes(group.uuid)) group.members = group.members.filter(m => m !== member.uuid)});
|
||||
updateGroupLists();
|
||||
$groups.forEach(group => {if (data.includes(group.uuid)) group.members = group.members.filter(m => m !== member.uuid)});
|
||||
groups.set($groups)
|
||||
|
||||
err = null;
|
||||
groupsToBeRemoved = [];
|
||||
loading = false;
|
||||
|
@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Accordion, AccordionItem, Row, Col, Input, Button, Label, Alert, Spinner, CardTitle, InputGroup } from 'sveltestrap';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { autoresize } from 'svelte-textarea-autoresize';
|
||||
import moment from 'moment';
|
||||
import FaPlus from 'svelte-icons/fa/FaPlus.svelte';
|
||||
import type { Member } from '../../api/types';
|
||||
import api from '../../api';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
const descriptions: string[] = JSON.parse(localStorage.getItem("pk-config"))?.description_templates;
|
||||
|
||||
@ -36,11 +37,7 @@
|
||||
// creating a deep copy here so that defaultMember doesn't get updated too
|
||||
let input: Member = JSON.parse(JSON.stringify(defaultMember));
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function create(data: Member) {
|
||||
dispatch('create', data);
|
||||
}
|
||||
$: members = getContext<Writable<Member[]>>("members")
|
||||
|
||||
async function submit() {
|
||||
let data = input;
|
||||
@ -92,7 +89,9 @@
|
||||
loading = true;
|
||||
try {
|
||||
let res = await api().members().post({data});
|
||||
create(res);
|
||||
$members.push(res)
|
||||
members.set($members)
|
||||
|
||||
input = JSON.parse(JSON.stringify(defaultMember));
|
||||
message = `Member ${data.name} successfully created!`
|
||||
err = [];
|
||||
@ -128,11 +127,11 @@
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<Label>Display name:</Label>
|
||||
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.display_name} maxlength={100} type="text" placeholder={input.display_name} />
|
||||
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.display_name} maxlength={100} placeholder={input.display_name} />
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<Label>Pronouns:</Label>
|
||||
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.pronouns} maxlength={100} type="text" placeholder={input.pronouns} />
|
||||
<textarea class="form-control" style="resize: none; height: 1em" bind:value={input.pronouns} maxlength={100} placeholder={input.pronouns} />
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-2">
|
||||
<Label>Birthday:</Label>
|
||||
@ -166,49 +165,49 @@
|
||||
<Col xs={12} lg={4} class="mb-3">
|
||||
<Label>Description:</Label>
|
||||
<Input type="select" bind:value={input.privacy.description_privacy}>
|
||||
<option default>public</option>
|
||||
<option>public</option>
|
||||
<option>private</option>
|
||||
</Input>
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-3">
|
||||
<Label>Name:</Label>
|
||||
<Input type="select" bind:value={input.privacy.name_privacy}>
|
||||
<option default>public</option>
|
||||
<option>public</option>
|
||||
<option>private</option>
|
||||
</Input>
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-3">
|
||||
<Label>Avatar:</Label>
|
||||
<Input type="select" bind:value={input.privacy.avatar_privacy}>
|
||||
<option default>public</option>
|
||||
<option>public</option>
|
||||
<option>private</option>
|
||||
</Input>
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-3">
|
||||
<Label>Birthday:</Label>
|
||||
<Input type="select" bind:value={input.privacy.birthday_privacy}>
|
||||
<option default>public</option>
|
||||
<option>public</option>
|
||||
<option>private</option>
|
||||
</Input>
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-3">
|
||||
<Label>Pronouns:</Label>
|
||||
<Input type="select" bind:value={input.privacy.pronoun_privacy}>
|
||||
<option default>public</option>
|
||||
<option>public</option>
|
||||
<option>private</option>
|
||||
</Input>
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-3">
|
||||
<Label>Visibility:</Label>
|
||||
<Input type="select" bind:value={input.privacy.visibility}>
|
||||
<option default>public</option>
|
||||
<option>public</option>
|
||||
<option>private</option>
|
||||
</Input>
|
||||
</Col>
|
||||
<Col xs={12} lg={4} class="mb-3">
|
||||
<Label>Metadata:</Label>
|
||||
<Input type="select" bind:value={input.privacy.metadata_privacy}>
|
||||
<option default>public</option>
|
||||
<option>public</option>
|
||||
<option>private</option>
|
||||
</Input>
|
||||
</Col>
|
||||
|
@ -1,14 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { tick, createEventDispatcher } from "svelte";
|
||||
import { tick, getContext } from "svelte";
|
||||
import { Col, Row, Input, Label, Button, Alert, Spinner } from "sveltestrap";
|
||||
|
||||
import type { Member, MemberPrivacy } from '../../api/types';
|
||||
import api from '../../api';
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
export let privacyOpen: boolean;
|
||||
export let member: Member;
|
||||
const togglePrivacyModal = () => (privacyOpen = !privacyOpen);
|
||||
|
||||
$: members = getContext<Writable<Member[]>>("members")
|
||||
|
||||
let err: string;
|
||||
let loading = false;
|
||||
let success = false;
|
||||
@ -18,12 +21,6 @@
|
||||
Object.keys(privacy).forEach(x => privacy[x] = target.value);
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function update(member) {
|
||||
dispatch('update', member);
|
||||
}
|
||||
|
||||
// I can't use the hacked together Required<T> type from the bulk privacy here
|
||||
// that breaks updating the displayed privacy after submitting
|
||||
// but there's not really any way for any privacy fields here to be missing
|
||||
@ -44,7 +41,15 @@
|
||||
const data: Member = {privacy: privacy};
|
||||
try {
|
||||
let res = await api().members(member.id).patch({data});
|
||||
update({...member, ...res});
|
||||
const newMember = {...member, ...res}
|
||||
const newList = $members.map((m: Member) => {
|
||||
if (member.uuid === m.uuid) {
|
||||
m = newMember
|
||||
}
|
||||
return m
|
||||
})
|
||||
members.set(newList)
|
||||
|
||||
success = true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
@ -4,11 +4,14 @@
|
||||
import { currentUser, loggedIn } from '../../stores';
|
||||
|
||||
import SystemMain from '../../components/system/Main.svelte';
|
||||
import List from '../../components/list/List.svelte';
|
||||
import MemberList from '../../components/list/MemberList.svelte';
|
||||
import GroupList from '../../components/list/GroupList.svelte';
|
||||
|
||||
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';
|
||||
import { setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
// get the state from the navigator so that we know which tab to start on
|
||||
let location = useLocation();
|
||||
@ -70,22 +73,43 @@
|
||||
}
|
||||
}
|
||||
|
||||
let memberList: Lists<Member> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
// context stores for each list
|
||||
let memberStore = writable<Member[]>([]);
|
||||
let groupStore = writable<Group[]>([]);
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
// state handling
|
||||
let errs: Record<string, string> = {};
|
||||
let loading: Record<string, boolean> = {};
|
||||
|
||||
setContext("members", memberStore);
|
||||
setContext("groups", groupStore);
|
||||
|
||||
fetchLists()
|
||||
|
||||
// fetch both lists, and store them inside a context store
|
||||
async function fetchLists() {
|
||||
loading.members = true;
|
||||
loading.groups = true;
|
||||
|
||||
try {
|
||||
const res = await api().systems("@me").members.get({ auth: true });
|
||||
memberStore.set(res)
|
||||
loading.members = false;
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errs.members = error.message;
|
||||
}
|
||||
|
||||
let groupList: Lists<Group> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
try {
|
||||
const res = await api().systems("@me").groups.get({ auth: true, query: { with_members: true } });
|
||||
groupStore.set(res)
|
||||
loading.groups = false;
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errs.groups = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
let groupListOptions: ListOptions = JSON.parse(JSON.stringify(defaultListOptions));
|
||||
@ -119,10 +143,10 @@
|
||||
<SystemMain bind:user={user} isPublic={false} />
|
||||
</TabPane>
|
||||
<TabPane tabId="members" tab="Members" active={tabPane === "members"}>
|
||||
<List on:viewChange={(e) => navigateTo("members", e.detail)} bind:otherList={groupList} bind:lists={memberList} bind:pageOptions={memberListPageOptions} bind:options={memberListOptions} />
|
||||
<MemberList on:viewChange={(e) => navigateTo("members", e.detail)} bind:listLoading={loading.members} pageOptions={memberListPageOptions} options={memberListOptions} />
|
||||
</TabPane>
|
||||
<TabPane tabId="groups" tab="Groups" active={tabPane === "groups"}>
|
||||
<List on:viewChange={(e) => navigateTo("groups", e.detail)} bind:otherList={memberList} bind:lists={groupList} bind:pageOptions={groupListPageOptions} bind:options={groupListOptions} />
|
||||
<GroupList on:viewChange={(e) => navigateTo("groups", e.detail)} bind:listLoading={loading.groups} pageOptions={groupListPageOptions} options={groupListOptions} />
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</Col>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { Container, Row, Col, Alert, Spinner, Card, CardHeader, CardBody, CardTitle, Tooltip } from "sveltestrap";
|
||||
import Body from '../../../components/group/Body.svelte';
|
||||
import { useParams, Link, navigate, useLocation } from 'svelte-navigator';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, setContext } from 'svelte';
|
||||
import api from "../../../api";
|
||||
import type { Member, Group } from "../../../api/types";
|
||||
import CardsHeader from "../../../components/common/CardsHeader.svelte";
|
||||
@ -14,6 +14,7 @@
|
||||
import { defaultListOptions, defaultPageOptions } from '../../../components/list/types';
|
||||
import { filterList, paginateList, getPageAmount } from '../../../components/list/functions';
|
||||
import PageControl from "../../../components/list/PageControl.svelte";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
|
||||
// get the state from the navigator so that we know which tab to start on
|
||||
let location = useLocation();
|
||||
@ -26,8 +27,6 @@
|
||||
let params = useParams();
|
||||
let err = "";
|
||||
let memberErr = "";
|
||||
let group: Group;
|
||||
let systemMembers: Group[] = [];
|
||||
let isDeleted = false;
|
||||
let notOwnSystem = false;
|
||||
let copied = false;
|
||||
@ -41,15 +40,23 @@
|
||||
if (!user) navigate("/");
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchGroup();
|
||||
onMount(async () => {
|
||||
await fetchGroup();
|
||||
});
|
||||
|
||||
let membersStore: Writable<Member[]> = writable([])
|
||||
let groupsStore: Writable<Group[]> = writable([])
|
||||
$: members = setContext<Writable<Member[]>>("members", membersStore)
|
||||
$: groups = setContext<Writable<Group[]>>("groups", groupsStore)
|
||||
$: group = $groups.filter(g => g.id === $params.id)[0] || {}
|
||||
|
||||
let title = isPublic ? "group" : "group (dash)";
|
||||
|
||||
async function fetchGroup() {
|
||||
try {
|
||||
group = await api().groups($params.id).get({auth: !isPublic});
|
||||
const res = await api().groups($params.id).get({auth: !isPublic});
|
||||
$groups = [res]
|
||||
group = $groups.filter(g => g.id === $params.id)[0];
|
||||
if (!isPublic && !group.privacy) {
|
||||
notOwnSystem = true;
|
||||
throw new Error("Group is not from own system.");
|
||||
@ -70,10 +77,17 @@
|
||||
|
||||
async function fetchMembers() {
|
||||
try {
|
||||
lists.rawList = await api().groups($params.id).members().get({auth: !isPublic});
|
||||
group.members = lists.rawList.map(function(member) {return member.uuid});
|
||||
if (!isPublic) {
|
||||
systemMembers = await api().systems("@me").members.get({ auth: true });
|
||||
if (isPublic) {
|
||||
const groupMembers: Member[] = await api().groups($params.id).members().get({auth: !isPublic});
|
||||
group.members = groupMembers.map((m: Member) => m.uuid);
|
||||
members.set(groupMembers);
|
||||
} else {
|
||||
const systemGroups: Group[] = await api().systems("@me").groups.get({ auth: true, query: { with_members: true } });
|
||||
group.members = systemGroups.filter((g: Group) => g.id === $params.id).map((m: Member) => m.uuid);
|
||||
groups.set(systemGroups);
|
||||
|
||||
const systemMembers = await api().systems("@me").members.get({ auth: true });
|
||||
members.set(systemMembers);
|
||||
}
|
||||
memberErr = "";
|
||||
memberLoading = false;
|
||||
@ -84,24 +98,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
let lists: Lists<Member> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
let nope: Lists<Group> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
let listOptions: ListOptions = {...defaultListOptions};
|
||||
|
||||
let pageOptions: PageOptions = {...defaultPageOptions,
|
||||
@ -134,18 +130,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: lists.processedList = filterList(lists.rawList, listOptions);
|
||||
$: lists.currentPage = paginateList(lists.processedList, pageOptions);
|
||||
$: pageAmount = getPageAmount(lists.processedList, pageOptions);
|
||||
$: groupMembers = $members.filter(m => group.members.includes(m.uuid));
|
||||
$: processedList = filterList(groupMembers, listOptions);
|
||||
$: currentPage = paginateList(processedList, pageOptions);
|
||||
$: pageAmount = getPageAmount(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}
|
||||
@ -173,7 +162,7 @@
|
||||
{:else if group && group.id}
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardsHeader bind:item={group}>
|
||||
<CardsHeader item={group}>
|
||||
<div slot="icon" style="cursor: pointer;" id={`group-copy-${group.id}`} on:click|stopPropagation={() => copyShortLink()} on:keydown={(e) => copyShortLink(e)} tabindex={0} >
|
||||
<FaUsers slot="icon" />
|
||||
</div>
|
||||
@ -181,7 +170,7 @@
|
||||
<Tooltip placement="top" target={`group-copy-${group.id}`}>{copied ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Body bind:members={systemMembers} bind:group={group} isPage={true} isPublic={isPublic}/>
|
||||
<Body {group} isPage={true} isPublic={isPublic}/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
@ -189,14 +178,14 @@
|
||||
<Alert color="primary"><Spinner size="sm" /> Fetching members...</Alert>
|
||||
{:else if memberErr}
|
||||
<Alert color="danger">{memberErr}</Alert>
|
||||
{:else if lists.rawList && lists.rawList.length > 0}
|
||||
{:else if groupMembers && groupMembers.length > 0}
|
||||
<PageControl bind:options={listOptions} bind:pageOptions />
|
||||
<span class="itemcounter">{lists.processedList.length} {pageOptions.type}s ({lists.currentPage.length} shown)</span>
|
||||
<span class="itemcounter">{processedList.length} {pageOptions.type}s ({currentPage.length} shown)</span>
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
{#if pageOptions.view === "card"}
|
||||
<CardView {pageOptions} {lists} otherList={nope} on:update={update} />
|
||||
<CardView {pageOptions} currentList={currentPage} />
|
||||
{:else}
|
||||
<ListView {pageOptions} {lists} otherList={nope} on:update={update} on:deletion={updateDelete} />
|
||||
<ListView {pageOptions} currentList={currentPage} fullListLength={groupMembers.length}/>
|
||||
{/if}
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
{/if}
|
||||
|
@ -3,7 +3,7 @@
|
||||
import Body from '../../../components/member/Body.svelte';
|
||||
import ListView from '../../../components/list/ListView.svelte';
|
||||
import { useParams, Link, navigate, useLocation } from 'svelte-navigator';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, setContext } from 'svelte';
|
||||
import api from "../../../api";
|
||||
import type { Member, Group } from "../../../api/types";
|
||||
import CardsHeader from "../../../components/common/CardsHeader.svelte";
|
||||
@ -14,6 +14,7 @@
|
||||
import { defaultListOptions, defaultPageOptions } from '../../../components/list/types';
|
||||
import { filterList, getPageAmount, paginateList } from '../../../components/list/functions';
|
||||
import PageControl from "../../../components/list/PageControl.svelte";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
|
||||
// get the state from the navigator so that we know which tab to start on
|
||||
let location = useLocation();
|
||||
@ -26,8 +27,6 @@
|
||||
let params = useParams();
|
||||
let err = "";
|
||||
let groupErr = "";
|
||||
let member: Member;
|
||||
let systemGroups: Group[] = [];
|
||||
let isDeleted = false;
|
||||
let notOwnSystem = false;
|
||||
let copied = false;
|
||||
@ -41,19 +40,34 @@
|
||||
if (!user) navigate("/");
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchMember();
|
||||
onMount(async () => {
|
||||
await fetchMember();
|
||||
});
|
||||
|
||||
let membersStore: Writable<Member[]> = writable([])
|
||||
let groupsStore: Writable<Group[]> = writable([])
|
||||
$: members = setContext<Writable<Member[]>>("members", membersStore)
|
||||
$: groups = setContext<Writable<Group[]>>("groups", groupsStore)
|
||||
$: member = $members.filter(m => m.id === $params.id)[0] || {}
|
||||
|
||||
let title = isPublic ? "member" : "member (dash)";
|
||||
|
||||
async function fetchMember() {
|
||||
try {
|
||||
member = await api().members($params.id).get({auth: !isPublic});
|
||||
if (!isPublic && !member.privacy) {
|
||||
if (isPublic) {
|
||||
const res: Member = await api().members($params.id).get({auth: !isPublic});
|
||||
$members = [res]
|
||||
member = $members.filter(m => m.id === $params.id)[0]
|
||||
} else {
|
||||
const res: Member[] = await api().systems("@me").members.get({ auth: true});
|
||||
$members = res;
|
||||
member = $members.filter(m => m.id === $params.id)[0]
|
||||
|
||||
if (!member.privacy) {
|
||||
notOwnSystem = true;
|
||||
throw new Error("Member is not from own system.");
|
||||
}
|
||||
}
|
||||
err = "";
|
||||
loading = false;
|
||||
if (member.name) {
|
||||
@ -70,9 +84,13 @@
|
||||
|
||||
async function fetchGroups() {
|
||||
try {
|
||||
lists.rawList = await api().members($params.id).groups().get({auth: !isPublic, query: { with_members: !isPublic } });
|
||||
let memberGroups: Group[] = await api().members($params.id).groups().get({auth: !isPublic });
|
||||
memberGroups.forEach(g => g.members = [])
|
||||
groups.set(memberGroups)
|
||||
|
||||
if (!isPublic) {
|
||||
systemGroups = await api().systems("@me").groups.get({ auth: true, query: { with_members: true } });
|
||||
const systemGroups: Group[] = await api().systems("@me").groups.get({ auth: true, query: { with_members: true } });
|
||||
groups.set(systemGroups)
|
||||
}
|
||||
groupErr = "";
|
||||
groupLoading = false;
|
||||
@ -83,24 +101,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
let lists: Lists<Group> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
let nope: Lists<Member> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
}
|
||||
|
||||
let listOptions: ListOptions = {...defaultListOptions};
|
||||
|
||||
let pageOptions: PageOptions = {...defaultPageOptions,
|
||||
@ -133,17 +133,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: 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);
|
||||
}
|
||||
$: memberGroups = $groups.filter(g => g.members.includes(member.uuid));
|
||||
$: processedList = filterList(memberGroups, listOptions);
|
||||
$: currentPage = paginateList(processedList, pageOptions);
|
||||
$: pageAmount = getPageAmount(processedList, pageOptions);
|
||||
</script>
|
||||
|
||||
{#if settings && settings.appearance.color_background && !notOwnSystem}
|
||||
@ -171,7 +164,7 @@
|
||||
{:else if member && member.id}
|
||||
<Card class="mb-4">
|
||||
<CardHeader>
|
||||
<CardsHeader bind:item={member}>
|
||||
<CardsHeader item={member}>
|
||||
<div slot="icon" style="cursor: pointer;" id={`member-copy-${member.id}`} on:click|stopPropagation={() => copyShortLink()} on:keydown={(e) => copyShortLink(e)} tabindex={0} >
|
||||
<FaAddressCard slot="icon" />
|
||||
</div>
|
||||
@ -179,7 +172,7 @@
|
||||
<Tooltip placement="top" target={`member-copy-${member.id}`}>{copied ? "Copied!" : "Copy public link"}</Tooltip>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Body bind:groups={systemGroups} bind:member={member} isPage={true} isPublic={isPublic}/>
|
||||
<Body {member} isPage={true} isPublic={isPublic}/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
@ -187,14 +180,14 @@
|
||||
<Alert color="primary"><Spinner size="sm" /> Fetching groups...</Alert>
|
||||
{:else if groupErr}
|
||||
<Alert color="danger">{groupErr}</Alert>
|
||||
{:else if lists.rawList && lists.rawList.length > 0}
|
||||
{:else if memberGroups && memberGroups.length > 0}
|
||||
<PageControl bind:options={listOptions} bind:pageOptions />
|
||||
<span class="itemcounter">{lists.processedList.length} {pageOptions.type}s ({lists.currentPage.length} shown)</span>
|
||||
<span class="itemcounter">{processedList.length} {pageOptions.type}s ({currentPage.length} shown)</span>
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
{#if pageOptions.view === "card"}
|
||||
<CardView {pageOptions} {lists} otherList={nope} on:update={update} />
|
||||
<CardView {pageOptions} currentList={currentPage} />
|
||||
{:else}
|
||||
<ListView {pageOptions} {lists} otherList={nope} on:update={update} on:deletion={updateDelete} />
|
||||
<ListView {pageOptions} currentList={currentPage} fullListLength={memberGroups.length}/>
|
||||
{/if}
|
||||
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
|
||||
{/if}
|
||||
|
@ -1,20 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Container, Col, Row, TabContent, TabPane, Alert, Spinner } from 'sveltestrap';
|
||||
import { useParams, useLocation, navigate } from "svelte-navigator";
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, setContext } from 'svelte';
|
||||
|
||||
import SystemMain from '../../components/system/Main.svelte';
|
||||
import List from '../../components/list/List.svelte';
|
||||
import MemberList from '../../components/list/MemberList.svelte';
|
||||
import GroupList from '../../components/list/GroupList.svelte';
|
||||
import { defaultListOptions, defaultPageOptions, type List as Lists, type ListOptions, type PageOptions } from '../../components/list/types';
|
||||
|
||||
import type{ Group, Member, System } from '../../api/types';
|
||||
import api from '../../api';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let user: System = {};
|
||||
let settings = JSON.parse(localStorage.getItem("pk-settings"));
|
||||
|
||||
let params = useParams();
|
||||
$: id = $params.id;
|
||||
$: systemId = $params.id;
|
||||
|
||||
let location = useLocation();
|
||||
let urlParams = $location.search && new URLSearchParams($location.search);
|
||||
@ -24,7 +26,7 @@
|
||||
|
||||
// change the URL when changing tabs
|
||||
function navigateTo(tab: string|number, view: string) {
|
||||
let url = `./${id}`;
|
||||
let url = `./${systemId}`;
|
||||
if (tab || view) url += "?";
|
||||
if (tab) url += `tab=${tab}`
|
||||
if (tab && view) url += "&";
|
||||
@ -40,11 +42,12 @@
|
||||
|
||||
onMount(() => {
|
||||
getSystem();
|
||||
fetchLists();
|
||||
})
|
||||
|
||||
async function getSystem() {
|
||||
try {
|
||||
let res: System = await api().systems(id).get();
|
||||
let res: System = await api().systems(systemId).get();
|
||||
user = res;
|
||||
title = user.name ? user.name : "system";
|
||||
} catch (error) {
|
||||
@ -53,22 +56,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
let memberList: Lists<Member> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
// context stores for each list
|
||||
let memberStore = writable<Member[]>([]);
|
||||
let groupStore = writable<Group[]>([]);
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
// state handling
|
||||
let errs: Record<string, string> = {};
|
||||
let loading: Record<string, boolean> = {};
|
||||
|
||||
setContext("members", memberStore);
|
||||
setContext("groups", groupStore);
|
||||
|
||||
// fetch both lists, and store them inside a context store
|
||||
async function fetchLists() {
|
||||
loading.members = true;
|
||||
loading.groups = true;
|
||||
|
||||
try {
|
||||
const res = await api().systems(systemId).members.get();
|
||||
memberStore.set(res)
|
||||
loading.members = false;
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errs.members = error.message;
|
||||
}
|
||||
|
||||
let groupList: Lists<Group> = {
|
||||
rawList: [],
|
||||
processedList: [],
|
||||
currentPage: [],
|
||||
try {
|
||||
const res = await api().systems(systemId).groups.get();
|
||||
groupStore.set(res)
|
||||
loading.groups = false;
|
||||
|
||||
shortGroups: [],
|
||||
shortMembers: [],
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errs.groups = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
let groupListOptions: ListOptions = defaultListOptions;
|
||||
@ -110,10 +132,10 @@
|
||||
<SystemMain bind:user={user} isPublic={true} />
|
||||
</TabPane>
|
||||
<TabPane tabId="members" tab="Members" active={tabPane === "members"}>
|
||||
<List on:viewChange={(e) => navigateTo("members", e.detail)} bind:otherList={groupList} bind:lists={memberList} bind:pageOptions={memberListPageOptions} bind:options={memberListOptions} />
|
||||
<MemberList on:viewChange={(e) => navigateTo("members", e.detail)} bind:listLoading={loading.members} pageOptions={memberListPageOptions} options={memberListOptions} {systemId} />
|
||||
</TabPane>
|
||||
<TabPane tabId="groups" tab="Groups" active={tabPane === "groups"}>
|
||||
<List on:viewChange={(e) => navigateTo("members", e.detail)} bind:otherList={memberList} bind:lists={groupList} bind:pageOptions={groupListPageOptions} bind:options={groupListOptions} />
|
||||
<GroupList on:viewChange={(e) => navigateTo("groups", e.detail)} bind:listLoading={loading.groups} pageOptions={groupListPageOptions} options={groupListOptions} {systemId} />
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
{/if}
|
||||
|
@ -9,7 +9,6 @@
|
||||
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;
|
||||
@ -34,7 +33,7 @@
|
||||
|
||||
if (searchParams && searchParams.get("amount")) {
|
||||
amount = parseInt(searchParams.get("amount"));
|
||||
if (amount === NaN) amount = 1;
|
||||
if (isNaN(amount)) amount = 1;
|
||||
else if (amount > 5) amount = 5;
|
||||
}
|
||||
|
||||
@ -190,18 +189,18 @@
|
||||
<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>
|
||||
<option default={amount === 2}>2</option>
|
||||
<option default={amount === 3}>3</option>
|
||||
<option default={amount === 4}>4</option>
|
||||
<option default={amount === 5}>5</option>
|
||||
<option>1</option>
|
||||
<option>2</option>
|
||||
<option>3</option>
|
||||
<option>4</option>
|
||||
<option>5</option>
|
||||
</Input>
|
||||
</Col>
|
||||
<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>
|
||||
<option value="false">no</option>
|
||||
<option value="true">yes</option>
|
||||
</Input>
|
||||
</Col>
|
||||
<Col xs={12} md={6} lg={4} class="mb-2">
|
||||
@ -215,8 +214,8 @@
|
||||
<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>
|
||||
<option value="true" default={usePrivateItems === true}>yes (include private {type}s)</option>
|
||||
<option value="false">no (only public {type}s)</option>
|
||||
<option value="true">yes (include private {type}s)</option>
|
||||
</Input>
|
||||
</Col>
|
||||
{/if}
|
||||
@ -237,9 +236,9 @@
|
||||
<Alert color="danger">{err}</Alert>
|
||||
{:else}
|
||||
{#if pageOptions.view === 'card'}
|
||||
<CardView {lists} {pageOptions} otherList={nope} />
|
||||
<CardView {pageOptions} currentList={lists.currentPage}/>
|
||||
{:else}
|
||||
<ListView {lists} {pageOptions} otherList={nope}/>
|
||||
<ListView {pageOptions} currentList={lists.currentPage} fullListLength={1} />
|
||||
{/if}
|
||||
{/if}
|
||||
</Col>
|
||||
|
Loading…
Reference in New Issue
Block a user