feat(webui): rudimentary combine column

feat/vault-scan-modes
Tomáš Mládek 2023-10-22 20:38:10 +02:00
parent 6288e8faec
commit 37d5cee2ad
4 changed files with 294 additions and 13 deletions

View File

@ -6,7 +6,7 @@
import { selected } from "./EntitySelect.svelte";
const dispatch = createEventDispatcher();
export let detachUrl: string | undefined = undefined;
export let address: string | undefined = undefined;
export let only: boolean;
let detail = only;
@ -23,7 +23,7 @@
}
function visit() {
window.open(normUrl(detachUrl), "_blank");
window.open(normUrl(`/browse/${address}`), "_blank");
}
let width = 460;
@ -50,7 +50,7 @@
<div class="browse-column" class:detail>
<div class="view" style="--width: {width}px">
<header>
{#if detachUrl}
{#if address}
<IconButton name="link" on:click={() => visit()} disabled={only}>
Detach
</IconButton>
@ -65,6 +65,14 @@
>
Detail
</IconButton>
{#if address}
<IconButton
name="intersect"
on:click={() => dispatch("combine", address)}
>
Combine
</IconButton>
{/if}
<IconButton
name="x-circle"
on:click={() => dispatch("close")}

View File

@ -0,0 +1,140 @@
<script lang="ts">
import { i18n } from "../i18n";
import EntitySetEditor from "./EntitySetEditor.svelte";
import EntryView from "./EntryView.svelte";
import Icon from "./utils/Icon.svelte";
import EntityList from "./widgets/EntityList.svelte";
import api from "../lib/api";
import { Query } from "@upnd/upend";
import { ATTR_IN } from "@upnd/upend/constants";
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
export let spec: string;
const individualSpecs = spec.split(/(?=[+-])/);
let includedGroups = individualSpecs
.filter((s) => s.startsWith("+"))
.map((s) => s.slice(1));
let excludedGroups = individualSpecs
.filter((s) => s.startsWith("-"))
.map((s) => s.slice(1));
$: if (includedGroups.length == 0 && excludedGroups.length == 0) {
dispatch("close");
}
const combinedWidgets = [
{
name: "List",
icon: "list-check",
components: ({ entities }) => [
{
component: EntityList,
props: {
entities,
thumbnails: false,
},
},
],
},
{
name: "EntityList",
icon: "image",
components: ({ entities }) => [
{
component: EntityList,
props: {
entities,
thumbnails: true,
},
},
],
},
];
let resultEntities = [];
async function updateResultEntities(
includedGroups: string[],
excludedGroups: string[],
) {
const included = includedGroups.length
? (
await api.query(
new Query().matches(
undefined,
ATTR_IN,
includedGroups.map((g) => `@${g}`),
),
)
).objects
: [];
const excluded = excludedGroups.length
? (
await api.query(
new Query().matches(
undefined,
ATTR_IN,
excludedGroups.map((g) => `@${g}`),
),
)
).objects
: [];
resultEntities = Object.keys(included).filter((e) => !(e in excluded));
}
$: updateResultEntities(includedGroups, excludedGroups);
</script>
<div class="view">
<h2>
<Icon plain name="intersect" />
{$i18n.t("Combine")}
</h2>
<div class="controls">
<EntitySetEditor
entities={includedGroups}
header={$i18n.t("Include")}
on:add={(ev) => (includedGroups = [...includedGroups, ev.detail])}
on:remove={(ev) =>
(includedGroups = includedGroups.filter((e) => e !== ev.detail))}
/>
<EntitySetEditor
entities={excludedGroups}
header={$i18n.t("Exclude")}
on:add={(ev) => (excludedGroups = [...excludedGroups, ev.detail])}
on:remove={(ev) =>
(excludedGroups = excludedGroups.filter((e) => e !== ev.detail))}
/>
</div>
<div class="entities">
<EntryView
title={$i18n.t("Matching entities")}
entities={resultEntities}
widgets={combinedWidgets}
/>
</div>
</div>
<style lang="scss">
.view {
display: flex;
flex-direction: column;
height: 100%;
}
h2 {
text-align: center;
margin: 0;
margin-top: -0.66em;
}
.controls {
margin-bottom: 1rem;
}
.entities {
flex-grow: 1;
overflow-y: auto;
height: 0;
}
</style>

View File

@ -0,0 +1,115 @@
<script lang="ts">
import UpObjectDisplay from "./display/UpObject.svelte";
import Selector from "./utils/Selector.svelte";
import IconButton from "./utils/IconButton.svelte";
import type { IValue } from "@upnd/upend/types";
import { i18n } from "../i18n";
import LabelBorder from "./utils/LabelBorder.svelte";
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
export let entities: string[];
export let header = "";
export let confirmRemoveMessage = $i18n.t(
"Are you sure you want to remove this?",
);
export let emptyMessage = $i18n.t("Nothing to show.");
let adding = false;
let selector: Selector;
$: if (adding && selector) selector.focus();
let entityToAdd: IValue | undefined;
async function add() {
if (!entityToAdd) {
return;
}
dispatch("add", entityToAdd.c as string);
entityToAdd = undefined;
}
async function remove(address: string) {
if (confirm(confirmRemoveMessage)) {
dispatch("remove", address);
}
}
</script>
<LabelBorder>
<span slot="header">{header}</span>
{#if adding}
<div class="selector">
<Selector
bind:this={selector}
type="value"
valueTypes={["Address"]}
bind:value={entityToAdd}
on:input={add}
on:focus={(ev) => {
if (!ev.detail) adding = false;
}}
placeholder={$i18n.t("Choose an entity...")}
/>
</div>
{/if}
<div class="body">
<div class="group-list">
{#each entities as entity}
<div class="group">
<UpObjectDisplay address={entity} link />
<IconButton subdued name="x-circle" on:click={() => remove(entity)} />
</div>
{:else}
<div class="no-groups">
{emptyMessage}
</div>
{/each}
</div>
{#if !adding}
<div class="add-button">
<IconButton
outline
small
name="folder-plus"
on:click={() => (adding = true)}
/>
</div>
{/if}
</div>
</LabelBorder>
<style lang="scss">
.group-list {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 0.2rem;
align-items: center;
}
.group {
display: inline-flex;
align-items: center;
}
.body {
display: flex;
align-items: start;
.group-list {
flex-grow: 1;
}
}
.selector {
width: 100%;
margin-bottom: 0.5rem;
}
.no-groups {
opacity: 0.66;
}
</style>

View File

@ -6,6 +6,7 @@
import EntitySelect, { selected } from "../components/EntitySelect.svelte";
import SelectedColumn from "../components/SelectedColumn.svelte";
import Inspect from "../components/Inspect.svelte";
import CombineColumn from "../components/CombineColumn.svelte";
const navigate = useNavigate();
const params = useParams();
@ -65,6 +66,12 @@
}
$: only = addresses.length === 1 && !$selected.length;
function addCombine(address: string) {
let _addresses = addresses.concat();
_addresses.push(`+${address}`);
navigate(`/browse/${_addresses.join(",")}`);
}
$: updateTitle("Browse", identities.join(" / "));
</script>
@ -75,16 +82,27 @@
>
{#each addresses as address, index}
<div class="column" data-index={index}>
<BrowseColumn
detachUrl="/browse/{address}"
{only}
on:close={() => close(index)}
on:resolved={(ev) => onIdentified(index, ev)}
on:detail={(ev) => onDetailChanged(index, ev)}
let:detail
>
<Inspect {address} {index} {detail} on:resolved on:close />
</BrowseColumn>
{#if ["+", "-"].some((c) => address.includes(c))}
<BrowseColumn
{only}
on:close={() => close(index)}
on:detail={(ev) => onDetailChanged(index, ev)}
>
<CombineColumn spec={address} on:close={() => close(index)} />
</BrowseColumn>
{:else}
<BrowseColumn
{address}
{only}
on:close={() => close(index)}
on:resolved={(ev) => onIdentified(index, ev)}
on:detail={(ev) => onDetailChanged(index, ev)}
on:combine={() => addCombine(address)}
let:detail
>
<Inspect {address} {index} {detail} on:resolved on:close />
</BrowseColumn>
{/if}
</div>
{/each}
{#if $selected.length}