351 lines
9.3 KiB
Svelte
351 lines
9.3 KiB
Svelte
<script lang="ts">
|
|
import filesize from "filesize";
|
|
import { formatRelative, fromUnixTime, parseISO } from "date-fns";
|
|
import Ellipsis from "../utils/Ellipsis.svelte";
|
|
import UpObject from "../display/UpObject.svelte";
|
|
import { createEventDispatcher } from "svelte";
|
|
import type { AttributeChange, AttributeUpdate } from "../../types/base";
|
|
import type { UpEntry, UpListing } from "upend";
|
|
import IconButton from "../utils/IconButton.svelte";
|
|
import Selector from "../utils/Selector.svelte";
|
|
import type { IValue } from "upend/types";
|
|
import Editable from "../utils/Editable.svelte";
|
|
import { query } from "../../lib/entity";
|
|
import { type Readable, readable } from "svelte/store";
|
|
import { defaultEntitySort, entityValueSort } from "../../util/sort";
|
|
import { attributeLabels } from "../../util/labels";
|
|
import { formatDuration } from "../../util/fragments/time";
|
|
import { i18n } from "../../i18n";
|
|
import UpLink from "../display/UpLink.svelte";
|
|
const dispatch = createEventDispatcher();
|
|
|
|
export let columns: string | undefined = undefined;
|
|
export let header = true;
|
|
|
|
export let orderByValue = false;
|
|
export let columnWidths: string[] | undefined = undefined;
|
|
|
|
export let entries: UpEntry[];
|
|
export let editable = false;
|
|
export let attributeOptions: string[] | undefined = undefined;
|
|
|
|
// Display
|
|
$: displayColumns = (columns || "entity, attribute, value")
|
|
.split(",")
|
|
.map((c) => c.trim());
|
|
|
|
const TIMESTAMP_COL = "timestamp";
|
|
const PROVENANCE_COL = "provenance";
|
|
const ENTITY_COL = "entity";
|
|
const ATTR_COL = "attribute";
|
|
const VALUE_COL = "value";
|
|
|
|
// Editing
|
|
let newEntryAttribute = "";
|
|
let newEntryValue: IValue | undefined;
|
|
|
|
async function addEntry() {
|
|
dispatch("change", {
|
|
type: "create",
|
|
attribute: newEntryAttribute,
|
|
value: newEntryValue,
|
|
} as AttributeChange);
|
|
newEntryAttribute = "";
|
|
newEntryValue = undefined;
|
|
}
|
|
async function removeEntry(address: string) {
|
|
if (confirm($i18n.t("Are you sure you want to remove the attribute?"))) {
|
|
dispatch("change", { type: "delete", address } as AttributeChange);
|
|
}
|
|
}
|
|
async function updateEntry(
|
|
address: string,
|
|
attribute: string,
|
|
value: IValue
|
|
) {
|
|
dispatch("change", {
|
|
type: "update",
|
|
address,
|
|
attribute,
|
|
value,
|
|
} as AttributeUpdate);
|
|
}
|
|
|
|
// Labelling
|
|
let labelListing: Readable<UpListing> = readable(undefined);
|
|
$: {
|
|
const addresses = [];
|
|
entries
|
|
.flatMap((e) =>
|
|
e.value.t === "Address" ? [e.entity, e.value.c] : [e.entity]
|
|
)
|
|
.forEach((addr) => {
|
|
if (!addresses.includes(addr)) {
|
|
addresses.push(addr);
|
|
}
|
|
});
|
|
|
|
const addressesString = addresses.map((addr) => `@${addr}`).join(" ");
|
|
|
|
labelListing = query(`(matches (in ${addressesString}) "LBL" ? )`).result;
|
|
}
|
|
|
|
// Sorting
|
|
let sortedEntries = entries;
|
|
let resort = false;
|
|
|
|
let sortKeys: { [key: string]: string[] } = {};
|
|
function addSortKeys(key: string, vals: string[]) {
|
|
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) sortEntries();
|
|
}
|
|
|
|
function sortEntries() {
|
|
sortedEntries = orderByValue
|
|
? entityValueSort(entries, Object.assign(sortKeys, $attributeLabels))
|
|
: defaultEntitySort(entries, Object.assign(sortKeys, $attributeLabels));
|
|
}
|
|
|
|
$: {
|
|
if ($labelListing) {
|
|
entries.forEach((entry) => {
|
|
addSortKeys(
|
|
entry.entity,
|
|
$labelListing.getObject(entry.entity).identify()
|
|
);
|
|
|
|
if (entry.value.t === "Address") {
|
|
addSortKeys(
|
|
entry.value.c,
|
|
$labelListing.getObject(String(entry.value.c)).identify()
|
|
);
|
|
}
|
|
});
|
|
sortEntries();
|
|
resort = true;
|
|
}
|
|
}
|
|
|
|
entries.forEach((entry) => {
|
|
addSortKeys(entry.entity, entry.listing.getObject(entry.entity).identify());
|
|
if (entry.value.t === "Address") {
|
|
addSortKeys(
|
|
entry.value.c,
|
|
entry.listing.getObject(String(entry.value.c)).identify()
|
|
);
|
|
}
|
|
});
|
|
|
|
// Formatting & Display
|
|
const COLUMN_LABELS: { [key: string]: string } = {
|
|
timestamp: $i18n.t("Added at"),
|
|
provenance: $i18n.t("Provenance"),
|
|
entity: $i18n.t("Entity"),
|
|
attribute: $i18n.t("Attribute"),
|
|
value: $i18n.t("Value"),
|
|
};
|
|
|
|
function formatValue(value: string | number, attribute: string): string {
|
|
switch (attribute) {
|
|
case "FILE_SIZE":
|
|
return filesize(parseInt(String(value), 10), { base: 2 });
|
|
case "ADDED":
|
|
case "LAST_VISITED":
|
|
return formatRelative(
|
|
fromUnixTime(parseInt(String(value), 10)),
|
|
new Date()
|
|
);
|
|
case "NUM_VISITED":
|
|
return `${value} times`;
|
|
case "MEDIA_DURATION":
|
|
return formatDuration(parseInt(String(value), 10));
|
|
default:
|
|
return String(value);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<table>
|
|
<colgroup>
|
|
{#if editable}
|
|
<col class="action-col" />
|
|
{/if}
|
|
{#each displayColumns as column, idx}
|
|
{#if columnWidths?.length}
|
|
<col
|
|
class="{column}-col"
|
|
style="width: {columnWidths[idx] || 'unset'}"
|
|
/>
|
|
{:else}
|
|
<col class="{column}-col" />
|
|
{/if}
|
|
{/each}
|
|
</colgroup>
|
|
|
|
{#if header}
|
|
<tr>
|
|
{#if editable}
|
|
<th />
|
|
{/if}
|
|
{#each displayColumns as column}
|
|
<th>{COLUMN_LABELS[column] || $attributeLabels[column] || column}</th>
|
|
{/each}
|
|
</tr>
|
|
{/if}
|
|
|
|
{#each sortedEntries as entry (entry.address)}
|
|
<tr>
|
|
{#if editable}
|
|
<td class="attr-action">
|
|
<IconButton
|
|
name="x-circle"
|
|
on:click={() => removeEntry(entry.address)}
|
|
/>
|
|
</td>
|
|
{/if}
|
|
{#each displayColumns as column}
|
|
{#if column == TIMESTAMP_COL}
|
|
<td title={entry.timestamp}
|
|
>{formatRelative(parseISO(entry.timestamp), new Date())}</td
|
|
>
|
|
{:else if column == PROVENANCE_COL}
|
|
<td>{entry.provenance}</td>
|
|
{:else if column == ENTITY_COL}
|
|
<td class="entity">
|
|
<UpObject
|
|
link
|
|
labels={$labelListing
|
|
?.getObject(String(entry.entity))
|
|
?.identify() || []}
|
|
address={entry.entity}
|
|
on:resolved={(event) => {
|
|
addSortKeys(entry.entity, event.detail);
|
|
}}
|
|
/>
|
|
</td>
|
|
{:else if column == ATTR_COL}
|
|
<td
|
|
class:formatted={Boolean(
|
|
Object.keys($attributeLabels).includes(entry.attribute)
|
|
)}
|
|
>
|
|
<UpLink to={{ attribute: entry.attribute }}>
|
|
<Ellipsis
|
|
value={$attributeLabels[entry.attribute] || entry.attribute}
|
|
title={$attributeLabels[entry.attribute]
|
|
? `${$attributeLabels[entry.attribute]} (${entry.attribute})`
|
|
: entry.attribute}
|
|
/>
|
|
</UpLink>
|
|
</td>
|
|
{:else if column == VALUE_COL}
|
|
<td class="value">
|
|
<Editable
|
|
{editable}
|
|
attribute={entry.attribute}
|
|
value={entry.value}
|
|
on:edit={(ev) =>
|
|
updateEntry(entry.address, entry.attribute, ev.detail)}
|
|
>
|
|
{#if entry.value.t === "Address"}
|
|
<UpObject
|
|
link
|
|
address={String(entry.value.c)}
|
|
labels={$labelListing
|
|
?.getObject(String(entry.value.c))
|
|
?.identify() || []}
|
|
on:resolved={(event) => {
|
|
addSortKeys(String(entry.value.c), event.detail);
|
|
}}
|
|
/>
|
|
{:else}
|
|
<div
|
|
class:formatted={Boolean(
|
|
formatValue(entry.value.c, entry.attribute)
|
|
)}
|
|
>
|
|
<Ellipsis
|
|
value={formatValue(entry.value.c, entry.attribute) ||
|
|
String(entry.value.c)}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</Editable>
|
|
</td>
|
|
{:else}
|
|
<td>?</td>
|
|
{/if}
|
|
{/each}
|
|
</tr>
|
|
{/each}
|
|
|
|
{#if editable}
|
|
<tr class="add-row">
|
|
<td class="attr-action">
|
|
<IconButton name="plus-circle" on:click={addEntry} />
|
|
</td>
|
|
{#if displayColumns.includes(ATTR_COL)}
|
|
<td>
|
|
<Selector
|
|
type="attribute"
|
|
bind:attribute={newEntryAttribute}
|
|
attributeOptions={attributeOptions || []}
|
|
/>
|
|
</td>
|
|
{/if}
|
|
{#if displayColumns.includes(VALUE_COL)}
|
|
<td>
|
|
<Selector type="value" bind:value={newEntryValue} />
|
|
</td>
|
|
{/if}
|
|
</tr>
|
|
{/if}
|
|
</table>
|
|
|
|
<style lang="scss" scoped>
|
|
table {
|
|
width: 100%;
|
|
table-layout: fixed;
|
|
|
|
border-spacing: 0.5em 0;
|
|
|
|
th {
|
|
text-align: left;
|
|
}
|
|
|
|
td {
|
|
font-family: var(--monospace-font);
|
|
line-break: anywhere;
|
|
|
|
border-radius: 4px;
|
|
padding: 2px;
|
|
|
|
&.attr-action {
|
|
max-width: 1em;
|
|
}
|
|
|
|
&.formatted,
|
|
.formatted {
|
|
font-family: var(--default-font);
|
|
}
|
|
}
|
|
|
|
.action-col {
|
|
width: 1.5em;
|
|
}
|
|
|
|
.attribute-col {
|
|
width: 33%;
|
|
}
|
|
}
|
|
</style>
|