2022-02-02 19:17:30 +01:00
|
|
|
<script lang="ts">
|
2022-08-01 21:08:34 +02:00
|
|
|
import { readable, type Readable } from "svelte/store";
|
2023-10-07 11:06:45 +02:00
|
|
|
import type { UpListing } from "@upnd/upend";
|
|
|
|
import type { Address, IValue } from "@upnd/upend/types";
|
2022-02-15 22:05:51 +01:00
|
|
|
import { query } from "../../lib/entity";
|
2022-03-18 21:27:24 +01:00
|
|
|
import UpObject from "../display/UpObject.svelte";
|
2023-01-21 18:57:57 +01:00
|
|
|
import UpObjectCard from "../display/UpObjectCard.svelte";
|
2023-10-07 11:06:45 +02:00
|
|
|
import { ATTR_LABEL } from "@upnd/upend/constants";
|
2023-07-06 18:01:58 +02:00
|
|
|
import { i18n } from "../../i18n";
|
|
|
|
import Icon from "../utils/Icon.svelte";
|
2023-09-07 18:57:45 +02:00
|
|
|
import IconButton from "../utils/IconButton.svelte";
|
|
|
|
import Selector from "../utils/Selector.svelte";
|
|
|
|
import { createEventDispatcher } from "svelte";
|
|
|
|
import type { WidgetChange } from "src/types/base";
|
|
|
|
import debug from "debug";
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
const dbg = debug(`kestrel:EntityList`);
|
2022-02-02 19:17:30 +01:00
|
|
|
|
2022-09-04 23:29:41 +02:00
|
|
|
export let entities: Address[];
|
|
|
|
export let thumbnails = true;
|
|
|
|
export let sort = true;
|
2023-09-07 18:57:45 +02:00
|
|
|
export let address: Address | undefined = undefined;
|
2022-09-04 23:29:41 +02:00
|
|
|
|
2023-09-07 18:57:45 +02:00
|
|
|
$: deduplicatedEntities = Array.from(new Set(entities));
|
2022-09-05 00:04:44 +02:00
|
|
|
|
2022-09-04 23:29:41 +02:00
|
|
|
let style: "list" | "grid" | "flex" = "grid";
|
|
|
|
|
|
|
|
let clientWidth: number;
|
2022-09-05 00:04:44 +02:00
|
|
|
$: style = !thumbnails || clientWidth < 600 ? "list" : "grid";
|
2022-02-02 19:17:30 +01:00
|
|
|
|
|
|
|
// Sorting
|
2023-09-07 18:57:45 +02:00
|
|
|
let sortedEntities: Address[] = [];
|
2022-02-02 19:17:30 +01:00
|
|
|
|
|
|
|
let sortKeys: { [key: string]: string[] } = {};
|
2023-07-29 19:24:08 +02:00
|
|
|
function addSortKeys(key: string, vals: string[], resort: boolean) {
|
2022-02-02 19:17:30 +01:00
|
|
|
if (!sortKeys[key]) {
|
|
|
|
sortKeys[key] = [];
|
|
|
|
}
|
|
|
|
let changed = false;
|
|
|
|
vals.forEach((val) => {
|
|
|
|
if (!sortKeys[key].includes(val)) {
|
|
|
|
changed = true;
|
|
|
|
sortKeys[key].push(val);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-12-22 13:15:20 +01:00
|
|
|
if (resort && changed) sortEntities();
|
2022-02-02 19:17:30 +01:00
|
|
|
}
|
|
|
|
|
2022-12-22 13:15:20 +01:00
|
|
|
function sortEntities() {
|
2022-09-04 23:29:41 +02:00
|
|
|
if (!sort) return;
|
|
|
|
|
2022-09-05 00:04:44 +02:00
|
|
|
sortedEntities = deduplicatedEntities.concat();
|
2022-09-04 23:29:41 +02:00
|
|
|
|
|
|
|
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,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2022-02-02 19:17:30 +01:00
|
|
|
}
|
|
|
|
|
2023-07-29 16:45:29 +02:00
|
|
|
// Labelling
|
|
|
|
let labelListing: Readable<UpListing> = readable(undefined);
|
|
|
|
$: {
|
2023-07-29 19:24:08 +02:00
|
|
|
const addressesString = deduplicatedEntities
|
|
|
|
.map((addr) => `@${addr}`)
|
|
|
|
.join(" ");
|
2023-07-29 16:45:29 +02:00
|
|
|
|
|
|
|
labelListing = query(
|
2023-08-25 23:35:29 +02:00
|
|
|
`(matches (in ${addressesString}) "${ATTR_LABEL}" ? )`,
|
2023-07-29 16:45:29 +02:00
|
|
|
).result;
|
|
|
|
}
|
|
|
|
|
2022-02-09 20:40:59 +01:00
|
|
|
$: {
|
|
|
|
if ($labelListing) {
|
2022-09-05 00:04:44 +02:00
|
|
|
deduplicatedEntities.forEach((address) => {
|
2023-07-29 19:24:08 +02:00
|
|
|
addSortKeys(
|
|
|
|
address,
|
|
|
|
$labelListing.getObject(address).identify(),
|
2023-08-25 23:35:29 +02:00
|
|
|
false,
|
2023-07-29 19:24:08 +02:00
|
|
|
);
|
2022-02-09 20:40:59 +01:00
|
|
|
});
|
2022-12-22 13:15:20 +01:00
|
|
|
sortEntities();
|
2022-02-09 20:40:59 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-18 14:08:27 +01:00
|
|
|
if (!sort) {
|
2022-12-22 13:15:20 +01:00
|
|
|
sortedEntities = entities;
|
2022-12-18 14:08:27 +01:00
|
|
|
}
|
2023-07-29 16:45:29 +02:00
|
|
|
|
|
|
|
// Visibility
|
|
|
|
let visible: Set<string> = new Set();
|
2023-07-29 19:24:08 +02:00
|
|
|
let observer = new IntersectionObserver((intersections) => {
|
|
|
|
intersections.forEach((intersection) => {
|
|
|
|
const address = (intersection.target as HTMLElement).dataset["address"];
|
|
|
|
if (!address) {
|
|
|
|
console.warn("Intersected wrong element?");
|
|
|
|
return;
|
|
|
|
}
|
2023-07-29 16:45:29 +02:00
|
|
|
|
2023-07-29 19:24:08 +02:00
|
|
|
if (intersection.isIntersecting) {
|
|
|
|
visible.add(address);
|
|
|
|
}
|
|
|
|
visible = visible;
|
2023-07-29 16:45:29 +02:00
|
|
|
});
|
2023-07-29 19:24:08 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
function observe(node: HTMLElement) {
|
|
|
|
observer.observe(node);
|
2023-07-29 16:45:29 +02:00
|
|
|
|
2023-07-29 19:24:08 +02:00
|
|
|
return {
|
|
|
|
destroy() {
|
|
|
|
observer.unobserve(node);
|
|
|
|
},
|
|
|
|
};
|
2023-07-29 16:45:29 +02:00
|
|
|
}
|
2023-09-07 18:57:45 +02:00
|
|
|
|
|
|
|
// Adding
|
|
|
|
let addSelector: Selector | undefined;
|
|
|
|
let adding = false;
|
|
|
|
|
|
|
|
$: if (adding && addSelector) addSelector.focus();
|
|
|
|
|
|
|
|
function addEntity(ev: CustomEvent<IValue>) {
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2022-02-02 19:17:30 +01:00
|
|
|
</script>
|
|
|
|
|
2022-09-04 23:29:41 +02:00
|
|
|
<div
|
2023-09-07 15:40:20 +02:00
|
|
|
class="entitylist style-{style}"
|
2022-09-04 23:29:41 +02:00
|
|
|
class:has-thumbnails={thumbnails}
|
|
|
|
bind:clientWidth
|
|
|
|
>
|
2023-09-07 21:53:56 +02:00
|
|
|
{#if !sortedEntities.length}
|
|
|
|
<div class="message">
|
|
|
|
{$i18n.t("No entries.")}
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
<div class="items">
|
|
|
|
{#each sortedEntities as entity (entity)}
|
|
|
|
<div data-address={entity} use:observe class="item">
|
|
|
|
{#if visible.has(entity)}
|
|
|
|
{#if thumbnails}
|
|
|
|
<UpObjectCard
|
|
|
|
address={entity}
|
|
|
|
labels={sortKeys[entity]}
|
|
|
|
banner={false}
|
|
|
|
on:resolved={(event) => {
|
|
|
|
addSortKeys(entity, event.detail, true);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<div class="icon">
|
|
|
|
<IconButton
|
|
|
|
name="trash"
|
|
|
|
color="#dc322f"
|
|
|
|
on:click={() => removeEntity(entity)}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
{:else}
|
|
|
|
<div class="object">
|
|
|
|
<UpObject
|
|
|
|
link
|
2023-07-29 16:45:29 +02:00
|
|
|
address={entity}
|
|
|
|
labels={sortKeys[entity]}
|
|
|
|
on:resolved={(event) => {
|
2023-07-29 19:24:08 +02:00
|
|
|
addSortKeys(entity, event.detail, true);
|
2023-07-29 16:45:29 +02:00
|
|
|
}}
|
|
|
|
/>
|
2023-09-07 21:53:56 +02:00
|
|
|
</div>
|
|
|
|
<div class="icon">
|
|
|
|
<IconButton
|
|
|
|
name="trash"
|
|
|
|
color="#dc322f"
|
|
|
|
on:click={() => removeEntity(entity)}
|
|
|
|
/>
|
|
|
|
</div>
|
2023-09-07 18:57:45 +02:00
|
|
|
{/if}
|
2023-09-07 21:53:56 +02:00
|
|
|
{:else}
|
|
|
|
<div class="skeleton" style="text-align: center">...</div>
|
|
|
|
{/if}
|
|
|
|
</div>
|
|
|
|
{/each}
|
|
|
|
{#if address}
|
2023-09-07 21:56:55 +02:00
|
|
|
<div class="add">
|
2023-09-07 21:53:56 +02:00
|
|
|
{#if adding}
|
|
|
|
<Selector
|
|
|
|
bind:this={addSelector}
|
2023-10-01 14:16:15 +02:00
|
|
|
placeholder={$i18n.t("Search database or paste an URL")}
|
2023-09-07 21:53:56 +02:00
|
|
|
type="value"
|
|
|
|
valueTypes={["Address"]}
|
|
|
|
on:input={addEntity}
|
|
|
|
on:focus={(ev) => {
|
|
|
|
if (!ev.detail) {
|
|
|
|
adding = false;
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
{:else}
|
|
|
|
<IconButton
|
|
|
|
name="plus-circle"
|
|
|
|
outline
|
|
|
|
subdued
|
|
|
|
on:click={() => {
|
|
|
|
adding = true;
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
{/if}
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
</div>
|
2022-02-02 19:17:30 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<style lang="scss">
|
2022-09-04 23:29:41 +02:00
|
|
|
.items {
|
|
|
|
gap: 4px;
|
|
|
|
}
|
|
|
|
|
2023-09-07 15:40:20 +02:00
|
|
|
.entitylist.has-thumbnails .items {
|
2022-02-02 19:17:30 +01:00
|
|
|
gap: 1rem;
|
2022-09-04 23:29:41 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 15:40:20 +02:00
|
|
|
:global(.entitylist.style-grid .items) {
|
2022-09-04 23:29:41 +02:00
|
|
|
display: grid;
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
2023-09-07 18:57:45 +02:00
|
|
|
align-items: end;
|
2022-09-04 23:29:41 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 15:40:20 +02:00
|
|
|
:global(.entitylist.style-flex .items) {
|
2022-09-04 23:29:41 +02:00
|
|
|
display: flex;
|
2022-02-02 19:17:30 +01:00
|
|
|
flex-wrap: wrap;
|
2022-03-22 20:38:14 +01:00
|
|
|
align-items: flex-end;
|
2022-02-02 19:17:30 +01:00
|
|
|
}
|
|
|
|
|
2023-09-07 15:40:20 +02:00
|
|
|
:global(.entitylist.style-list .items) {
|
2022-09-04 23:29:41 +02:00
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
align-items: stretch;
|
|
|
|
}
|
2023-07-06 18:01:58 +02:00
|
|
|
|
2023-08-31 12:53:58 +02:00
|
|
|
.item {
|
|
|
|
min-width: 0;
|
2023-09-07 18:57:45 +02:00
|
|
|
overflow: hidden;
|
2023-08-31 12:53:58 +02:00
|
|
|
}
|
|
|
|
|
2023-07-06 18:01:58 +02:00
|
|
|
.message {
|
|
|
|
text-align: center;
|
|
|
|
margin: 0.5em;
|
2023-09-07 21:53:56 +02:00
|
|
|
opacity: 0.66;
|
2023-07-06 18:01:58 +02:00
|
|
|
}
|
2023-09-07 18:57:45 +02:00
|
|
|
|
|
|
|
.entitylist:not(.has-thumbnails) {
|
|
|
|
.item {
|
|
|
|
display: flex;
|
|
|
|
.object {
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
.icon {
|
|
|
|
width: 0;
|
|
|
|
transition: width 0.3s ease;
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
.icon {
|
|
|
|
width: 1.5em;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.entitylist.has-thumbnails {
|
|
|
|
.item {
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
.icon {
|
|
|
|
position: absolute;
|
|
|
|
top: 0.5em;
|
|
|
|
right: 0.5em;
|
|
|
|
|
|
|
|
opacity: 0;
|
|
|
|
transition: opacity 0.3s ease;
|
|
|
|
}
|
|
|
|
|
|
|
|
&:hover .icon {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.add {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
}
|
|
|
|
|
|
|
|
.entitylist.style-grid .add {
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
}
|
2022-02-02 19:17:30 +01:00
|
|
|
</style>
|