refactor(webui): Selector refactor, non-destructive search
ci/woodpecker/push/woodpecker Pipeline failed
Details
ci/woodpecker/push/woodpecker Pipeline failed
Details
parent
91d8688bc9
commit
48a398f4f2
|
@ -38,10 +38,6 @@ export type IValue =
|
|||
| {
|
||||
t: "Null";
|
||||
c: null;
|
||||
}
|
||||
| {
|
||||
t: "Invalid";
|
||||
c: null;
|
||||
};
|
||||
|
||||
export interface InvariantEntry {
|
||||
|
|
|
@ -31,8 +31,7 @@
|
|||
<div class="controls">
|
||||
<Selector
|
||||
bind:this={selector}
|
||||
type="value"
|
||||
valueTypes={["Address"]}
|
||||
types={["Address", "NewAddress", "Attribute"]}
|
||||
on:input={(ev) => {
|
||||
dispatch("input", ev.detail);
|
||||
editable = false;
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<script lang="ts">
|
||||
import UpObjectDisplay from "./display/UpObject.svelte";
|
||||
import Selector from "./utils/Selector.svelte";
|
||||
import Selector, { type SelectorValue } 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";
|
||||
|
@ -22,13 +21,11 @@
|
|||
|
||||
$: if (adding && selector) selector.focus();
|
||||
|
||||
let entityToAdd: IValue | undefined;
|
||||
async function add() {
|
||||
if (!entityToAdd) {
|
||||
async function add(ev: CustomEvent<SelectorValue>) {
|
||||
if (ev.detail.t !== "Address") {
|
||||
return;
|
||||
}
|
||||
dispatch("add", entityToAdd.c as string);
|
||||
entityToAdd = undefined;
|
||||
dispatch("add", ev.detail.c);
|
||||
}
|
||||
|
||||
async function remove(address: string) {
|
||||
|
@ -45,9 +42,7 @@
|
|||
<div class="selector">
|
||||
<Selector
|
||||
bind:this={selector}
|
||||
type="value"
|
||||
valueTypes={["Address"]}
|
||||
bind:value={entityToAdd}
|
||||
types={["Address", "NewAddress"]}
|
||||
on:input={add}
|
||||
on:focus={(ev) => {
|
||||
if (!ev.detail) adding = false;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import UpObjectDisplay from "./display/UpObject.svelte";
|
||||
import Selector from "./utils/Selector.svelte";
|
||||
import Selector, { type SelectorValue } from "./utils/Selector.svelte";
|
||||
import IconButton from "./utils/IconButton.svelte";
|
||||
import api from "../lib/api";
|
||||
import { i18n } from "../i18n";
|
||||
|
@ -20,14 +20,15 @@
|
|||
|
||||
$: typeEntries = $entity?.attr[`~${ATTR_OF}`] || [];
|
||||
|
||||
let attributeToAdd: string;
|
||||
async function add() {
|
||||
if (!attributeToAdd) return;
|
||||
async function add(ev: CustomEvent<SelectorValue>) {
|
||||
if (ev.detail.t !== "Attribute") {
|
||||
return;
|
||||
}
|
||||
|
||||
await api.putEntry({
|
||||
entity: {
|
||||
t: "Attribute",
|
||||
c: attributeToAdd,
|
||||
c: ev.detail.name,
|
||||
},
|
||||
attribute: ATTR_OF,
|
||||
value: { t: "Address", c: $entity.address },
|
||||
|
@ -57,8 +58,7 @@
|
|||
<div class="selector">
|
||||
<Selector
|
||||
bind:this={typeSelector}
|
||||
type="attribute"
|
||||
bind:attribute={attributeToAdd}
|
||||
types={["Attribute", "NewAttribute"]}
|
||||
on:input={add}
|
||||
placeholder={$i18n.t("Assign an attribute to this type...")}
|
||||
on:focus={(ev) => {
|
||||
|
|
|
@ -367,9 +367,8 @@
|
|||
<div class="content">
|
||||
{#key currentAnnotation}
|
||||
<Selector
|
||||
type="value"
|
||||
valueTypes={["String", "Address"]}
|
||||
value={currentAnnotation.data}
|
||||
types={["String", "Address"]}
|
||||
initial={currentAnnotation.data}
|
||||
disabled={!editable}
|
||||
on:input={(ev) => {
|
||||
currentAnnotation.update({ data: ev.detail });
|
||||
|
|
|
@ -1,30 +1,49 @@
|
|||
<script lang="ts">
|
||||
import { Link, useLocation, useNavigate } from "svelte-navigator";
|
||||
import { useMatch } from "svelte-navigator";
|
||||
import { Link, useNavigate } from "svelte-navigator";
|
||||
// import { useMatch } from "svelte-navigator";
|
||||
import { addEmitter } from "../AddModal.svelte";
|
||||
import Icon from "../utils/Icon.svelte";
|
||||
import Input from "../utils/Input.svelte";
|
||||
import { jobsEmitter } from "./Jobs.svelte";
|
||||
import api from "../../lib/api";
|
||||
import Selector, { type SelectorValue } from "../utils/Selector.svelte";
|
||||
import { i18n } from "../../i18n";
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
// const location = useLocation();
|
||||
|
||||
const searchMatch = useMatch("/search/:query");
|
||||
// const searchMatch = useMatch("/search/:query");
|
||||
|
||||
let searchQuery = $searchMatch?.params.query
|
||||
? decodeURIComponent($searchMatch?.params.query)
|
||||
: "";
|
||||
$: if (!$location.pathname.includes("search")) searchQuery = "";
|
||||
function onInput(event: CustomEvent<string>) {
|
||||
searchQuery = event.detail;
|
||||
// let searchQuery = $searchMatch?.params.query
|
||||
// ? decodeURIComponent($searchMatch?.params.query)
|
||||
// : "";
|
||||
// $: if (!$location.pathname.includes("search")) searchQuery = "";
|
||||
|
||||
if (searchQuery.length > 0) {
|
||||
navigate(`/search/${encodeURIComponent(searchQuery)}`, {
|
||||
replace: $location.pathname.includes("search"),
|
||||
});
|
||||
} else {
|
||||
navigate("/");
|
||||
let selector: Selector;
|
||||
|
||||
async function onInput(event: CustomEvent<SelectorValue>) {
|
||||
const value = event.detail;
|
||||
switch (value.t) {
|
||||
case "Address":
|
||||
navigate(`/browse/${value.c}`);
|
||||
break;
|
||||
case "Attribute":
|
||||
{
|
||||
const attributeAddress = await api.componentsToAddress({
|
||||
t: "Attribute",
|
||||
c: value.name,
|
||||
});
|
||||
navigate(`/browse/${attributeAddress}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
selector.reset();
|
||||
|
||||
// searchQuery = event.detail;
|
||||
|
||||
// if (searchQuery.length > 0) {
|
||||
// navigate(`/search/${encodeURIComponent(searchQuery)}`, {
|
||||
// replace: $location.pathname.includes("search"),
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
let fileInput: HTMLInputElement;
|
||||
|
@ -48,13 +67,14 @@
|
|||
</Link>
|
||||
</h1>
|
||||
<div class="input">
|
||||
<Input
|
||||
placeholder="Search or add..."
|
||||
value={searchQuery}
|
||||
<Selector
|
||||
types={["Address", "NewAddress", "Attribute"]}
|
||||
placeholder={$i18n.t("Search or add")}
|
||||
on:input={onInput}
|
||||
bind:this={selector}
|
||||
>
|
||||
<Icon name="search" slot="prefix" />
|
||||
</Input>
|
||||
</Selector>
|
||||
</div>
|
||||
<button class="button" on:click={() => fileInput.click()}>
|
||||
<Icon name="upload" />
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
<script lang="ts">
|
||||
import Selector from "./Selector.svelte";
|
||||
import Selector, {
|
||||
type SELECTOR_TYPE,
|
||||
type SelectorValue,
|
||||
} from "./Selector.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { IValue } from "@upnd/upend/types";
|
||||
import IconButton from "./IconButton.svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let value: IValue | undefined = undefined;
|
||||
let newValue: IValue = value;
|
||||
export let types: SELECTOR_TYPE[] | undefined = undefined;
|
||||
let newValue: SelectorValue = value;
|
||||
|
||||
let editing = false;
|
||||
|
||||
|
@ -16,6 +20,11 @@
|
|||
|
||||
$: if (editing && selector) selector.focus();
|
||||
$: if (!focus && !hover) editing = false;
|
||||
|
||||
function onInput(ev: CustomEvent<SelectorValue>) {
|
||||
newValue = ev.detail;
|
||||
selector.focus();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -35,11 +44,10 @@
|
|||
}}
|
||||
>
|
||||
<Selector
|
||||
type="value"
|
||||
bind:value={newValue}
|
||||
{types}
|
||||
bind:this={selector}
|
||||
on:focus={(ev) => (focus = ev.detail)}
|
||||
on:input={() => selector.focus()}
|
||||
on:input={onInput}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
|
|
|
@ -1,70 +1,154 @@
|
|||
<script lang="ts" context="module">
|
||||
export type SELECTOR_TYPE =
|
||||
| "Address"
|
||||
| "NewAddress"
|
||||
| "Attribute"
|
||||
| "NewAttribute"
|
||||
| "String"
|
||||
| "Number"
|
||||
| "Null";
|
||||
|
||||
export type SelectorValue = {
|
||||
t: SELECTOR_TYPE;
|
||||
} & (
|
||||
| {
|
||||
t: "Address";
|
||||
c: Address;
|
||||
labels?: string[];
|
||||
}
|
||||
| {
|
||||
t: "Attribute";
|
||||
name: string;
|
||||
labels?: string[];
|
||||
}
|
||||
| {
|
||||
t: "String";
|
||||
c: string;
|
||||
}
|
||||
| {
|
||||
t: "Number";
|
||||
c: number;
|
||||
}
|
||||
| {
|
||||
t: "Null";
|
||||
c: null;
|
||||
}
|
||||
);
|
||||
|
||||
export type SelectorOption =
|
||||
| SelectorValue
|
||||
| { t: "NewAddress"; c: string }
|
||||
| { t: "NewAttribute"; name: string; label: string };
|
||||
|
||||
export async function selectorValueAsValue(
|
||||
value: SelectorValue,
|
||||
): Promise<IValue> {
|
||||
switch (value.t) {
|
||||
case "Address":
|
||||
return {
|
||||
t: "Address",
|
||||
c: value.c,
|
||||
};
|
||||
case "Attribute":
|
||||
return {
|
||||
t: "Address",
|
||||
c: await api.componentsToAddress({ t: "Attribute", c: value.name }),
|
||||
};
|
||||
case "String":
|
||||
return {
|
||||
t: "String",
|
||||
c: value.c,
|
||||
};
|
||||
case "Number":
|
||||
return {
|
||||
t: "Number",
|
||||
c: value.c,
|
||||
};
|
||||
case "Null":
|
||||
return {
|
||||
t: "Null",
|
||||
c: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { debounce } from "lodash";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { UpListing } from "@upnd/upend";
|
||||
import type { IValue, VALUE_TYPE } from "@upnd/upend/types";
|
||||
import type { Address, IValue } from "@upnd/upend/types";
|
||||
import { baseSearchOnce, createLabelled } from "../../util/search";
|
||||
import UpObject from "../display/UpObject.svelte";
|
||||
import IconButton from "./IconButton.svelte";
|
||||
import Input from "./Input.svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
import { matchSorter } from "match-sorter";
|
||||
import api from "../../lib/api";
|
||||
import { ATTR_LABEL } from "@upnd/upend/constants";
|
||||
import { i18n } from "../../i18n";
|
||||
import debug from "debug";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const dbg = debug("kestrel:Selector");
|
||||
|
||||
const MAX_OPTIONS = 25;
|
||||
export let MAX_OPTIONS = 25;
|
||||
|
||||
export let type: "attribute" | "value";
|
||||
export let types: SELECTOR_TYPE[] = [
|
||||
"Address",
|
||||
"NewAddress",
|
||||
"Attribute",
|
||||
"String",
|
||||
"Number",
|
||||
];
|
||||
export let attributeOptions: string[] | undefined = undefined;
|
||||
export let valueTypes: VALUE_TYPE[] | undefined = undefined;
|
||||
export let placeholder = "";
|
||||
export let disabled = false;
|
||||
|
||||
export let attribute: string | undefined = undefined;
|
||||
export let value: IValue | undefined = undefined;
|
||||
|
||||
if (type == "attribute") {
|
||||
value = {
|
||||
c: attribute,
|
||||
t: "String",
|
||||
};
|
||||
}
|
||||
let inputValue = String(value?.c || "");
|
||||
$: if (value === undefined) inputValue = undefined;
|
||||
|
||||
function onInput(ev: CustomEvent<string>) {
|
||||
if (type == "attribute") {
|
||||
attribute = ev.detail;
|
||||
} else {
|
||||
value = {
|
||||
t: "String",
|
||||
c: ev.detail,
|
||||
};
|
||||
export let initial: SelectorValue | undefined = undefined;
|
||||
let inputValue = "";
|
||||
$: {
|
||||
if (initial) {
|
||||
switch (initial.t) {
|
||||
case "Address":
|
||||
case "String":
|
||||
inputValue = initial.c;
|
||||
break;
|
||||
case "Attribute":
|
||||
inputValue = initial.name;
|
||||
break;
|
||||
case "Number":
|
||||
inputValue = String(initial.c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectorOption {
|
||||
attribute?: {
|
||||
labels: string[];
|
||||
name: string;
|
||||
};
|
||||
value?: IValue;
|
||||
labelToCreate?: string;
|
||||
let current:
|
||||
| (SelectorOption & { t: "Address" | "Attribute" | "String" | "Number" })
|
||||
| undefined = undefined;
|
||||
|
||||
export function reset() {
|
||||
inputValue = "";
|
||||
current = undefined;
|
||||
dispatch("input", current);
|
||||
}
|
||||
|
||||
let options: SelectorOption[] = [];
|
||||
let searchResult: UpListing;
|
||||
const updateOptions = debounce(async (query: string, doSearch: boolean) => {
|
||||
switch (type) {
|
||||
case "attribute": {
|
||||
const allAttributes = await api.fetchAllAttributes();
|
||||
const attributes = attributeOptions
|
||||
? allAttributes.filter((attr) => attributeOptions.includes(attr.name))
|
||||
: allAttributes;
|
||||
options = attributes
|
||||
options = [];
|
||||
|
||||
if (query.length === 0 && !types.includes("Attribute")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (types.includes("Attribute")) {
|
||||
const allAttributes = await api.fetchAllAttributes();
|
||||
const attributes = attributeOptions
|
||||
? allAttributes.filter((attr) => attributeOptions.includes(attr.name))
|
||||
: allAttributes;
|
||||
options.push(
|
||||
...attributes
|
||||
.filter(
|
||||
(attr) =>
|
||||
attr.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
|
@ -72,110 +156,96 @@
|
|||
label.toLowerCase().includes(query.toLowerCase()),
|
||||
),
|
||||
)
|
||||
.map((attribute) => {
|
||||
return {
|
||||
attribute,
|
||||
};
|
||||
});
|
||||
const attributeToCreate = inputValue
|
||||
.toUpperCase()
|
||||
.replaceAll(/[^A-Z0-9]/g, "_");
|
||||
if (
|
||||
!attributeOptions &&
|
||||
inputValue &&
|
||||
!allAttributes.map((attr) => attr.name).includes(attributeToCreate)
|
||||
) {
|
||||
options.push({
|
||||
attribute: {
|
||||
labels: [],
|
||||
name: attributeToCreate,
|
||||
},
|
||||
labelToCreate: inputValue,
|
||||
});
|
||||
}
|
||||
options = options;
|
||||
break;
|
||||
.map(
|
||||
(attribute) =>
|
||||
({
|
||||
t: "Attribute",
|
||||
...attribute,
|
||||
}) as SelectorOption,
|
||||
),
|
||||
);
|
||||
|
||||
const attributeToCreate = query
|
||||
.toUpperCase()
|
||||
.replaceAll(/[^A-Z0-9]/g, "_");
|
||||
if (
|
||||
!attributeOptions &&
|
||||
query &&
|
||||
!allAttributes.map((attr) => attr.name).includes(attributeToCreate) &&
|
||||
types.includes("NewAttribute")
|
||||
) {
|
||||
options.push({
|
||||
t: "NewAttribute",
|
||||
name: attributeToCreate,
|
||||
label: query,
|
||||
});
|
||||
}
|
||||
case "value": {
|
||||
options = [];
|
||||
options = options;
|
||||
}
|
||||
|
||||
if (query.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (valueTypes === undefined || valueTypes.includes("Number")) {
|
||||
const number = parseFloat(query);
|
||||
if (!Number.isNaN(number)) {
|
||||
options.push({
|
||||
value: {
|
||||
t: "Number",
|
||||
c: number,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (valueTypes === undefined || valueTypes.includes("String")) {
|
||||
options.push({
|
||||
value: {
|
||||
t: "String",
|
||||
c: query,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (valueTypes === undefined || valueTypes.includes("Address")) {
|
||||
if (doSearch) {
|
||||
searchResult = await baseSearchOnce(query);
|
||||
}
|
||||
|
||||
let exactHits = Object.entries(addressToLabels)
|
||||
.filter(([_, labels]) =>
|
||||
labels
|
||||
.map((l) => l.toLowerCase())
|
||||
.includes(inputValue.toLowerCase()),
|
||||
)
|
||||
.map(([addr, _]) => addr);
|
||||
|
||||
if (exactHits.length) {
|
||||
exactHits.forEach((addr) =>
|
||||
options.push({
|
||||
value: {
|
||||
t: "Address",
|
||||
c: addr,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
options.push({
|
||||
labelToCreate: inputValue,
|
||||
});
|
||||
}
|
||||
|
||||
const validOptions = searchResult.entries
|
||||
.filter((e) => e.attribute === ATTR_LABEL)
|
||||
.filter((e) => !exactHits.includes(e.entity));
|
||||
|
||||
const sortedOptions = matchSorter(validOptions, inputValue, {
|
||||
keys: ["value.c"],
|
||||
});
|
||||
|
||||
options.push(
|
||||
...sortedOptions.map((e) => {
|
||||
return {
|
||||
value: {
|
||||
t: "Address",
|
||||
c: e.entity,
|
||||
},
|
||||
} as SelectorOption;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
options = options;
|
||||
break;
|
||||
if (types.includes("Number")) {
|
||||
const number = parseFloat(query);
|
||||
if (!Number.isNaN(number)) {
|
||||
options.push({
|
||||
t: "Number",
|
||||
c: number,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (types.includes("String") && query.length) {
|
||||
options.push({
|
||||
t: "String",
|
||||
c: query,
|
||||
});
|
||||
}
|
||||
|
||||
if (types.includes("Address")) {
|
||||
if (doSearch) {
|
||||
searchResult = await baseSearchOnce(query);
|
||||
}
|
||||
|
||||
let exactHits = Object.entries(addressToLabels)
|
||||
.filter(([_, labels]) =>
|
||||
labels.map((l) => l.toLowerCase()).includes(query.toLowerCase()),
|
||||
)
|
||||
.map(([addr, _]) => addr);
|
||||
|
||||
if (exactHits.length) {
|
||||
exactHits.forEach((addr) =>
|
||||
options.push({
|
||||
t: "Address",
|
||||
c: addr,
|
||||
labels: addressToLabels[addr],
|
||||
}),
|
||||
);
|
||||
} else if (query.length && types.includes("NewAddress")) {
|
||||
options.push({
|
||||
t: "NewAddress",
|
||||
c: query,
|
||||
});
|
||||
}
|
||||
|
||||
const validOptions = searchResult.entries
|
||||
.filter((e) => e.attribute === ATTR_LABEL)
|
||||
.filter((e) => !exactHits.includes(e.entity));
|
||||
|
||||
const sortedOptions = matchSorter(validOptions, inputValue, {
|
||||
keys: ["value.c"],
|
||||
});
|
||||
|
||||
options.push(
|
||||
...sortedOptions.map((e) => {
|
||||
return {
|
||||
t: "Address",
|
||||
c: e.entity,
|
||||
labels: [e.value.c],
|
||||
} as SelectorOption;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
options = options;
|
||||
}, 200);
|
||||
|
||||
$: dbg("Options: %O", options);
|
||||
|
@ -194,38 +264,62 @@
|
|||
}
|
||||
|
||||
async function set(option: SelectorOption) {
|
||||
switch (type) {
|
||||
case "attribute":
|
||||
if (option.labelToCreate) {
|
||||
// First, "create" attribute, getting its address.
|
||||
const [_, address] = await api.putEntry({
|
||||
entity: { t: "Attribute", c: option.attribute.name },
|
||||
});
|
||||
// Second, label it.
|
||||
await api.putEntityAttribute(address, ATTR_LABEL, {
|
||||
t: "String",
|
||||
c: option.labelToCreate,
|
||||
});
|
||||
}
|
||||
attribute = option.attribute.name;
|
||||
inputValue = option.attribute.name;
|
||||
dbg("Setting option %O", option);
|
||||
|
||||
switch (option.t) {
|
||||
case "Address":
|
||||
inputValue = option.c;
|
||||
current = option;
|
||||
break;
|
||||
case "value":
|
||||
if (!option.labelToCreate) {
|
||||
value = option.value;
|
||||
inputValue = String(option.value.c);
|
||||
} else {
|
||||
const addr = await createLabelled(option.labelToCreate);
|
||||
value = {
|
||||
case "NewAddress":
|
||||
{
|
||||
const addr = await createLabelled(option.c);
|
||||
inputValue = addr;
|
||||
current = {
|
||||
t: "Address",
|
||||
c: addr,
|
||||
labels: [option.c],
|
||||
};
|
||||
}
|
||||
|
||||
break;
|
||||
case "Attribute":
|
||||
inputValue = option.name;
|
||||
current = option;
|
||||
break;
|
||||
case "NewAttribute":
|
||||
inputValue = option.name;
|
||||
|
||||
{
|
||||
const address = await api.componentsToAddress({
|
||||
t: "Attribute",
|
||||
c: option.name,
|
||||
});
|
||||
await api.putEntityAttribute(address, ATTR_LABEL, {
|
||||
t: "String",
|
||||
c: option.label,
|
||||
});
|
||||
|
||||
current = {
|
||||
t: "Attribute",
|
||||
name: option.name,
|
||||
labels: [option.label],
|
||||
};
|
||||
inputValue = addr;
|
||||
}
|
||||
break;
|
||||
case "String":
|
||||
inputValue = option.c;
|
||||
current = option;
|
||||
break;
|
||||
case "Number":
|
||||
inputValue = String(option.c);
|
||||
current = option;
|
||||
break;
|
||||
}
|
||||
dbg("Setting value to %O", value);
|
||||
dispatch("input", value);
|
||||
|
||||
dbg("Result set value: %O", current);
|
||||
dispatch("input", current);
|
||||
|
||||
options = [];
|
||||
optionFocusIndex = -1;
|
||||
hover = false;
|
||||
|
@ -278,7 +372,7 @@
|
|||
|
||||
let input: Input;
|
||||
export function focus() {
|
||||
dbg("Focusing input");
|
||||
// dbg("Focusing input");
|
||||
input.focus();
|
||||
}
|
||||
|
||||
|
@ -292,10 +386,10 @@
|
|||
</script>
|
||||
|
||||
<div class="selector">
|
||||
{#if value?.t == "Address" && inputValue}
|
||||
{#if current?.t === "Address" && inputValue.length > 0}
|
||||
<div class="input">
|
||||
<div class="label">
|
||||
<UpObject link address={String(value.c)} />
|
||||
<UpObject link address={String(current.c)} />
|
||||
</div>
|
||||
<IconButton name="x" on:click={() => (inputValue = "")} />
|
||||
</div>
|
||||
|
@ -303,12 +397,13 @@
|
|||
<Input
|
||||
bind:this={input}
|
||||
bind:value={inputValue}
|
||||
on:input={onInput}
|
||||
on:focusChange={(ev) => (inputFocused = ev.detail)}
|
||||
on:keydown={handleArrowKeys}
|
||||
{disabled}
|
||||
{placeholder}
|
||||
/>
|
||||
>
|
||||
<slot name="prefix" slot="prefix" />
|
||||
</Input>
|
||||
{/if}
|
||||
<ul
|
||||
class="options"
|
||||
|
@ -333,36 +428,35 @@
|
|||
}
|
||||
}}
|
||||
>
|
||||
{#if option.attribute}
|
||||
{#if option.labelToCreate}
|
||||
<div class="content">{option.labelToCreate}</div>
|
||||
<div class="type">Create attribute ({option.attribute.name})</div>
|
||||
{:else if option.attribute.labels.length}
|
||||
{#if option.t === "Address"}
|
||||
{@const address = option.c}
|
||||
<UpObject
|
||||
{address}
|
||||
labels={option.labels}
|
||||
on:resolved={(ev) => onAddressResolved(address, ev)}
|
||||
/>
|
||||
{:else if option.t === "NewAddress"}
|
||||
<div class="content">{option.c}</div>
|
||||
<div class="type">{$i18n.t("Create object")}</div>
|
||||
{:else if option.t === "Attribute"}
|
||||
{#if option.labels.length}
|
||||
<div class="content">
|
||||
{#each option.attribute.labels as label}
|
||||
{#each option.labels as label}
|
||||
<div class="label">{label}</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="type">{option.attribute.name}</div>
|
||||
<div class="type">{option.name}</div>
|
||||
{:else}
|
||||
<div class="content">
|
||||
{option.attribute.name}
|
||||
{option.name}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if option.value}
|
||||
{#if option.value.t == "Address"}
|
||||
<UpObject
|
||||
address={String(option.value.c)}
|
||||
on:resolved={(ev) =>
|
||||
onAddressResolved(String(option.value.c), ev)}
|
||||
/>
|
||||
{:else}
|
||||
<div class="type">{option.value.t}</div>
|
||||
<div class="content">{option.value.c}</div>
|
||||
{/if}
|
||||
{:else if option.labelToCreate}
|
||||
<div class="type">{$i18n.t("Add object")}</div>
|
||||
<div class="content">{option.labelToCreate}</div>
|
||||
{:else if option.t === "NewAttribute"}
|
||||
<div class="content">{option.label}</div>
|
||||
<div class="type">{$i18n.t("Create attribute")} ({option.name})</div>
|
||||
{:else}
|
||||
<div class="type">{option.t}</div>
|
||||
<div class="content">{option.c}</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { readable, type Readable } from "svelte/store";
|
||||
import type { UpListing } from "@upnd/upend";
|
||||
import type { Address, IValue } from "@upnd/upend/types";
|
||||
import type { Address } from "@upnd/upend/types";
|
||||
import { query } from "../../lib/entity";
|
||||
import UpObject from "../display/UpObject.svelte";
|
||||
import UpObjectCard from "../display/UpObjectCard.svelte";
|
||||
import { ATTR_LABEL } from "@upnd/upend/constants";
|
||||
import { i18n } from "../../i18n";
|
||||
import IconButton from "../utils/IconButton.svelte";
|
||||
import Selector from "../utils/Selector.svelte";
|
||||
import Selector, { type SelectorValue } from "../utils/Selector.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { WidgetChange } from "src/types/base";
|
||||
import debug from "debug";
|
||||
|
@ -131,7 +131,7 @@
|
|||
|
||||
$: if (adding && addSelector) addSelector.focus();
|
||||
|
||||
function addEntity(ev: CustomEvent<IValue>) {
|
||||
function addEntity(ev: CustomEvent<SelectorValue>) {
|
||||
dbg("Adding entity", ev.detail);
|
||||
const addAddress = ev.detail?.t == "Address" ? ev.detail.c : undefined;
|
||||
if (!addAddress) return;
|
||||
|
@ -224,8 +224,7 @@
|
|||
<Selector
|
||||
bind:this={addSelector}
|
||||
placeholder={$i18n.t("Search database or paste an URL")}
|
||||
type="value"
|
||||
valueTypes={["Address"]}
|
||||
types={["Address", "NewAddress"]}
|
||||
on:input={addEntity}
|
||||
on:focus={(ev) => {
|
||||
if (!ev.detail) {
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
import type { WidgetChange, AttributeUpdate } from "../../types/base";
|
||||
import type { UpEntry, UpListing } from "@upnd/upend";
|
||||
import IconButton from "../utils/IconButton.svelte";
|
||||
import Selector from "../utils/Selector.svelte";
|
||||
import type { IValue } from "@upnd/upend/types";
|
||||
import Selector, {
|
||||
selectorValueAsValue,
|
||||
type SelectorValue,
|
||||
} from "../utils/Selector.svelte";
|
||||
import Editable from "../utils/Editable.svelte";
|
||||
import { query } from "../../lib/entity";
|
||||
import { type Readable, readable } from "svelte/store";
|
||||
|
@ -28,7 +30,6 @@
|
|||
|
||||
export let entries: UpEntry[];
|
||||
export let attributes: string[] | undefined = undefined;
|
||||
export let attributeOptions: string[] | undefined = undefined;
|
||||
|
||||
// Display
|
||||
$: displayColumns = (columns || "entity, attribute, value")
|
||||
|
@ -62,16 +63,16 @@
|
|||
let addFocus = false;
|
||||
let newAttrSelector: Selector;
|
||||
let newEntryAttribute = "";
|
||||
let newEntryValue: IValue | undefined;
|
||||
let newEntryValue: SelectorValue | undefined;
|
||||
|
||||
$: if (adding && newAttrSelector) newAttrSelector.focus();
|
||||
$: if (!addFocus && !addHover) adding = false;
|
||||
|
||||
async function addEntry(attribute: string, value: IValue) {
|
||||
async function addEntry(attribute: string, value: SelectorValue) {
|
||||
dispatch("change", {
|
||||
type: "create",
|
||||
attribute,
|
||||
value,
|
||||
value: await selectorValueAsValue(value),
|
||||
} as WidgetChange);
|
||||
newEntryAttribute = "";
|
||||
newEntryValue = undefined;
|
||||
|
@ -84,13 +85,13 @@
|
|||
async function updateEntry(
|
||||
address: string,
|
||||
attribute: string,
|
||||
value: IValue,
|
||||
value: SelectorValue,
|
||||
) {
|
||||
dispatch("change", {
|
||||
type: "update",
|
||||
address,
|
||||
attribute,
|
||||
value,
|
||||
value: await selectorValueAsValue(value),
|
||||
} as AttributeUpdate);
|
||||
}
|
||||
|
||||
|
@ -386,9 +387,8 @@
|
|||
{#if column == ATTR_COL}
|
||||
<div class="cell mark-attribute">
|
||||
<Selector
|
||||
type="attribute"
|
||||
bind:attribute={newEntryAttribute}
|
||||
{attributeOptions}
|
||||
types={["Attribute", "NewAttribute"]}
|
||||
on:input={(ev) => (newEntryAttribute = ev.detail.name)}
|
||||
on:focus={(ev) => (addFocus = ev.detail)}
|
||||
bind:this={newAttrSelector}
|
||||
/>
|
||||
|
@ -396,8 +396,7 @@
|
|||
{:else if column === VALUE_COL}
|
||||
<div class="cell mark-value">
|
||||
<Selector
|
||||
type="value"
|
||||
bind:value={newEntryValue}
|
||||
on:input={(ev) => (newEntryValue = ev.detail)}
|
||||
on:focus={(ev) => (addFocus = ev.detail)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -24,33 +24,49 @@ type Story = StoryObj<Selector>;
|
|||
|
||||
export const Attribute: Story = {
|
||||
args: {
|
||||
type: "attribute",
|
||||
types: ["Attribute", "NewAttribute"],
|
||||
},
|
||||
};
|
||||
|
||||
export const AllValues: Story = {
|
||||
args: {
|
||||
type: "value",
|
||||
types: [
|
||||
"Attribute",
|
||||
"NewAttribute",
|
||||
"Address",
|
||||
"NewAddress",
|
||||
"String",
|
||||
"Number",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Entities: Story = {
|
||||
args: {
|
||||
type: "value",
|
||||
valueTypes: ["Address"],
|
||||
types: ["Address", "NewAddress"],
|
||||
},
|
||||
};
|
||||
|
||||
export const ExistingEntities: Story = {
|
||||
args: {
|
||||
types: ["Address"],
|
||||
},
|
||||
};
|
||||
|
||||
export const Strings: Story = {
|
||||
args: {
|
||||
type: "value",
|
||||
valueTypes: ["String"],
|
||||
types: ["String"],
|
||||
},
|
||||
};
|
||||
|
||||
export const Numbers: Story = {
|
||||
args: {
|
||||
type: "value",
|
||||
valueTypes: ["Number"],
|
||||
types: ["Number"],
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleValues: Story = {
|
||||
args: {
|
||||
types: ["String", "Number"],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import CombineColumn from "../components/CombineColumn.svelte";
|
||||
import GroupColumn from "../components/GroupColumn.svelte";
|
||||
import SurfaceColumn from "../components/SurfaceColumn.svelte";
|
||||
import type { SelectorValue } from "src/components/utils/Selector.svelte";
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
|
||||
|
@ -16,7 +17,10 @@
|
|||
let identities: string[] = [];
|
||||
$: addresses = $params.addresses.split(",");
|
||||
|
||||
function add(address: string) {
|
||||
function add(value: SelectorValue) {
|
||||
if (value.t !== "Address") return;
|
||||
const address = value.c;
|
||||
|
||||
let _addresses = addresses.concat();
|
||||
_addresses.push(address);
|
||||
navigate(`/browse/${_addresses.join(",")}`);
|
||||
|
@ -148,7 +152,7 @@
|
|||
{#key addresses}
|
||||
<div class="column" data-index="add">
|
||||
<BrowseAdd
|
||||
on:input={(ev) => add(ev.detail.c)}
|
||||
on:input={(ev) => add(ev.detail)}
|
||||
on:editable={() => scrollToVisible("add")}
|
||||
/>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue