feat(webui): required & optional attributes
ci/woodpecker/push/woodpecker Pipeline failed Details

TODO: honor distinction in EntryLists as well
fix/notes-editor
Tomáš Mládek 2024-01-28 19:27:01 +01:00
parent 75faa28ff3
commit 0811d9ccd8
2 changed files with 126 additions and 42 deletions

View File

@ -182,19 +182,38 @@
}
async function fetchCorrectlyTagged() {
const attributes = (
if (!$entity?.attr[`~${ATTR_OF}`]?.length) {
return;
}
const allAttributes = (
await Promise.all(
($entity?.attr[`~${ATTR_OF}`] ?? []).map((e) => api.addressToComponents(e.entity))
($entity?.attr[`~${ATTR_OF}`] ?? []).map(async (e) => {
return { address: e.entity, components: await api.addressToComponents(e.entity) };
})
)
)
.filter((ac) => ac.t == 'Attribute')
.map((ac) => ac.c)
.filter((ac) => ac.components.t == 'Attribute')
.filter(isDefined);
const attributeRequiredQuery = await api.query(
Query.matches(
allAttributes.map((ac) => `@${ac.address}`),
'TYPE_REQUIRED',
Any
)
);
const requiredAttributes = allAttributes
.filter((ac) => {
return attributeRequiredQuery.getObject(ac.address).attr['TYPE_REQUIRED']?.length;
})
.map((ac) => ac.components.c as string);
const attributeQuery = await api.query(
Query.matches(
tagged.map((t) => `@${t.entity}`),
attributes,
requiredAttributes,
Any
)
);
@ -204,7 +223,7 @@
for (const element of tagged) {
const entity = attributeQuery.getObject(element.entity);
if (attributes.every((attr) => entity.attr[attr])) {
if (requiredAttributes.every((attr) => entity.attr[attr])) {
correctlyTagged = [...correctlyTagged, element.entity];
} else {
incorrectlyTagged = [...incorrectlyTagged, element.entity];

View File

@ -4,7 +4,8 @@
import IconButton from './utils/IconButton.svelte';
import api from '$lib/api';
import { i18n } from '../i18n';
import type { UpObject, UpEntry } from '@upnd/upend';
import { type UpObject, type UpEntry, Query } from '@upnd/upend';
import { Any } from '@upnd/upend/query';
import type { Readable } from 'svelte/store';
import { ATTR_OF } from '@upnd/upend/constants';
import { createEventDispatcher } from 'svelte';
@ -18,7 +19,33 @@
$: if (adding && typeSelector) typeSelector.focus();
$: typeEntries = $entity?.attr[`~${ATTR_OF}`] || [];
let types: Array<{ address: string; entry: UpEntry; required: UpEntry | undefined }> = [];
$: updateTypes($entity?.attr[`~${ATTR_OF}`] || []);
async function updateTypes(entries: UpEntry[]) {
types = [];
const query = await api.query(
Query.matches(
entries.flatMap((e) => [`@${e.address}`, `@${e.entity}`]),
Any,
Any
)
);
types = entries
.map((entry) => ({
address: entry.entity,
entry,
required: query.getObject(entry.address)?.attr['TYPE_REQUIRED']?.[0]
}))
.sort((a, b) => {
const aLabel = query.getObject(a.address)?.identify().join('/');
const bLabel = query.getObject(b.address)?.identify().join('/');
if (aLabel && bLabel) {
return aLabel.localeCompare(bLabel);
}
return a.address.localeCompare(b.address);
});
}
async function add(ev: CustomEvent<SelectorValue>) {
if (!$entity || ev.detail.t !== 'Attribute') {
@ -51,10 +78,26 @@
dispatch('change');
}
}
async function setRequired(entry: UpEntry, required: boolean) {
if (required) {
await api.putEntry({
entity: entry.address,
attribute: 'TYPE_REQUIRED',
value: { t: 'Null', c: null }
});
} else {
const requiredAddress = types.find((t) => t.entry === entry)?.required?.address;
if (requiredAddress) {
await api.deleteEntry(requiredAddress);
}
}
dispatch('change');
}
</script>
{#if typeEntries.length || $entity?.attr['~IN']?.length}
<LabelBorder hide={typeEntries.length === 0}>
{#if types.length || $entity?.attr['~IN']?.length}
<LabelBorder hide={types.length === 0}>
<span slot="header">{$i18n.t('Type Attributes')}</span>
{#if adding}
<div class="selector">
@ -70,41 +113,34 @@
</div>
{/if}
<div class="body">
<ul class="attributes">
{#each typeEntries as typeEntry}
<li class="attribute">
<div class="label">
<UpObjectDisplay address={typeEntry.entity} link />
</div>
<div class="controls">
<IconButton name="x-circle" on:click={() => remove(typeEntry)} />
</div>
</li>
<div class="attributes">
{#each types as type}
<div class="label">
<UpObjectDisplay address={type.address} link />
</div>
<button
class:required={type.required}
on:click={() => setRequired(type.entry, !type.required)}
>
{type.required ? $i18n.t('Required') : $i18n.t('Optional')}
</button>
<div class="controls">
<IconButton name="x-circle" on:click={() => remove(type.entry)} />
</div>
{:else}
<li class="no-attributes">
<div class="no-attributes">
{$i18n.t('No attributes assigned to this type.')}
</li>
</div>
{/each}
</ul>
<div class="add-button">
<IconButton outline small name="plus-circle" on:click={() => (adding = true)} />
<div class="add-button">
<IconButton outline small name="plus-circle" on:click={() => (adding = true)} />
</div>
</div>
</div>
</LabelBorder>
{/if}
<style lang="scss">
.attributes {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 0.25em;
}
.attribute {
display: flex;
}
.body {
display: flex;
align-items: start;
@ -114,6 +150,41 @@
}
}
.attributes {
display: grid;
grid-template-columns: repeat(3, minmax(0, auto));
gap: 0.25em 1em;
}
.controls {
display: flex;
align-items: center;
justify-content: flex-end;
}
button {
border: none;
background: none;
cursor: pointer;
padding: 0;
margin: 0;
color: var(--color-text);
opacity: 0.8;
transition: opacity 0.2s ease-in-out;
&.required {
opacity: 1;
}
&:hover {
opacity: 1;
}
}
.add-button {
grid-column: 1 / span 3;
display: flex;
flex-direction: column;
}
.selector {
width: 100%;
margin-bottom: 0.5rem;
@ -122,10 +193,4 @@
.no-attributes {
opacity: 0.66;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
</style>