feat/orm-diffs
Laurin Weger 3 days ago
parent cce7dbfbe9
commit 2bdaa84438
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 10
      engine/verifier/src/orm/handle_frontend_update.rs
  2. 1
      engine/verifier/src/orm/initialize.rs
  3. 20
      sdk/js/alien-deepsignals/test-map-type.js
  4. 28
      sdk/js/alien-deepsignals/test-set-map.js
  5. 25
      sdk/js/alien-deepsignals/test-set-paths.js
  6. 11
      sdk/js/examples/multi-framework-signals/src/app/pages/index.astro
  7. 440
      sdk/js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx
  8. 175
      sdk/js/examples/multi-framework-signals/src/frontends/svelte/HelloWorld.svelte
  9. 389
      sdk/js/examples/multi-framework-signals/src/frontends/vue/HelloWorld.vue

@ -302,10 +302,7 @@ fn create_sparql_update_query_for_diff(
remove_statement, remove_statement,
wheres.join(" .\n ") wheres.join(" .\n ")
)); ));
log_info!( log_info!("[create_sparql_update_query_for_diff] Added delete query.");
"[create_sparql_update_query_for_diff] Added delete query #{}",
sparql_sub_queries.len()
);
// var_counter += 1; // Not necessary because not used afterwards. // var_counter += 1; // Not necessary because not used afterwards.
} }
// The actual INSERT. // The actual INSERT.
@ -315,10 +312,7 @@ fn create_sparql_update_query_for_diff(
add_statement, add_statement,
where_statements.join(". \n ") where_statements.join(". \n ")
)); ));
log_info!( log_info!("[create_sparql_update_query_for_diff] Added insert query.");
"[create_sparql_update_query_for_diff] Added insert query #{}",
sparql_sub_queries.len()
);
} }
} }

@ -62,7 +62,6 @@ impl Verifier {
.push(orm_subscription); .push(orm_subscription);
let orm_objects = self.create_orm_object_for_shape(nuri, session_id, &shape_type)?; let orm_objects = self.create_orm_object_for_shape(nuri, session_id, &shape_type)?;
// log_debug!("create_orm_object_for_shape return {:?}", orm_objects);
let _ = tx let _ = tx
.send(AppResponse::V0(AppResponseV0::OrmInitial(orm_objects))) .send(AppResponse::V0(AppResponseV0::OrmInitial(orm_objects)))

@ -0,0 +1,20 @@
import { deepSignal } from './dist/index.js';
const root = deepSignal({
mySet: new Set([
{ "@id": "obj1", value: 10 },
{ "@id": "obj2", value: 20 }
])
});
const result = root.mySet.values().map(entry => entry);
console.log('Type:', typeof result);
console.log('Constructor:', result.constructor.name);
console.log('Result:', result);
console.log('Has next?:', typeof result.next);
console.log('Is iterable?:', Symbol.iterator in result);
// Convert to array
const arr = Array.from(result);
console.log('Array:', arr);
console.log('First entry:', arr[0]);

@ -0,0 +1,28 @@
import { deepSignal, subscribeDeepMutations } from './dist/index.js';
const root = deepSignal({
mySet: new Set([
{ "@id": "obj1", value: 10 },
{ "@id": "obj2", value: 20 }
])
});
subscribeDeepMutations(root, (patches) => {
console.log('Patches:', JSON.stringify(patches, null, 2));
});
// Use .map() to get entries
const entries = root.mySet.values().map(entry => {
console.log('Entry:', entry);
return entry;
});
console.log('Got entries:', entries.length);
console.log('Modifying first entry...');
// Modify the first one
entries[0].value = 100;
setTimeout(() => {
console.log('Done');
}, 100);

@ -0,0 +1,25 @@
import { deepSignal, subscribeDeepMutations } from './dist/index.js';
const root = deepSignal({
mySet: new Set([
{ "@id": "obj1", value: 10 }
])
});
subscribeDeepMutations(root, (patches) => {
console.log('Patches:', JSON.stringify(patches, null, 2));
});
// Get the first entry from the set
const entries = root.mySet.values();
const firstEntry = entries.next().value;
console.log('First entry:', firstEntry);
console.log('Modifying value...');
// Modify it
firstEntry.value = 20;
setTimeout(() => {
console.log('Done');
}, 100);

@ -29,7 +29,8 @@ const title = "Multi-framework app";
console.log(info.V0.details); console.log(info.V0.details);
initNg(ng, event.session); initNg(ng, event.session);
window.ng = ng; window.ng = ng;
window.session = event.session;
}, },
true, true,
[] []
@ -37,15 +38,15 @@ const title = "Multi-framework app";
</script> </script>
<Layout title={title}> <Layout title={title}>
<!-- <Highlight vue> <Highlight vue>
<VueRoot client:only /> <VueRoot client:only />
</Highlight> --> </Highlight>
<Highlight react> <Highlight react>
<ReactRoot client:only="react" /> <ReactRoot client:only="react" />
</Highlight> </Highlight>
<!--
<Highlight svelte> <Highlight svelte>
<SvelteRoot client:only /> <SvelteRoot client:only />
</Highlight> --> </Highlight>
</Layout> </Layout>

@ -3,195 +3,289 @@ import { useShape } from "@ng-org/signals/react";
import flattenObject from "../utils/flattenObject"; import flattenObject from "../utils/flattenObject";
import { TestObjectShapeType } from "../../shapes/orm/testShape.shapeTypes"; import { TestObjectShapeType } from "../../shapes/orm/testShape.shapeTypes";
import { BasicShapeType } from "../../shapes/orm/basic.shapeTypes"; import { BasicShapeType } from "../../shapes/orm/basic.shapeTypes";
import type { ShapeType } from "@ng-org/shex-orm";
import type { Basic } from "../../shapes/orm/basic.typings";
const sparqlExampleData = `
PREFIX ex: <http://example.org/>
INSERT DATA {
<urn:test:obj1> a ex:TestObject ;
ex:stringValue "hello world" ;
ex:numValue 42 ;
ex:boolValue true ;
ex:arrayValue 1,2,3 ;
ex:objectValue <urn:test:id3> ;
ex:anotherObject <urn:test:id1>, <urn:test:id2> ;
ex:numOrStr "either" ;
ex:lit1Or2 "lit1" ;
ex:unrelated "some value" ;
ex:anotherUnrelated 4242 .
<urn:test:id3>
ex:nestedString "nested" ;
ex:nestedNum 7 ;
ex:nestedArray 5,6 .
<urn:test:id1>
ex:prop1 "one" ;
ex:prop2 1 .
<urn:test:id2>
ex:prop1 "two" ;
ex:prop2 2 .
<urn:test:obj2> a ex:TestObject ;
ex:stringValue "hello world #2" ;
ex:numValue 422 ;
ex:boolValue false ;
ex:arrayValue 4,5,6 ;
ex:objectValue <urn:test:id6> ;
ex:anotherObject <urn:test:id4>, <urn:test:id5> ;
ex:numOrStr 4 ;
ex:lit1Or2 "lit2" ;
ex:unrelated "some value2" ;
ex:anotherUnrelated 42422 .
<urn:test:id6>
ex:nestedString "nested2" ;
ex:nestedNum 72 ;
ex:nestedArray 7,8,9 .
<urn:test:id4>
ex:prop1 "one2" ;
ex:prop2 12 .
<urn:test:id5>
ex:prop1 "two2" ;
ex:prop2 22 .
<urn:basicObject4>
a <http://example.org/Basic> ;
ex:basicString "string of object 1" .
<urn:basicObject5>
a <http://example.org/Basic> ;
ex:basicString "string of object 2" .
}
`;
export function HelloWorldReact() { export function HelloWorldReact() {
const state = [...(useShape(BasicShapeType)?.entries() || [])][0]; const state = useShape(BasicShapeType);
// @ts-expect-error // @ts-expect-error
window.reactState = state; window.reactState = state;
console.log("react state", state);
if (!state) return <>Loading state</>; if (!state) return <div>Loading...</div>;
// Create a table from the state object: One column for keys, one for values, one with an input to change the value. // Create a table from the state object: One column for keys, one for values, one with an input to change the value.
return ( return (
<div> <div>
<p>Rendered in React</p> <p>Rendered in React</p>
{/* <button <button
onClick={() => { onClick={() => {
state.boolValue = !state.boolValue; window.ng.sparql_update(
state.numValue += 2; window.session.session_id,
sparqlExampleData,
"did:ng:" + window.session.private_store_id
);
}} }}
> >
click me to change multiple props Add example data
</button> */} </button>
<table border={1} cellPadding={5}> <div>
<thead> {state.values()?.map((ormObj) => (
<tr> <table border={1} cellPadding={5} key={ormObj["@id"]}>
<th>Key</th> <thead>
<th>Value</th> <tr>
<th>Edit</th> <th>Key</th>
</tr> <th>Value</th>
</thead> <th>Edit</th>
<tbody>
{(() => {
const setNestedValue = (
obj: any,
path: string,
value: any
) => {
const keys = path.split(".");
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
};
const getNestedValue = (obj: any, path: string) => {
return path
.split(".")
.reduce((current, key) => current[key], obj);
};
return flattenObject(state).map(([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>
{value instanceof Set
? Array.from(value).join(", ")
: Array.isArray(value)
? `[${value.join(", ")}]`
: JSON.stringify(value)}
</td>
<td>
{typeof value === "string" ? (
<input
type="text"
value={value}
onChange={(e) => {
setNestedValue(
state,
key,
e.target.value
);
}}
/>
) : typeof value === "number" ? (
<input
type="number"
value={value}
onChange={(e) => {
setNestedValue(
state,
key,
Number(e.target.value)
);
}}
/>
) : typeof value === "boolean" ? (
<input
type="checkbox"
checked={value}
onChange={(e) => {
setNestedValue(
state,
key,
e.target.checked
);
}}
/>
) : Array.isArray(value) ? (
<div>
<button
onClick={() => {
const currentArray =
getNestedValue(
state,
key
);
setNestedValue(state, key, [
...currentArray,
currentArray.length + 1,
]);
}}
>
Add
</button>
<button
onClick={() => {
const currentArray =
getNestedValue(
state,
key
);
if (
currentArray.length > 0
) {
setNestedValue(
state,
key,
currentArray.slice(
0,
-1
)
);
}
}}
>
Remove
</button>
</div>
) : value instanceof Set ? (
<div>
<button
onClick={() => {
const currentSet =
getNestedValue(
state,
key
);
currentSet.add(
`item${currentSet.size + 1}`
);
}}
>
Add
</button>
<button
onClick={() => {
const currentSet =
getNestedValue(
state,
key
);
const lastItem =
Array.from(
currentSet
).pop();
if (lastItem) {
currentSet.delete(
lastItem
);
}
}}
>
Remove
</button>
</div>
) : (
"N/A"
)}
</td>
</tr> </tr>
)); </thead>
})()} <tbody>
</tbody> {(() => {
</table> const setNestedValue = (
obj: any,
path: string,
value: any
) => {
const keys = path.split(".");
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
};
const getNestedValue = (
obj: any,
path: string
) => {
return path
.split(".")
.reduce(
(current, key) => current[key],
obj
);
};
return flattenObject(ormObj).map(
([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>
{value instanceof Set
? Array.from(value).join(
", "
)
: Array.isArray(value)
? `[${value.join(", ")}]`
: JSON.stringify(value)}
</td>
<td>
{typeof value === "string" ? (
<input
type="text"
value={value}
onChange={(e) => {
setNestedValue(
state,
key,
e.target.value
);
}}
/>
) : typeof value ===
"number" ? (
<input
type="number"
value={value}
onChange={(e) => {
setNestedValue(
state,
key,
Number(
e.target
.value
)
);
}}
/>
) : typeof value ===
"boolean" ? (
<input
type="checkbox"
checked={value}
onChange={(e) => {
setNestedValue(
state,
key,
e.target.checked
);
}}
/>
) : Array.isArray(value) ? (
<div>
<button
onClick={() => {
const currentArray =
getNestedValue(
state,
key
);
setNestedValue(
state,
key,
[
...currentArray,
currentArray.length +
1,
]
);
}}
>
Add
</button>
<button
onClick={() => {
const currentArray =
getNestedValue(
state,
key
);
if (
currentArray.length >
0
) {
setNestedValue(
state,
key,
currentArray.slice(
0,
-1
)
);
}
}}
>
Remove
</button>
</div>
) : value instanceof Set ? (
<div>
<button
onClick={() => {
const currentSet =
getNestedValue(
state,
key
);
currentSet.add(
`item${currentSet.size + 1}`
);
}}
>
Add
</button>
<button
onClick={() => {
const currentSet =
getNestedValue(
state,
key
);
const lastItem =
Array.from(
currentSet
).pop();
if (lastItem) {
currentSet.delete(
lastItem
);
}
}}
>
Remove
</button>
</div>
) : (
"N/A"
)}
</td>
</tr>
)
);
})()}
</tbody>
</table>
))}
</div>
</div> </div>
); );
} }

@ -19,9 +19,9 @@
} }
cur[keys[keys.length - 1]] = value; cur[keys[keys.length - 1]] = value;
} }
const flatEntries = $derived( const flattenedObjects = $derived(
$shapeObject $shapeObject
? $shapeObject.entries().map((o) => flattenObject(o)[0] || ({} as any)) ? $shapeObject.values().map((o) => flattenObject(o)[0] || ({} as any))
: [] : []
); );
$effect(() => { $effect(() => {
@ -32,92 +32,95 @@
{#if $shapeObject} {#if $shapeObject}
<div> <div>
<p>Rendered in Svelte</p> <p>Rendered in Svelte</p>
<table border="1" cellpadding="5">
<thead> {#each flattenedObjects as flatEntries}
<tr> <table border="1" cellpadding="5">
<th>Key</th> <thead>
<th>Value</th>
<th>Edit</th>
</tr>
</thead>
<tbody>
{#each flatEntries as [key, value] (key)}
<tr> <tr>
<td style="white-space:nowrap;">{key}</td> <th>Key</th>
<td> <th>Value</th>
{#if value instanceof Set} <th>Edit</th>
{Array.from(value).join(", ")}
{:else if Array.isArray(value)}
[{value.join(", ")}]
{:else}
{JSON.stringify(value)}
{/if}
</td>
<td>
{#if typeof value === "string"}
<input
type="text"
{value}
oninput={(e: any) =>
setNestedValue($shapeObject, key, e.target.value)}
/>
{:else if typeof value === "number"}
<input
type="number"
{value}
oninput={(e: any) =>
setNestedValue($shapeObject, key, Number(e.target.value))}
/>
{:else if typeof value === "boolean"}
<input
type="checkbox"
checked={value}
onchange={(e: any) =>
setNestedValue($shapeObject, key, e.target.checked)}
/>
{:else if Array.isArray(value)}
<div style="display:flex; gap:.5rem;">
<button
onclick={() => {
const cur = getNestedValue($shapeObject, key) || [];
setNestedValue($shapeObject, key, [
...cur,
cur.length + 1,
]);
}}>Add</button
>
<button
onclick={() => {
const cur = getNestedValue($shapeObject, key) || [];
if (cur.length)
setNestedValue($shapeObject, key, cur.slice(0, -1));
}}>Remove</button
>
</div>
{:else if value instanceof Set}
<div style="display:flex; gap:.5rem;">
<button
onclick={() => {
const cur: Set<any> = getNestedValue($shapeObject, key);
cur.add(`item${cur.size + 1}`);
}}>Add</button
>
<button
onclick={() => {
const cur: Set<any> = getNestedValue($shapeObject, key);
const last = Array.from(cur).pop();
if (last !== undefined) cur.delete(last);
}}>Remove</button
>
</div>
{:else}
N/A
{/if}
</td>
</tr> </tr>
{/each} </thead>
</tbody> <tbody>
</table> {#each flatEntries as [key, value] (key)}
<tr>
<td style="white-space:nowrap;">{key}</td>
<td>
{#if value instanceof Set}
{Array.from(value).join(", ")}
{:else if Array.isArray(value)}
[{value.join(", ")}]
{:else}
{JSON.stringify(value)}
{/if}
</td>
<td>
{#if typeof value === "string"}
<input
type="text"
{value}
oninput={(e: any) =>
setNestedValue($shapeObject, key, e.target.value)}
/>
{:else if typeof value === "number"}
<input
type="number"
{value}
oninput={(e: any) =>
setNestedValue($shapeObject, key, Number(e.target.value))}
/>
{:else if typeof value === "boolean"}
<input
type="checkbox"
checked={value}
onchange={(e: any) =>
setNestedValue($shapeObject, key, e.target.checked)}
/>
{:else if Array.isArray(value)}
<div style="display:flex; gap:.5rem;">
<button
onclick={() => {
const cur = getNestedValue($shapeObject, key) || [];
setNestedValue($shapeObject, key, [
...cur,
cur.length + 1,
]);
}}>Add</button
>
<button
onclick={() => {
const cur = getNestedValue($shapeObject, key) || [];
if (cur.length)
setNestedValue($shapeObject, key, cur.slice(0, -1));
}}>Remove</button
>
</div>
{:else if value instanceof Set}
<div style="display:flex; gap:.5rem;">
<button
onclick={() => {
const cur: Set<any> = getNestedValue($shapeObject, key);
cur.add(`item${cur.size + 1}`);
}}>Add</button
>
<button
onclick={() => {
const cur: Set<any> = getNestedValue($shapeObject, key);
const last = Array.from(cur).pop();
if (last !== undefined) cur.delete(last);
}}>Remove</button
>
</div>
{:else}
N/A
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
{/each}
</div> </div>
{:else} {:else}
<p>Loading state</p> <p>Loading state</p>

@ -5,215 +5,214 @@ import flattenObject from "../utils/flattenObject";
import { TestObjectShapeType } from "../../shapes/orm/testShape.shapeTypes"; import { TestObjectShapeType } from "../../shapes/orm/testShape.shapeTypes";
// Acquire deep signal object (proxy) for a shape; scope second arg left empty string for parity // Acquire deep signal object (proxy) for a shape; scope second arg left empty string for parity
const shapeObj = useShape(TestObjectShapeType); const shapeObjects = useShape(TestObjectShapeType);
// Expose for devtools exploration // Expose for devtools exploration
// @ts-ignore // @ts-ignore
window.vueState = shapeObj; window.vueState = shapeObjects;
const flatEntries = computed(() => flattenObject(shapeObj));
</script> </script>
<template> <template>
<div class="vue"> <div class="vue">
<p>Rendered in Vue</p> <p>Rendered in Vue</p>
<template v-if="shapeObj && 'type' in shapeObj"> <template v-if="shapeObjects">
<!-- Direct property access --> <template v-for="obj in shapeObjects" :key="obj">
<input type="text" v-model="shapeObj.type" /> <template v-for="flatEntries in [flattenObject(obj)]">
<input type="text" v-model="shapeObj.objectValue.nestedString" /> <!-- Property access through object recursion -->
<table
<!-- Property access through object recursion --> border="1"
<table cellpadding="5"
border="1" style="margin-top: 1rem; max-width: 100%; font-size: 0.9rem"
cellpadding="5"
style="margin-top: 1rem; max-width: 100%; font-size: 0.9rem"
>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Edit</th>
</tr>
</thead>
<tbody>
<tr
v-for="[path, value, key, parent] in flatEntries"
:key="path"
> >
<!-- Key--> <thead>
<td style="white-space: nowrap">{{ path }}</td> <tr>
<th>Key</th>
<th>Value</th>
<th>Edit</th>
</tr>
</thead>
<tbody>
<tr
v-for="[path, value, key, parent] in flatEntries"
:key="path"
>
<!-- Key-->
<td style="white-space: nowrap">{{ path }}</td>
<!-- Value --> <!-- Value -->
<td> <td>
<template v-if="value instanceof Set"> <template v-if="value instanceof Set">
{{ Array.from(value).join(", ") }} {{ Array.from(value).join(", ") }}
</template> </template>
<template v-else-if="Array.isArray(value)"> <template v-else-if="Array.isArray(value)">
[{{ value.join(", ") }}] [{{ value.join(", ") }}]
</template> </template>
<template v-else> <template v-else>
{{ JSON.stringify(value) }} {{ JSON.stringify(value) }}
</template> </template>
</td> </td>
<!-- Edit --> <!-- Edit -->
<td> <td>
<!-- String editing --> <!-- String editing -->
<template v-if="typeof value === 'string'"> <template v-if="typeof value === 'string'">
<template v-if="path.indexOf('.') === -1"> <template v-if="path.indexOf('.') === -1">
<input <input
type="text" type="text"
v-model="shapeObj[key]" v-model="obj[key]"
/> />
</template> </template>
<template v-else> <template v-else>
<input <input
type="text" type="text"
v-bind:value="parent[key]" v-bind:value="parent[key]"
v-on:input=" v-on:input="
(e) => { (e) => {
parent[key] = ( parent[key] = (
e.target as any e.target as any
).value; ).value;
} }
" "
/> />
</template> </template>
</template> </template>
<!-- Number editing --> <!-- Number editing -->
<template v-else-if="typeof value === 'number'"> <template v-else-if="typeof value === 'number'">
<template v-if="path.indexOf('.') === -1"> <template v-if="path.indexOf('.') === -1">
<input <input
type="number" type="number"
v-model="shapeObj[key]" v-model="obj[key]"
/> />
</template> </template>
<template v-else> <template v-else>
<input <input
type="number" type="number"
v-bind:value="parent[key]" v-bind:value="parent[key]"
v-on:input=" v-on:input="
(e) => { (e) => {
parent[key] = +(e.target as any) parent[key] = +(e.target as any)
.value; .value;
} }
" "
/> />
</template> </template>
</template> </template>
<!-- Boolean editing --> <!-- Boolean editing -->
<template v-else-if="typeof value === 'boolean'"> <template v-else-if="typeof value === 'boolean'">
<template v-if="path.indexOf('.') === -1"> <template v-if="path.indexOf('.') === -1">
<input <input
type="checkbox" type="checkbox"
v-model="shapeObj[key]" v-model="obj[key]"
/> />
</template> </template>
<template v-else> <template v-else>
<input <input
type="checkbox" type="checkbox"
v-bind:value="value" v-bind:value="value"
v-on:input=" v-on:input="
(e) => { (e) => {
parent[key] = ( parent[key] = (
e.target as any e.target as any
).value; ).value;
} }
" "
/> />
</template> </template>
</template> </template>
<!-- Array editing --> <!-- Array editing -->
<template v-else-if="Array.isArray(value)"> <template v-else-if="Array.isArray(value)">
<template v-if="path.indexOf('.') === -1"> <template v-if="path.indexOf('.') === -1">
<div style="display: flex; gap: 0.5rem"> <div style="display: flex; gap: 0.5rem">
<button <button
@click=" @click="
() => { () => {
parent[key] = [ parent[key] = [
...value, ...value,
value.length + 1, value.length + 1,
]; ];
} }
" "
> >
Add Add
</button> </button>
<button <button
@click=" @click="
() => { () => {
parent[key] = parent[key] =
value.slice(1); value.slice(1);
} }
" "
> >
Remove Remove
</button> </button>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div style="display: flex; gap: 0.5rem"> <div style="display: flex; gap: 0.5rem">
<button <button
@click=" @click="
() => { () => {
parent[key] = [ parent[key] = [
...value, ...value,
value.length + 1, value.length + 1,
]; ];
} }
" "
> >
Add Add
</button> </button>
<button <button
@click=" @click="
() => { () => {
parent[key] = parent[key] =
value.slice(1); value.slice(1);
} }
" "
> >
Remove Remove
</button> </button>
</div> </div>
</template> </template>
</template> </template>
<!-- Set editing --> <!-- Set editing -->
<template v-else-if="value instanceof Set"> <template v-else-if="value instanceof Set">
<div style="display: flex; gap: 0.5rem"> <div style="display: flex; gap: 0.5rem">
<button <button
@click=" @click="
() => { () => {
value.add( value.add(
`item${value.size + 1}` `item${value.size + 1}`
); );
} }
" "
> >
Add Add
</button> </button>
<button <button
@click=" @click="
() => { () => {
const last = const last =
Array.from(value).pop(); Array.from(value).pop();
if (last !== undefined) if (last !== undefined)
value.delete(last); value.delete(last);
} }
" "
> >
Remove Remove
</button> </button>
</div> </div>
</template> </template>
<template v-else> N/A </template> <template v-else> N/A </template>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</template>
</template>
</template> </template>
<template v-else> <template v-else>
<p>Loading state</p> <p>Loading state</p>

Loading…
Cancel
Save