refactor: EntryList uses CSS grid instead of tables
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
parent
c3305efaaa
commit
ae0c588928
|
@ -8,6 +8,7 @@
|
|||
export let outline = false;
|
||||
export let subdued = false;
|
||||
export let small = false;
|
||||
export let plain = false;
|
||||
export let color: string | undefined = "var(--active-color, var(--primary))";
|
||||
</script>
|
||||
|
||||
|
@ -17,6 +18,7 @@
|
|||
class:outline
|
||||
class:subdued
|
||||
class:small
|
||||
class:plain
|
||||
{disabled}
|
||||
{title}
|
||||
style="--color: {color}"
|
||||
|
@ -39,6 +41,10 @@
|
|||
color: inherit;
|
||||
opacity: 0.66;
|
||||
|
||||
&.plain {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
|
|
|
@ -41,6 +41,21 @@
|
|||
const ATTR_COL = "attribute";
|
||||
const VALUE_COL = "value";
|
||||
|
||||
$: templateColumns = (
|
||||
(displayColumns || []).map((column, idx) => {
|
||||
if (columnWidths?.[idx]) return columnWidths[idx];
|
||||
|
||||
switch (column) {
|
||||
case ATTR_COL:
|
||||
return "minmax(1em, 1fr)";
|
||||
default:
|
||||
return "minmax(1em, 2fr)";
|
||||
}
|
||||
}) as string[]
|
||||
)
|
||||
.concat(["auto"])
|
||||
.join(" ");
|
||||
|
||||
// Editing
|
||||
let adding = false;
|
||||
let addHover = false;
|
||||
|
@ -229,255 +244,228 @@
|
|||
})();
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<table>
|
||||
<colgroup>
|
||||
{#each displayColumns as column, idx}
|
||||
{#if columnWidths?.length}
|
||||
<col
|
||||
class="{column}-col"
|
||||
style="width: {columnWidths[idx] || 'unset'}"
|
||||
/>
|
||||
{:else}
|
||||
<col class="{column}-col" />
|
||||
{/if}
|
||||
<div class="entry-list" style:--template-columns={templateColumns}>
|
||||
{#if header}
|
||||
<header>
|
||||
{#each displayColumns as column}
|
||||
<div class="label">
|
||||
{COLUMN_LABELS[column] || $attributeLabels[column] || column}
|
||||
</div>
|
||||
{/each}
|
||||
<col class="action-col" />
|
||||
</colgroup>
|
||||
<div class="attr-action"></div>
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
{#if header}
|
||||
<tr>
|
||||
{#each displayColumns as column}
|
||||
<th>{COLUMN_LABELS[column] || $attributeLabels[column] || column}</th>
|
||||
{/each}
|
||||
<th />
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#each sortedEntries as entry (entry.address)}
|
||||
<tr data-address={entry.address} use:observe>
|
||||
{#if visible.has(entry.address)}
|
||||
{#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 mark-entity">
|
||||
{#each sortedEntries as entry (entry.address)}
|
||||
{#if visible.has(entry.address)}
|
||||
{#each displayColumns as column}
|
||||
{#if column == TIMESTAMP_COL}
|
||||
<div class="cell" title={entry.timestamp}>
|
||||
{formatRelative(parseISO(entry.timestamp), new Date())}
|
||||
</div>
|
||||
{:else if column == PROVENANCE_COL}
|
||||
<div class="cell">{entry.provenance}</div>
|
||||
{:else if column == ENTITY_COL}
|
||||
<div class="cell entity mark-entity">
|
||||
<UpObject
|
||||
link
|
||||
labels={$labelListing
|
||||
?.getObject(String(entry.entity))
|
||||
?.identify() || []}
|
||||
address={entry.entity}
|
||||
on:resolved={(event) => {
|
||||
addSortKeys(entry.entity, event.detail, true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if column == ATTR_COL}
|
||||
<div
|
||||
class="cell mark-attribute"
|
||||
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>
|
||||
</div>
|
||||
{:else if column == VALUE_COL}
|
||||
<div class="cell value mark-value">
|
||||
<Editable
|
||||
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.entity))
|
||||
?.getObject(String(entry.value.c))
|
||||
?.identify() || []}
|
||||
address={entry.entity}
|
||||
on:resolved={(event) => {
|
||||
addSortKeys(entry.entity, event.detail, true);
|
||||
addSortKeys(String(entry.value.c), event.detail, true);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{:else if column == ATTR_COL}
|
||||
<td
|
||||
class:formatted={Boolean(
|
||||
Object.keys($attributeLabels).includes(entry.attribute),
|
||||
)}
|
||||
class="mark-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 mark-value">
|
||||
<Editable
|
||||
value={entry.value}
|
||||
on:edit={(ev) =>
|
||||
updateEntry(entry.address, entry.attribute, ev.detail)}
|
||||
{:else}
|
||||
<div
|
||||
class:formatted={Boolean(
|
||||
formatValue(entry.value.c, entry.attribute),
|
||||
)}
|
||||
>
|
||||
{#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, true);
|
||||
}}
|
||||
/>
|
||||
{: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}
|
||||
<td class="attr-action">
|
||||
<IconButton
|
||||
subdued
|
||||
name="x-circle"
|
||||
on:click={() => removeEntry(entry.address)}
|
||||
/>
|
||||
</td>
|
||||
<Ellipsis
|
||||
value={formatValue(entry.value.c, entry.attribute) ||
|
||||
String(entry.value.c)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</Editable>
|
||||
</div>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="99">
|
||||
<div class="skeleton" style="text-align: center">...</div>
|
||||
</td>
|
||||
</tr>
|
||||
<div>?</div>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
{/each}
|
||||
<div class="attr-action">
|
||||
<IconButton
|
||||
plain
|
||||
subdued
|
||||
name="x-circle"
|
||||
color="#dc322f"
|
||||
on:click={() => removeEntry(entry.address)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="skeleton" data-address={entry.address} use:observe>...</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#each unusedAttributes as attribute}
|
||||
<tr>
|
||||
{#each unusedAttributes as attribute}
|
||||
{#each displayColumns as column}
|
||||
{#if column == ATTR_COL}
|
||||
<div
|
||||
class="cell mark-attribute"
|
||||
class:formatted={Boolean(
|
||||
Object.keys($attributeLabels).includes(attribute),
|
||||
)}
|
||||
>
|
||||
<UpLink to={{ attribute }}>
|
||||
<Ellipsis
|
||||
value={$attributeLabels[attribute] || attribute}
|
||||
title={$attributeLabels[attribute]
|
||||
? `${$attributeLabels[attribute]} (${attribute})`
|
||||
: attribute}
|
||||
/>
|
||||
</UpLink>
|
||||
</div>
|
||||
{:else if column == VALUE_COL}
|
||||
<div class="cell">
|
||||
<Editable on:edit={(ev) => addEntry(attribute, ev.detail)}>
|
||||
<span class="unset">{$i18n.t("(unset)")}</span>
|
||||
</Editable>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="cell"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="attr-action"></div>
|
||||
{/each}
|
||||
|
||||
{#if !attributes?.length}
|
||||
{#if adding}
|
||||
<div
|
||||
class="add-row"
|
||||
on:mouseenter={() => (addHover = true)}
|
||||
on:mouseleave={() => (addHover = false)}
|
||||
>
|
||||
{#each displayColumns as column}
|
||||
{#if column == ATTR_COL}
|
||||
<td
|
||||
class:formatted={Boolean(
|
||||
Object.keys($attributeLabels).includes(attribute),
|
||||
)}
|
||||
class="mark-attribute"
|
||||
>
|
||||
<UpLink to={{ attribute }}>
|
||||
<Ellipsis
|
||||
value={$attributeLabels[attribute] || attribute}
|
||||
title={$attributeLabels[attribute]
|
||||
? `${$attributeLabels[attribute]} (${attribute})`
|
||||
: attribute}
|
||||
/>
|
||||
</UpLink>
|
||||
</td>
|
||||
{:else if column == VALUE_COL}
|
||||
<td>
|
||||
<Editable on:edit={(ev) => addEntry(attribute, ev.detail)}>
|
||||
<span class="unset">{$i18n.t("(unset)")}</span>
|
||||
</Editable>
|
||||
</td>
|
||||
<div class="cell mark-attribute">
|
||||
<Selector
|
||||
type="attribute"
|
||||
bind:attribute={newEntryAttribute}
|
||||
{attributeOptions}
|
||||
on:focus={(ev) => (addFocus = ev.detail)}
|
||||
bind:this={newAttrSelector}
|
||||
/>
|
||||
</div>
|
||||
{:else if column === VALUE_COL}
|
||||
<div class="cell mark-value">
|
||||
<Selector
|
||||
type="value"
|
||||
bind:value={newEntryValue}
|
||||
on:focus={(ev) => (addFocus = ev.detail)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<td></td>
|
||||
<div class="cell"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
<td class="attr-action"></td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
{#if !attributes?.length}
|
||||
{#if adding}
|
||||
<tr
|
||||
class="add-row"
|
||||
on:mouseenter={() => (addHover = true)}
|
||||
on:mouseleave={() => (addHover = false)}
|
||||
>
|
||||
{#each displayColumns as column}
|
||||
{#if column == ATTR_COL}
|
||||
<td>
|
||||
<Selector
|
||||
type="attribute"
|
||||
bind:attribute={newEntryAttribute}
|
||||
{attributeOptions}
|
||||
on:focus={(ev) => (addFocus = ev.detail)}
|
||||
bind:this={newAttrSelector}
|
||||
/>
|
||||
</td>
|
||||
{:else if column === VALUE_COL}
|
||||
<td>
|
||||
<Selector
|
||||
type="value"
|
||||
bind:value={newEntryValue}
|
||||
on:focus={(ev) => (addFocus = ev.detail)}
|
||||
/>
|
||||
</td>
|
||||
{:else}
|
||||
<td></td>
|
||||
{/if}
|
||||
{/each}
|
||||
<td class="attr-action">
|
||||
<IconButton
|
||||
name="save"
|
||||
on:click={() => addEntry(newEntryAttribute, newEntryValue)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td class="fullwidth" colspan={displayColumns.length + 1}>
|
||||
<IconButton
|
||||
outline
|
||||
subdued
|
||||
name="plus-circle"
|
||||
on:click={() => (adding = true)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
<div class="attr-action">
|
||||
<IconButton
|
||||
name="save"
|
||||
on:click={() => addEntry(newEntryAttribute, newEntryValue)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="add-button">
|
||||
<IconButton
|
||||
outline
|
||||
subdued
|
||||
name="plus-circle"
|
||||
on:click={() => (adding = true)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
overflow-x: clip;
|
||||
}
|
||||
.entry-list {
|
||||
display: grid;
|
||||
grid-template-columns: var(--template-columns);
|
||||
gap: 0.05rem 0.5rem;
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
header {
|
||||
display: contents;
|
||||
|
||||
// gaps between columns, but hug the edges
|
||||
width: calc(100% + 1em);
|
||||
margin-left: -0.5em;
|
||||
border-spacing: 0.5em 0;
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
.label {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
.cell {
|
||||
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;
|
||||
.attr-action {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.attribute-col {
|
||||
width: 33%;
|
||||
.add-row {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
:global(td.fullwidth > *) {
|
||||
width: 100%;
|
||||
.add-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.unset {
|
||||
|
|
Loading…
Reference in New Issue