2021-11-11 23:37:42 +01:00
|
|
|
<script lang="ts">
|
|
|
|
import filesize from "filesize";
|
|
|
|
import { format, fromUnixTime } from "date-fns";
|
2021-12-21 20:02:47 +01:00
|
|
|
import Ellipsis from "../utils/Ellipsis.svelte";
|
|
|
|
import UpObject from "../display/UpObject.svelte";
|
2021-12-04 23:06:21 +01:00
|
|
|
import { createEventDispatcher, getContext } from "svelte";
|
2022-01-09 21:46:52 +01:00
|
|
|
import type { AttributeChange, AttributeUpdate } from "../../types/base";
|
2021-12-04 23:06:21 +01:00
|
|
|
import { useParams } from "svelte-navigator";
|
|
|
|
import type { Writable } from "svelte/store";
|
2021-12-19 13:54:16 +01:00
|
|
|
import type { UpEntry } from "upend";
|
2021-12-31 00:46:19 +01:00
|
|
|
import IconButton from "../utils/IconButton.svelte";
|
|
|
|
import Selector from "../utils/Selector.svelte";
|
2022-01-09 21:24:49 +01:00
|
|
|
import type { IValue } from "upend/types";
|
|
|
|
import Editable from "../utils/Editable.svelte";
|
2021-12-02 18:45:29 +01:00
|
|
|
const dispatch = createEventDispatcher();
|
2021-12-04 23:06:21 +01:00
|
|
|
const params = useParams();
|
2021-11-11 23:37:42 +01:00
|
|
|
|
2021-12-21 22:16:33 +01:00
|
|
|
export let columns: string;
|
2021-12-01 15:39:26 +01:00
|
|
|
export let header = true;
|
2021-11-30 22:59:34 +01:00
|
|
|
|
2021-12-19 19:20:09 +01:00
|
|
|
export let entries: UpEntry[];
|
2021-11-11 23:37:42 +01:00
|
|
|
export let editable = false;
|
|
|
|
|
2021-12-01 15:39:26 +01:00
|
|
|
// Display
|
2021-12-21 22:16:33 +01:00
|
|
|
$: columns = columns || "attribute, value";
|
2021-12-01 15:39:26 +01:00
|
|
|
$: showEntity = columns.includes("entity");
|
|
|
|
$: showAttribute = columns.includes("attribute");
|
|
|
|
$: showValue = columns.includes("value");
|
|
|
|
|
2021-11-30 23:18:39 +01:00
|
|
|
// Editing
|
2021-12-31 00:46:19 +01:00
|
|
|
let newEntryAttribute = "";
|
2022-01-09 21:24:49 +01:00
|
|
|
let newEntryValue: IValue | undefined;
|
2021-11-30 23:18:39 +01:00
|
|
|
|
2021-11-11 23:37:42 +01:00
|
|
|
async function addEntry() {
|
2021-12-02 18:45:29 +01:00
|
|
|
dispatch("change", {
|
|
|
|
type: "create",
|
|
|
|
attribute: newEntryAttribute,
|
|
|
|
value: newEntryValue,
|
|
|
|
} as AttributeChange);
|
|
|
|
newEntryAttribute = "";
|
2022-01-09 21:24:49 +01:00
|
|
|
newEntryValue = undefined;
|
2021-11-11 23:37:42 +01:00
|
|
|
}
|
2022-01-09 21:46:52 +01:00
|
|
|
async function removeEntry(address: string) {
|
2021-12-02 18:45:29 +01:00
|
|
|
if (confirm("Are you sure you want to remove the attribute?")) {
|
2022-01-09 21:46:52 +01:00
|
|
|
dispatch("change", { type: "delete", address } as AttributeChange);
|
2021-12-02 18:45:29 +01:00
|
|
|
}
|
2021-11-11 23:37:42 +01:00
|
|
|
}
|
2022-01-09 21:46:52 +01:00
|
|
|
async function updateEntry(address: string, attribute: string, value: IValue) {
|
2021-12-02 18:45:29 +01:00
|
|
|
dispatch("change", {
|
|
|
|
type: "update",
|
2022-01-09 21:46:52 +01:00
|
|
|
address,
|
2021-12-02 18:45:29 +01:00
|
|
|
attribute,
|
|
|
|
value,
|
2022-01-09 21:46:52 +01:00
|
|
|
} as AttributeUpdate);
|
2021-11-11 23:37:42 +01:00
|
|
|
}
|
|
|
|
|
2021-11-30 23:18:39 +01:00
|
|
|
// Sorting
|
2021-12-20 14:04:44 +01:00
|
|
|
let sortedAttributes = entries;
|
|
|
|
|
2021-12-20 13:22:30 +01:00
|
|
|
let sortKeys: { [key: string]: string[] } = {};
|
2021-12-20 14:11:53 +01:00
|
|
|
function addSortKeys(key: string, vals: string[], resort = true) {
|
2021-12-20 13:22:30 +01:00
|
|
|
if (!sortKeys[key]) {
|
|
|
|
sortKeys[key] = [];
|
|
|
|
}
|
2021-12-20 14:11:53 +01:00
|
|
|
let changed = false;
|
2021-12-20 13:22:30 +01:00
|
|
|
vals.forEach((val) => {
|
|
|
|
if (!sortKeys[key].includes(val)) {
|
2021-12-20 14:11:53 +01:00
|
|
|
changed = true;
|
2021-12-20 13:22:30 +01:00
|
|
|
sortKeys[key].push(val);
|
|
|
|
}
|
|
|
|
});
|
2021-12-20 14:11:53 +01:00
|
|
|
|
|
|
|
if (resort && changed) sortAttributes();
|
2021-12-20 13:22:30 +01:00
|
|
|
}
|
|
|
|
|
2021-12-20 14:04:44 +01:00
|
|
|
function sortAttributes() {
|
|
|
|
sortedAttributes = entries
|
|
|
|
.concat()
|
|
|
|
.sort((aEntry, bEntry) => {
|
2021-12-20 13:22:30 +01:00
|
|
|
if (
|
2022-01-28 16:46:08 +01:00
|
|
|
!sortKeys[aEntry.value.c]?.length ||
|
|
|
|
!sortKeys[bEntry.value.c]?.length
|
2021-12-20 13:22:30 +01:00
|
|
|
) {
|
2021-12-20 14:04:44 +01:00
|
|
|
if (
|
|
|
|
Boolean(sortKeys[aEntry.value.c]?.length) &&
|
2022-01-28 16:46:08 +01:00
|
|
|
!sortKeys[bEntry.value.c]?.length
|
2021-12-20 14:04:44 +01:00
|
|
|
) {
|
|
|
|
return -1;
|
|
|
|
} else if (
|
2022-01-28 16:46:08 +01:00
|
|
|
!sortKeys[aEntry.value.c]?.length &&
|
2021-12-20 14:04:44 +01:00
|
|
|
Boolean(sortKeys[bEntry.value.c]?.length)
|
|
|
|
) {
|
|
|
|
return 1;
|
|
|
|
} else {
|
|
|
|
return String(aEntry.value.c).localeCompare(String(bEntry.value.c));
|
|
|
|
}
|
2021-12-01 20:13:01 +01:00
|
|
|
} else {
|
2021-12-20 14:04:44 +01:00
|
|
|
return sortKeys[aEntry.value.c][0].localeCompare(
|
|
|
|
sortKeys[bEntry.value.c][0],
|
|
|
|
undefined,
|
|
|
|
{ numeric: true, sensitivity: "base" }
|
|
|
|
);
|
2021-12-01 20:13:01 +01:00
|
|
|
}
|
2021-12-20 14:04:44 +01:00
|
|
|
})
|
|
|
|
.sort((aEntry, bEntry) => {
|
|
|
|
return String(aEntry.value.c).length - String(bEntry.value.c).length;
|
|
|
|
})
|
|
|
|
.sort((aEntry, bEntry) => {
|
|
|
|
return aEntry.attribute.localeCompare(bEntry.attribute);
|
|
|
|
})
|
|
|
|
.sort((aEntry, bEntry) => {
|
2021-12-20 13:22:30 +01:00
|
|
|
if (
|
2022-01-28 16:46:08 +01:00
|
|
|
!sortKeys[aEntry.entity]?.length ||
|
|
|
|
!sortKeys[bEntry.entity]?.length
|
2021-12-20 13:22:30 +01:00
|
|
|
) {
|
2021-12-20 14:04:44 +01:00
|
|
|
if (
|
|
|
|
Boolean(sortKeys[aEntry.entity]?.length) &&
|
2022-01-28 16:46:08 +01:00
|
|
|
!sortKeys[bEntry.entity]?.length
|
2021-12-20 14:04:44 +01:00
|
|
|
) {
|
|
|
|
return -1;
|
|
|
|
} else if (
|
2022-01-28 16:46:08 +01:00
|
|
|
!sortKeys[aEntry.entity]?.length &&
|
2021-12-20 14:04:44 +01:00
|
|
|
Boolean(sortKeys[bEntry.entity]?.length)
|
|
|
|
) {
|
|
|
|
return 1;
|
|
|
|
} else {
|
|
|
|
return aEntry.entity.localeCompare(bEntry.entity);
|
|
|
|
}
|
2021-12-01 20:13:01 +01:00
|
|
|
} else {
|
2021-12-20 14:04:44 +01:00
|
|
|
return sortKeys[aEntry.entity][0].localeCompare(
|
|
|
|
sortKeys[bEntry.entity][0]
|
|
|
|
);
|
2021-12-01 20:13:01 +01:00
|
|
|
}
|
2021-12-20 14:04:44 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
entries.forEach((entry) => {
|
|
|
|
addSortKeys(entry.entity, entry.listing.getObject(entry.entity).identify());
|
|
|
|
if (entry.value.t === "Address") {
|
|
|
|
addSortKeys(
|
|
|
|
String(entry.value.c),
|
2021-12-20 14:11:53 +01:00
|
|
|
entry.listing.getObject(String(entry.value.c)).identify(),
|
|
|
|
false
|
2021-12-20 14:04:44 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
2021-11-30 23:18:39 +01:00
|
|
|
|
2021-12-20 14:11:53 +01:00
|
|
|
sortAttributes();
|
|
|
|
|
2021-12-04 23:06:21 +01:00
|
|
|
// Navigation highlights
|
|
|
|
const { index } = getContext("browse") as { index: Writable<number> };
|
|
|
|
$: addresses = $params.addresses.split(",");
|
|
|
|
|
2021-11-30 23:18:39 +01:00
|
|
|
// Formatting & Display
|
2021-11-11 23:37:42 +01:00
|
|
|
const ATTRIBUTE_LABELS: { [key: string]: string } = {
|
|
|
|
FILE_MIME: "MIME type",
|
|
|
|
FILE_MTIME: "Last modified",
|
|
|
|
FILE_SIZE: "File size",
|
|
|
|
};
|
|
|
|
|
2021-12-19 13:54:16 +01:00
|
|
|
const VALUE_FORMATTERS: { [key: string]: (val: string | number) => string } =
|
|
|
|
{
|
|
|
|
FILE_MTIME: (val) =>
|
|
|
|
format(fromUnixTime(parseInt(String(val), 10)), "PPpp"),
|
|
|
|
FILE_SIZE: (val) => filesize(parseInt(String(val), 10), { base: 2 }),
|
|
|
|
};
|
2021-11-11 23:37:42 +01:00
|
|
|
|
|
|
|
function formatAttribute(attribute: string) {
|
|
|
|
return ATTRIBUTE_LABELS[attribute];
|
|
|
|
}
|
|
|
|
|
2022-01-08 19:05:01 +01:00
|
|
|
function formatValue(value: string | number, attribute: string): string {
|
2021-11-11 23:37:42 +01:00
|
|
|
const handler = VALUE_FORMATTERS[attribute];
|
|
|
|
if (handler) {
|
2022-01-08 19:05:01 +01:00
|
|
|
try {
|
2022-01-09 21:24:49 +01:00
|
|
|
return handler(value);
|
2022-01-08 19:05:01 +01:00
|
|
|
} catch (error) {
|
|
|
|
console.warn(`Error while formatting "${value}": ${error}`);
|
2022-01-09 21:24:49 +01:00
|
|
|
}
|
2021-11-11 23:37:42 +01:00
|
|
|
}
|
2022-01-08 19:05:01 +01:00
|
|
|
return String(value);
|
|
|
|
}
|
2021-11-30 23:18:39 +01:00
|
|
|
|
|
|
|
// Optimizations
|
|
|
|
let resolve = [];
|
2021-11-11 23:37:42 +01:00
|
|
|
</script>
|
|
|
|
|
2021-11-30 22:59:34 +01:00
|
|
|
<table>
|
|
|
|
<colgroup>
|
|
|
|
{#if editable}
|
|
|
|
<col class="attr-action-col" />
|
|
|
|
{/if}
|
2021-12-01 15:39:26 +01:00
|
|
|
{#if showEntity}
|
2021-11-30 22:59:34 +01:00
|
|
|
<col class="entity-col" />
|
|
|
|
{/if}
|
2021-12-01 15:39:26 +01:00
|
|
|
{#if showAttribute}
|
2021-11-30 22:59:34 +01:00
|
|
|
<col class="attr-col" />
|
|
|
|
{/if}
|
2021-12-01 15:39:26 +01:00
|
|
|
{#if showValue}
|
2021-11-30 22:59:34 +01:00
|
|
|
<col class="val-col" />
|
|
|
|
{/if}
|
|
|
|
</colgroup>
|
|
|
|
|
2021-12-01 15:39:26 +01:00
|
|
|
{#if header}
|
|
|
|
<tr>
|
|
|
|
{#if editable}
|
|
|
|
<th />
|
|
|
|
{/if}
|
|
|
|
{#if showEntity}
|
|
|
|
<th>Entity</th>
|
|
|
|
{/if}
|
|
|
|
{#if showAttribute}
|
|
|
|
<th>Attribute</th>
|
|
|
|
{/if}
|
|
|
|
{#if showValue}
|
|
|
|
<th>Value</th>
|
|
|
|
{/if}
|
|
|
|
</tr>
|
|
|
|
{/if}
|
2021-11-30 22:59:34 +01:00
|
|
|
|
2021-12-19 13:54:16 +01:00
|
|
|
{#each sortedAttributes as entry (entry.address)}
|
2021-12-04 23:06:21 +01:00
|
|
|
<tr
|
|
|
|
class:left-active={entry.entity == addresses[$index - 1] ||
|
|
|
|
entry.value.c == addresses[$index - 1]}
|
|
|
|
class:right-active={entry.value.c == addresses[$index + 1] ||
|
|
|
|
entry.entity == addresses[$index + 1]}
|
|
|
|
>
|
2021-11-11 23:37:42 +01:00
|
|
|
{#if editable}
|
2021-11-30 22:59:34 +01:00
|
|
|
<td class="attr-action">
|
2021-12-30 19:28:43 +01:00
|
|
|
<IconButton
|
2021-12-19 13:54:16 +01:00
|
|
|
name="x-circle"
|
2021-12-30 19:28:43 +01:00
|
|
|
on:click={() => removeEntry(entry.address)}
|
2021-12-19 13:54:16 +01:00
|
|
|
/>
|
2021-11-30 22:59:34 +01:00
|
|
|
</td>
|
2021-11-11 23:37:42 +01:00
|
|
|
{/if}
|
2021-11-30 22:59:34 +01:00
|
|
|
|
2021-12-01 15:39:26 +01:00
|
|
|
{#if showEntity}
|
2021-12-01 20:07:42 +01:00
|
|
|
<td class="entity">
|
2021-12-21 18:47:54 +01:00
|
|
|
<UpObject
|
2021-12-01 20:07:42 +01:00
|
|
|
link
|
2021-12-19 19:20:09 +01:00
|
|
|
labels={entry.listing.getObject(String(entry.entity)).identify()}
|
2021-12-01 20:07:42 +01:00
|
|
|
address={entry.entity}
|
|
|
|
on:resolved={(event) => {
|
2021-12-20 13:22:30 +01:00
|
|
|
addSortKeys(entry.entity, event.detail);
|
2021-12-01 20:07:42 +01:00
|
|
|
}}
|
|
|
|
/>
|
2021-11-30 22:59:34 +01:00
|
|
|
</td>
|
2021-11-11 23:37:42 +01:00
|
|
|
{/if}
|
2021-11-30 22:59:34 +01:00
|
|
|
|
2021-12-01 15:39:26 +01:00
|
|
|
{#if showAttribute}
|
2021-11-30 22:59:34 +01:00
|
|
|
<td class:formatted={Boolean(formatAttribute(entry.attribute))}>
|
2021-12-11 21:37:12 +01:00
|
|
|
<Ellipsis
|
|
|
|
value={formatAttribute(entry.attribute) || entry.attribute}
|
|
|
|
/>
|
2021-11-30 22:59:34 +01:00
|
|
|
</td>
|
2021-11-11 23:37:42 +01:00
|
|
|
{/if}
|
2021-11-30 22:59:34 +01:00
|
|
|
|
2021-12-01 15:39:26 +01:00
|
|
|
{#if showValue}
|
2021-11-30 22:59:34 +01:00
|
|
|
<td class="value">
|
2022-01-09 21:24:49 +01:00
|
|
|
<Editable
|
2021-11-30 22:59:34 +01:00
|
|
|
{editable}
|
2022-01-09 21:24:49 +01:00
|
|
|
value={entry.value}
|
|
|
|
on:edit={(ev) =>
|
|
|
|
updateEntry(entry.address, entry.attribute, ev.detail)}
|
2021-11-30 22:59:34 +01:00
|
|
|
>
|
|
|
|
{#if entry.value.t === "Address"}
|
2021-12-21 18:47:54 +01:00
|
|
|
<UpObject
|
2021-11-30 22:59:34 +01:00
|
|
|
link
|
2021-12-19 13:54:16 +01:00
|
|
|
address={String(entry.value.c)}
|
2021-12-19 19:20:09 +01:00
|
|
|
labels={entry.listing
|
|
|
|
.getObject(String(entry.value.c))
|
|
|
|
.identify()}
|
2021-12-19 13:54:16 +01:00
|
|
|
resolve={Boolean(resolve[entry.address]) || true}
|
2021-11-30 23:18:39 +01:00
|
|
|
on:resolved={(event) => {
|
2021-12-20 13:22:30 +01:00
|
|
|
addSortKeys(String(entry.value.c), event.detail);
|
2021-11-30 23:18:39 +01:00
|
|
|
}}
|
2021-11-30 22:59:34 +01:00
|
|
|
/>
|
|
|
|
{:else}
|
|
|
|
<div
|
|
|
|
class:formatted={Boolean(
|
|
|
|
formatValue(entry.value.c, entry.attribute)
|
|
|
|
)}
|
|
|
|
>
|
2021-12-11 21:37:12 +01:00
|
|
|
<Ellipsis
|
|
|
|
value={formatValue(entry.value.c, entry.attribute) ||
|
2021-12-19 13:54:16 +01:00
|
|
|
String(entry.value.c)}
|
2021-12-11 21:37:12 +01:00
|
|
|
/>
|
2021-11-30 22:59:34 +01:00
|
|
|
</div>
|
|
|
|
{/if}
|
2022-01-09 21:24:49 +01:00
|
|
|
</Editable>
|
2021-11-30 22:59:34 +01:00
|
|
|
</td>
|
|
|
|
{/if}
|
|
|
|
</tr>
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
{#if editable}
|
2022-01-09 21:24:49 +01:00
|
|
|
<tr class="add-row">
|
2021-11-30 22:59:34 +01:00
|
|
|
<td class="attr-action">
|
2021-12-30 19:28:43 +01:00
|
|
|
<IconButton name="plus-circle" on:click={addEntry} />
|
2021-11-30 22:59:34 +01:00
|
|
|
</td>
|
2021-12-01 15:39:26 +01:00
|
|
|
{#if showAttribute}
|
2021-11-30 22:59:34 +01:00
|
|
|
<td>
|
2022-01-09 21:24:49 +01:00
|
|
|
<Selector type="attribute" bind:attribute={newEntryAttribute} />
|
2021-11-30 22:59:34 +01:00
|
|
|
</td>
|
|
|
|
{/if}
|
2021-12-01 15:39:26 +01:00
|
|
|
{#if showValue}
|
2021-11-30 22:59:34 +01:00
|
|
|
<td>
|
2022-01-09 21:24:49 +01:00
|
|
|
<Selector type="value" bind:value={newEntryValue} />
|
2021-11-30 22:59:34 +01:00
|
|
|
</td>
|
|
|
|
{/if}
|
|
|
|
</tr>
|
|
|
|
{/if}
|
|
|
|
</table>
|
2021-11-11 23:37:42 +01:00
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2021-12-30 23:24:38 +01:00
|
|
|
@use "../../styles/colors";
|
|
|
|
|
2021-11-11 23:37:42 +01:00
|
|
|
table {
|
|
|
|
width: 100%;
|
|
|
|
table-layout: fixed;
|
|
|
|
|
2021-12-06 14:35:18 +01:00
|
|
|
border-spacing: 0.5em 0;
|
2021-12-05 13:29:06 +01:00
|
|
|
|
2021-12-31 00:46:19 +01:00
|
|
|
tr {
|
2021-12-04 23:06:21 +01:00
|
|
|
&.left-active {
|
2021-12-06 14:35:18 +01:00
|
|
|
td:first-child {
|
|
|
|
background: linear-gradient(
|
|
|
|
90deg,
|
2021-12-30 23:24:38 +01:00
|
|
|
colors.$orange 0%,
|
2021-12-11 21:37:12 +01:00
|
|
|
transparent 100%
|
2021-12-06 14:35:18 +01:00
|
|
|
);
|
|
|
|
}
|
2021-12-04 23:06:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
&.right-active {
|
2021-12-06 14:35:18 +01:00
|
|
|
td:last-child {
|
|
|
|
background: linear-gradient(
|
|
|
|
90deg,
|
2021-12-11 21:37:12 +01:00
|
|
|
transparent 0%,
|
2021-12-30 23:24:38 +01:00
|
|
|
colors.$orange 100%
|
2021-12-06 14:35:18 +01:00
|
|
|
);
|
|
|
|
}
|
2021-12-04 23:06:21 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-11 23:37:42 +01:00
|
|
|
th {
|
|
|
|
text-align: left;
|
|
|
|
}
|
|
|
|
|
|
|
|
td {
|
|
|
|
font-family: var(--monospace-font);
|
|
|
|
line-break: anywhere;
|
|
|
|
|
2021-12-06 15:24:00 +01:00
|
|
|
border-radius: 4px;
|
|
|
|
padding: 2px;
|
|
|
|
|
2021-11-11 23:37:42 +01:00
|
|
|
&.attr-action {
|
|
|
|
max-width: 1em;
|
|
|
|
}
|
|
|
|
|
2021-12-01 16:04:23 +01:00
|
|
|
&.formatted,
|
|
|
|
.formatted {
|
2021-11-11 23:37:42 +01:00
|
|
|
font-family: var(--default-font);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.attr-action-col {
|
|
|
|
width: 1.5em;
|
|
|
|
}
|
|
|
|
|
|
|
|
.attr-col {
|
|
|
|
width: 33%;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|