225 lines
5.5 KiB
Svelte
225 lines
5.5 KiB
Svelte
<script lang="ts">
|
|
import { readable, type Readable } from 'svelte/store';
|
|
import { Query, type UpListing } from '@upnd/upend';
|
|
import { Any } from '@upnd/upend/query';
|
|
import type { Address } from '@upnd/upend/types';
|
|
import { query } from '$lib/entity';
|
|
import UpObject from '../display/UpObject.svelte';
|
|
import UpObjectCard from '../display/UpObjectCard.svelte';
|
|
import { ATTR_LABEL } from '@upnd/upend/constants';
|
|
import { i18n } from '$lib/i18n';
|
|
import IconButton from '../utils/IconButton.svelte';
|
|
import Selector, { type SelectorValue } from '../utils/Selector.svelte';
|
|
import { createEventDispatcher } from 'svelte';
|
|
import type { WidgetChange } from '$lib/types/base';
|
|
import debug from 'debug';
|
|
import api from '$lib/api';
|
|
import UpAttribute from '$lib/components/display/UpAttribute.svelte';
|
|
import UpValue from '$lib/components/display/UpValue.svelte';
|
|
const dispatch = createEventDispatcher();
|
|
const dbg = debug(`kestrel:Table`);
|
|
|
|
export let entities: Address[];
|
|
export let sort = true;
|
|
export let address: Address | undefined = undefined;
|
|
export let columns: string[] = [];
|
|
|
|
let currentColumns: string[] = columns;
|
|
|
|
let values: UpListing | undefined = undefined;
|
|
$: update(entities, currentColumns);
|
|
async function update(entities: Address[], columns: string[]) {
|
|
values = await api.query(
|
|
Query.matches(
|
|
entities.map((entity) => `@${entity}`),
|
|
columns,
|
|
Any
|
|
)
|
|
);
|
|
}
|
|
|
|
$: deduplicatedEntities = Array.from(new Set(entities));
|
|
|
|
// Sorting
|
|
let sortedEntities: Address[] = [];
|
|
|
|
let sortKeys: { [key: string]: string[] } = {};
|
|
function addSortKeys(key: string, vals: string[], resort: boolean) {
|
|
if (!sortKeys[key]) {
|
|
sortKeys[key] = [];
|
|
}
|
|
let changed = false;
|
|
vals.forEach((val) => {
|
|
if (!sortKeys[key].includes(val)) {
|
|
changed = true;
|
|
sortKeys[key].push(val);
|
|
}
|
|
});
|
|
|
|
if (resort && changed) sortEntities();
|
|
}
|
|
|
|
function sortEntities() {
|
|
if (!sort) return;
|
|
|
|
sortedEntities = deduplicatedEntities.concat();
|
|
|
|
sortedEntities.sort((a, b) => {
|
|
if (!sortKeys[a]?.length || !sortKeys[b]?.length) {
|
|
if (Boolean(sortKeys[a]?.length) && !sortKeys[b]?.length) {
|
|
return -1;
|
|
} else if (!sortKeys[a]?.length && Boolean(sortKeys[b]?.length)) {
|
|
return 1;
|
|
} else {
|
|
return a.localeCompare(b);
|
|
}
|
|
} else {
|
|
return sortKeys[a][0].localeCompare(sortKeys[b][0], undefined, {
|
|
numeric: true
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Labelling
|
|
let labelListing: Readable<UpListing | undefined> = readable(undefined);
|
|
$: {
|
|
const addressesString = deduplicatedEntities.map((addr) => `@${addr}`).join(' ');
|
|
|
|
labelListing = query(`(matches (in ${addressesString}) "${ATTR_LABEL}" ? )`).result;
|
|
}
|
|
|
|
$: {
|
|
if ($labelListing) {
|
|
deduplicatedEntities.forEach((address) => {
|
|
addSortKeys(address, $labelListing?.getObject(address).identify() || [], false);
|
|
});
|
|
sortEntities();
|
|
}
|
|
}
|
|
|
|
if (!sort) {
|
|
sortedEntities = entities;
|
|
}
|
|
|
|
// Visibility
|
|
let visible: Set<string> = new Set();
|
|
let observer = new IntersectionObserver((intersections) => {
|
|
intersections.forEach((intersection) => {
|
|
const address = (intersection.target as HTMLElement).dataset['address'];
|
|
if (!address) {
|
|
console.warn('Intersected wrong element?');
|
|
return;
|
|
}
|
|
|
|
if (intersection.isIntersecting) {
|
|
visible.add(address);
|
|
}
|
|
visible = visible;
|
|
});
|
|
});
|
|
|
|
function observe(node: HTMLElement) {
|
|
observer.observe(node);
|
|
|
|
return {
|
|
destroy() {
|
|
observer.unobserve(node);
|
|
}
|
|
};
|
|
}
|
|
//
|
|
// // Adding
|
|
// let addSelector: Selector | undefined;
|
|
// let adding = false;
|
|
//
|
|
// $: if (adding && addSelector) addSelector.focus();
|
|
//
|
|
// function addEntity(ev: CustomEvent<SelectorValue>) {
|
|
// dbg('Adding entity', ev.detail);
|
|
// const addAddress = ev.detail?.t == 'Address' ? ev.detail.c : undefined;
|
|
// if (!addAddress) return;
|
|
//
|
|
// dispatch('change', {
|
|
// type: 'entry-add',
|
|
// address: addAddress
|
|
// } as WidgetChange);
|
|
// }
|
|
//
|
|
// function removeEntity(address: string) {
|
|
// if (confirm($i18n.t('Are you sure you want to remove this entry from members?') || '')) {
|
|
// dbg('Removing entity', address);
|
|
// dispatch('change', {
|
|
// type: 'entry-delete',
|
|
// address
|
|
// } as WidgetChange);
|
|
// }
|
|
// }
|
|
</script>
|
|
|
|
<div class="table" style="--columns-count: {currentColumns.length}">
|
|
{#if !sortedEntities.length}
|
|
<div class="message">
|
|
{$i18n.t('No entries.')}
|
|
</div>
|
|
{/if}
|
|
<div class="header">
|
|
<div>
|
|
{$i18n.t('Name')}
|
|
</div>
|
|
{#each currentColumns as attribute}
|
|
<UpAttribute {attribute} />
|
|
{/each}
|
|
</div>
|
|
{#each sortedEntities as entity (entity)}
|
|
<div data-address={entity} use:observe class="row" class:visible={visible.has(entity)}>
|
|
{#if visible.has(entity)}
|
|
<div class="object">
|
|
<UpObject
|
|
link
|
|
address={entity}
|
|
labels={sortKeys[entity]}
|
|
on:resolved={(event) => {
|
|
addSortKeys(entity, event.detail, true);
|
|
}}
|
|
/>
|
|
</div>
|
|
{#each currentColumns as attribute}
|
|
{@const value = values?.getObject(entity)?.get(attribute)}
|
|
{#if value}
|
|
<UpValue {value} {attribute} />
|
|
{:else}
|
|
<div class="null">X</div>
|
|
{/if}
|
|
{/each}
|
|
{:else}
|
|
<div class="skeleton" style="text-align: center">...</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
<style lang="scss">
|
|
@use '../../styles/colors';
|
|
|
|
.table {
|
|
display: grid;
|
|
grid-template-columns: 1fr repeat(var(--columns-count), 1fr);
|
|
|
|
gap: 0.2em;
|
|
|
|
.header,
|
|
.row.visible {
|
|
display: contents;
|
|
}
|
|
|
|
.row:not(.visible) {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.header {
|
|
font-weight: bold;
|
|
}
|
|
}
|
|
</style>
|