repair datum editor

This commit is contained in:
Brent Schroeter 2025-10-16 06:40:11 +00:00
parent 395a547b94
commit 5a24454787
9 changed files with 417 additions and 354 deletions

View file

@ -72,7 +72,7 @@ pub(super) async fn get(
}; };
let mut sql_raw = format!( let mut sql_raw = format!(
"select {0} from {1}.{2}", "select {0} from {1}.{2} order by _id",
pkey_attrs pkey_attrs
.iter() .iter()
.chain(attrs.iter()) .chain(attrs.iter())

View file

@ -21,7 +21,7 @@ mod update_form_transitions_handler;
mod update_portal_name_handler; mod update_portal_name_handler;
mod update_prompts_handler; mod update_prompts_handler;
mod update_rel_name_handler; mod update_rel_name_handler;
mod update_value_handler; mod update_values_handler;
pub(super) fn new_router() -> Router<App> { pub(super) fn new_router() -> Router<App> {
Router::<App>::new() Router::<App>::new()
@ -46,8 +46,8 @@ pub(super) fn new_router() -> Router<App> {
) )
.route("/p/{portal_id}/insert", post(insert_handler::post)) .route("/p/{portal_id}/insert", post(insert_handler::post))
.route( .route(
"/p/{portal_id}/update-value", "/p/{portal_id}/update-values",
post(update_value_handler::post), post(update_values_handler::post),
) )
.route("/p/{portal_id}/set-filter", post(set_filter_handler::post)) .route("/p/{portal_id}/set-filter", post(set_filter_handler::post))
.route_with_tsr("/p/{portal_id}/form/", get(form_handler::get)) .route_with_tsr("/p/{portal_id}/form/", get(form_handler::get))

View file

@ -5,9 +5,6 @@ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{IntoResponse as _, Response}, response::{IntoResponse as _, Response},
}; };
// [`axum_extra`]'s form extractor is preferred:
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
use axum_extra::extract::Form;
use interim_models::{ use interim_models::{
datum::Datum, datum::Datum,
portal::Portal, portal::Portal,
@ -17,7 +14,7 @@ use interim_models::{
use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass}; use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass};
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
use sqlx::{postgres::types::Oid, query}; use sqlx::{Acquire as _, postgres::types::Oid, query};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
@ -36,12 +33,17 @@ pub(super) struct PathParams {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub(super) struct FormBody { pub(super) struct FormBody {
cells: Vec<CellInfo>,
}
#[derive(Debug, Deserialize)]
pub(super) struct CellInfo {
column: String, column: String,
pkeys: HashMap<String, Datum>, pkey: HashMap<String, Datum>,
value: Datum, value: Datum,
} }
/// HTTP POST handler for updating a single value in a backing Postgres table. /// HTTP POST handler for updating cell values in a backing Postgres table.
/// ///
/// This handler expects 3 path parameters with the structure described by /// This handler expects 3 path parameters with the structure described by
/// [`PathParams`]. /// [`PathParams`].
@ -55,7 +57,7 @@ pub(super) async fn post(
rel_oid, rel_oid,
workspace_id, workspace_id,
}): Path<PathParams>, }): Path<PathParams>,
Form(form): Form<FormBody>, Json(form): Json<FormBody>,
) -> Result<Response, AppError> { ) -> Result<Response, AppError> {
// Check workspace authorization. // Check workspace authorization.
let workspace_perms = WorkspaceUserPerm::belonging_to_user(user.id) let workspace_perms = WorkspaceUserPerm::belonging_to_user(user.id)
@ -70,7 +72,7 @@ pub(super) async fn post(
// permission to access/alter both as needed. // permission to access/alter both as needed.
// Prevent users from modifying Phonograph metadata columns. // Prevent users from modifying Phonograph metadata columns.
if form.column.starts_with('_') { if form.cells.iter().any(|cell| cell.column.starts_with('_')) {
return Err(forbidden!("access denied to update system metadata column")); return Err(forbidden!("access denied to update system metadata column"));
} }
@ -91,19 +93,24 @@ pub(super) async fn post(
.fetch_all(&mut workspace_client) .fetch_all(&mut workspace_client)
.await?; .await?;
let conn = workspace_client.get_conn();
let mut txn = conn.begin().await?;
for cell in form.cells {
// TODO: simplify pkey management // TODO: simplify pkey management
form.pkeys cell.pkey
.get(&pkey_attrs.first().unwrap().attname) .get(&pkey_attrs.first().unwrap().attname)
.unwrap() .unwrap()
.clone() .clone()
.bind_onto(form.value.bind_onto(query(&format!( .bind_onto(cell.value.bind_onto(query(&format!(
"update {ident} set {value_col} = $1 where {pkey_col} = $2", "update {ident} set {value_col} = $1 where {pkey_col} = $2",
ident = rel.get_identifier(), ident = rel.get_identifier(),
value_col = escape_identifier(&form.column), value_col = escape_identifier(&cell.column),
pkey_col = escape_identifier(&pkey_attrs.first().unwrap().attname), pkey_col = escape_identifier(&pkey_attrs.first().unwrap().attname),
)))) ))))
.execute(workspace_client.get_conn()) .execute(&mut *txn)
.await?; .await?;
}
txn.commit().await?;
Ok(Json(json!({ "ok": true })).into_response()) Ok(Json(json!({ "ok": true })).into_response())
} }

View file

@ -26,6 +26,5 @@
</main> </main>
</div> </div>
<script type="module" src="{{ settings.root_path }}/js_dist/table-viewer.webc.mjs"></script> <script type="module" src="{{ settings.root_path }}/js_dist/table-viewer.webc.mjs"></script>
<script type="module" src="{{ settings.root_path }}/js_dist/field-adder.webc.mjs"></script>
<script type="module" src="{{ settings.root_path }}/js_dist/filter-menu.webc.mjs"></script> <script type="module" src="{{ settings.root_path }}/js_dist/filter-menu.webc.mjs"></script>
{% endblock %} {% endblock %}

View file

@ -71,6 +71,7 @@ $table-border-color: #ccc;
align-items: center; align-items: center;
display: flex; display: flex;
flex: none; flex: none;
user-select: none;
width: 100%; width: 100%;
&--selected { &--selected {
@ -203,18 +204,51 @@ $table-border-color: #ccc;
} }
} }
.datum-editor { .table-viewer__datum-editor {
align-items: stretch;
border-top: globals.$default-border; border-top: globals.$default-border;
display: flex; display: flex;
grid-area: editor; grid-area: editor;
height: 12rem; height: 6rem;
}
&__input { .datum-editor {
@include globals.reset_input; &__container {
padding: 0.75rem 0.5rem; border-left: solid 4px transparent;
font-family: globals.$font-family-data; display: grid;
flex: 1; flex: 1;
grid-template: 'type-selector type-selector' max-content
'null-control value-control' max-content
'helpers helpers' auto / max-content auto;
&:has(:focus) {
border-left-color: #07f;
}
&--incomplete {
border-left-color: #f33;
}
}
&__type-selector {
grid-area: type-selector;
}
&__null-control {
@include globals.reset_button;
align-self: start;
grid-area: null-control;
padding: 0.75rem;
&--disabled {
opacity: 0.75;
}
}
&__text-input {
@include globals.reset_input;
grid-area: value-control;
font-family: globals.$font-family-data;
padding: 0.75rem 0.5rem;
} }
} }

View file

@ -1,23 +1,64 @@
<script lang="ts"> <script lang="ts">
import { type EditorState } from "./editor-state.svelte"; import icon_cube_transparent from "../assets/heroicons/20/solid/cube-transparent.svg?raw";
import icon_cube from "../assets/heroicons/20/solid/cube.svg?raw";
import type { Datum } from "./datum.svelte";
import {
datum_from_editor_state,
editor_state_from_datum,
type EditorState,
} from "./editor-state.svelte";
import { type FieldInfo } from "./field.svelte"; import { type FieldInfo } from "./field.svelte";
type Props = { type Props = {
/**
* For use cases in which the user may select between multiple datum types,
* such as when embedded in an expression editor, this array represents the
* permissible set of field parameters.
*/
assignable_fields?: ReadonlyArray<FieldInfo>; assignable_fields?: ReadonlyArray<FieldInfo>;
editor_state: EditorState;
field_info: FieldInfo; field_info: FieldInfo;
on_change?(value?: Datum): void;
value?: Datum;
}; };
let { let {
assignable_fields = [], assignable_fields = [],
editor_state = $bindable(),
field_info = $bindable(), field_info = $bindable(),
on_change,
value = $bindable(),
}: Props = $props(); }: Props = $props();
let editor_state = $state<EditorState | undefined>();
let type_selector_menu_button_element = $state< let type_selector_menu_button_element = $state<
HTMLButtonElement | undefined HTMLButtonElement | undefined
>(); >();
let type_selector_popover_element = $state<HTMLDivElement | undefined>(); let type_selector_popover_element = $state<HTMLDivElement | undefined>();
let text_input_element = $state<HTMLInputElement | undefined>();
$effect(() => {
if (value) {
editor_state = editor_state_from_datum(value);
}
});
export function focus() {
text_input_element?.focus();
}
function handle_input() {
if (!editor_state) {
console.warn("preconditions for handle_input() not met");
return;
}
value = datum_from_editor_state(
editor_state,
field_info.field.presentation,
);
on_change?.(value);
}
function handle_type_selector_menu_button_click() { function handle_type_selector_menu_button_click() {
type_selector_popover_element?.togglePopover(); type_selector_popover_element?.togglePopover();
@ -30,8 +71,12 @@
} }
</script> </script>
<div class="datum-editor__container"> <div
{#if assignable_fields.length > 0} class="datum-editor__container"
class:datum-editor__container--incomplete={!value}
>
{#if editor_state}
{#if assignable_fields?.length > 0}
<div class="datum-editor__type-selector"> <div class="datum-editor__type-selector">
<button <button
bind:this={type_selector_menu_button_element} bind:this={type_selector_menu_button_element}
@ -58,12 +103,44 @@
</div> </div>
</div> </div>
{/if} {/if}
<div class="datum-editor__content"> <button
type="button"
class="datum-editor__null-control"
class:datum-editor__null-control--disabled={editor_state.text_value !==
""}
disabled={editor_state.text_value !== ""}
onclick={() => {
if (!editor_state) {
console.warn("null control onclick() preconditions not met");
return;
}
editor_state.is_null = !editor_state.is_null;
handle_input();
}}
>
{#if editor_state.is_null}
{@html icon_cube_transparent}
{:else}
{@html icon_cube}
{/if}
</button>
{#if field_info.field.presentation.t === "Text" || field_info.field.presentation.t === "Uuid"} {#if field_info.field.presentation.t === "Text" || field_info.field.presentation.t === "Uuid"}
<input bind:value={editor_state.text_value} type="text" /> <input
bind:this={text_input_element}
value={editor_state.text_value}
oninput={({ currentTarget: { value } }) => {
if (editor_state) {
editor_state.text_value = value;
handle_input();
}
}}
class="datum-editor__text-input"
type="text"
/>
{:else if field_info.field.presentation.t === "Timestamp"} {:else if field_info.field.presentation.t === "Timestamp"}
<input bind:value={editor_state.date_value} type="date" /> <input value={editor_state.date_value} type="date" />
<input bind:value={editor_state.time_value} type="time" /> <input value={editor_state.time_value} type="time" />
{/if}
<div class="datum-editor__helpers"></div>
{/if} {/if}
</div> </div>
</div>

View file

@ -57,8 +57,11 @@ export function datum_from_editor_state(
value: EditorState, value: EditorState,
presentation: Presentation, presentation: Presentation,
): Datum | undefined { ): Datum | undefined {
if (presentation.t === "Dropdown") {
return { t: "Text", c: value.is_null ? undefined : value.text_value };
}
if (presentation.t === "Text") { if (presentation.t === "Text") {
return { t: "Text", c: value.text_value }; return { t: "Text", c: value.is_null ? undefined : value.text_value };
} }
if (presentation.t === "Timestamp") { if (presentation.t === "Timestamp") {
// FIXME // FIXME
@ -66,7 +69,12 @@ export function datum_from_editor_state(
} }
if (presentation.t === "Uuid") { if (presentation.t === "Uuid") {
try { try {
return { t: "Uuid", c: uuid.stringify(uuid.parse(value.text_value)) }; return {
t: "Uuid",
c: value.is_null
? undefined
: uuid.stringify(uuid.parse(value.text_value)),
};
} catch { } catch {
// uuid.parse() throws a TypeError if unsuccessful. // uuid.parse() throws a TypeError if unsuccessful.
return undefined; return undefined;

View file

@ -14,14 +14,9 @@
import ExpressionSelector from "./expression-selector.svelte"; import ExpressionSelector from "./expression-selector.svelte";
import { type PgExpressionAny } from "./expression.svelte"; import { type PgExpressionAny } from "./expression.svelte";
import ExpressionEditor from "./expression-editor.webc.svelte"; import ExpressionEditor from "./expression-editor.webc.svelte";
import {
DEFAULT_EDITOR_STATE,
editor_state_from_datum,
type EditorState,
datum_from_editor_state,
} from "./editor-state.svelte";
import { type FieldInfo } from "./field.svelte"; import { type FieldInfo } from "./field.svelte";
import { type Presentation } from "./presentation.svelte"; import { type Presentation } from "./presentation.svelte";
import type { Datum } from "./datum.svelte";
const ASSIGNABLE_PRESENTATIONS: Presentation[] = [ const ASSIGNABLE_PRESENTATIONS: Presentation[] = [
{ t: "Text", c: { input_mode: { t: "MultiLine", c: {} } } }, { t: "Text", c: { input_mode: { t: "MultiLine", c: {} } } },
@ -49,23 +44,12 @@
let { identifier_hints = [], value = $bindable() }: Props = $props(); let { identifier_hints = [], value = $bindable() }: Props = $props();
let editor_state = $state<EditorState>( // Dynamic state to bind to datum editor.
value?.t === "Literal" let editor_value = $state<Datum | undefined>();
? editor_state_from_datum(value.c)
: DEFAULT_EDITOR_STATE,
);
let editor_field_info = $state<FieldInfo>(ASSIGNABLE_FIELDS[0]); let editor_field_info = $state<FieldInfo>(ASSIGNABLE_FIELDS[0]);
$effect(() => { $effect(() => {
if (value?.t === "Literal" && editor_field_info) { editor_value = value?.t === "Literal" ? value.c : undefined;
const datum_value = datum_from_editor_state(
editor_state,
editor_field_info.field.presentation,
);
if (datum_value) {
value.c = datum_value;
}
}
}); });
function handle_identifier_selector_change( function handle_identifier_selector_change(
@ -75,6 +59,12 @@
value.c.parts_raw = [ev.currentTarget.value]; value.c.parts_raw = [ev.currentTarget.value];
} }
} }
function handle_editor_change(datum_value: Datum) {
if (value?.t === "Literal") {
value.c = datum_value;
}
}
</script> </script>
<div class="expression-editor__container"> <div class="expression-editor__container">
@ -102,9 +92,10 @@
</select> </select>
{:else if value.t === "Literal"} {:else if value.t === "Literal"}
<DatumEditor <DatumEditor
bind:editor_state
bind:field_info={editor_field_info} bind:field_info={editor_field_info}
bind:value={editor_value}
assignable_fields={ASSIGNABLE_FIELDS} assignable_fields={ASSIGNABLE_FIELDS}
on_change={handle_editor_change}
/> />
{/if} {/if}
</div> </div>

View file

@ -17,11 +17,6 @@
import icon_sparkles from "../assets/heroicons/20/solid/sparkles.svg?raw"; import icon_sparkles from "../assets/heroicons/20/solid/sparkles.svg?raw";
import { type Datum, datum_schema } from "./datum.svelte"; import { type Datum, datum_schema } from "./datum.svelte";
import DatumEditor from "./datum-editor.svelte"; import DatumEditor from "./datum-editor.svelte";
import {
DEFAULT_EDITOR_STATE,
datum_from_editor_state,
type EditorState,
} from "./editor-state.svelte";
import { import {
type Coords, type Coords,
type Row, type Row,
@ -42,15 +37,18 @@
let { columns = [] }: Props = $props(); let { columns = [] }: Props = $props();
type CommittedChange = { type CellDelta = {
coords_initial: Coords; // Assumes that primary keys are immutable and that rows are only added or
// This will be identical to coords_initial, unless the change altered a // removed upon a refresh.
// primary key. coords: Coords;
coords_updated: Coords;
value_initial: Datum; value_initial: Datum;
value_updated: Datum; value_updated: Datum;
}; };
type Delta = {
cells: CellDelta[];
};
type LazyData = { type LazyData = {
rows: Row[]; rows: Row[];
fields: FieldInfo[]; fields: FieldInfo[];
@ -62,14 +60,27 @@
original_value: Datum; original_value: Datum;
}; };
type ParsedPkey = Record<string, Datum>;
let selections = $state<Selection[]>([]); let selections = $state<Selection[]>([]);
let editing = $state(false); // While the datum editor is focused and while updated values are being pushed
let editor_state = $state<EditorState>(DEFAULT_EDITOR_STATE); // to the server, other actions such as changing the set of selected cells are
let committed_changes = $state<CommittedChange[][]>([]); // restricted.
let reverted_changes = $state<CommittedChange[][]>([]); let editor_value = $state<Datum | undefined>(undefined);
let editor_input_element = $state<HTMLInputElement | undefined>(); let deltas = $state<{
commit_queued: Delta[];
commit_pending: Delta[];
committed: Delta[];
revert_queued: Delta[];
revert_pending: Delta[];
reverted: Delta[];
}>({
commit_queued: [],
commit_pending: [],
committed: [],
revert_queued: [],
revert_pending: [],
reverted: [],
});
let datum_editor = $state<DatumEditor | undefined>();
let table_element = $state<HTMLDivElement | undefined>(); let table_element = $state<HTMLDivElement | undefined>();
let inserter_rows = $state<Row[]>([]); let inserter_rows = $state<Row[]>([]);
let lazy_data = $state<LazyData | undefined>(); let lazy_data = $state<LazyData | undefined>();
@ -117,18 +128,18 @@
} else if (sel.region === "inserter") { } else if (sel.region === "inserter") {
cell_data = inserter_rows[sel.coords[0]].data[sel.coords[1]]; cell_data = inserter_rows[sel.coords[0]].data[sel.coords[1]];
} }
if (cell_data?.t === "Text" || cell_data?.t === "Uuid") { editor_value = cell_data;
editor_state.text_value = cell_data.c ?? "";
} else { } else {
editor_state.text_value = ""; editor_value = undefined;
}
} else {
editor_state.text_value = "";
} }
} }
function try_move_selection(direction: "Down" | "Left" | "Right" | "Up") { function move_selection(direction: "Down" | "Left" | "Right" | "Up") {
if (lazy_data && !editing && selections.length > 0) { if (!lazy_data || selections.length === 0) {
console.warn("move_selection() preconditions not met");
return;
}
const last_selection = selections[selections.length - 1]; const last_selection = selections[selections.length - 1];
if ( if (
direction === "Right" && direction === "Right" &&
@ -153,10 +164,7 @@
set_selections([ set_selections([
{ {
region: "main", region: "main",
coords: [ coords: [last_selection.coords[0] + 1, last_selection.coords[1]],
last_selection.coords[0] + 1,
last_selection.coords[1],
],
}, },
]); ]);
} else { } else {
@ -173,10 +181,7 @@
set_selections([ set_selections([
{ {
region: "inserter", region: "inserter",
coords: [ coords: [last_selection.coords[0] + 1, last_selection.coords[1]],
last_selection.coords[0] + 1,
last_selection.coords[1],
],
}, },
]); ]);
} }
@ -187,10 +192,7 @@
set_selections([ set_selections([
{ {
region: "main", region: "main",
coords: [ coords: [last_selection.coords[0] - 1, last_selection.coords[1]],
last_selection.coords[0] - 1,
last_selection.coords[1],
],
}, },
]); ]);
} }
@ -199,10 +201,7 @@
set_selections([ set_selections([
{ {
region: "inserter", region: "inserter",
coords: [ coords: [last_selection.coords[0] - 1, last_selection.coords[1]],
last_selection.coords[0] - 1,
last_selection.coords[1],
],
}, },
]); ]);
} else { } else {
@ -217,20 +216,15 @@
} }
} }
} }
}
function try_sync_edit_to_cells() { function try_sync_edit_to_cells() {
if (lazy_data && editing && selections.length === 1) { if (lazy_data && selections.length === 1) {
const [sel] = selections; const [sel] = selections;
const parsed = datum_from_editor_state( if (editor_value !== undefined) {
editor_state,
lazy_data.fields[sel.coords[1]].field.presentation,
);
if (parsed !== undefined) {
if (sel.region === "main") { if (sel.region === "main") {
lazy_data.rows[sel.coords[0]].data[sel.coords[1]] = parsed; lazy_data.rows[sel.coords[0]].data[sel.coords[1]] = editor_value;
} else if (sel.region === "inserter") { } else if (sel.region === "inserter") {
inserter_rows[sel.coords[0]].data[sel.coords[1]] = parsed; inserter_rows[sel.coords[0]].data[sel.coords[1]] = editor_value;
} else { } else {
throw new Error("Unknown region"); throw new Error("Unknown region");
} }
@ -238,66 +232,71 @@
} }
} }
function try_start_edit() { function try_queue_delta() {
if (!editing) { // Copy `editor_value` so that it can be used intuitively within closures.
editing = true; const editor_value_scoped = editor_value;
editor_input_element?.focus(); if (editor_value_scoped === undefined) {
cancel_edit();
} else {
if (selections.length > 0) {
deltas.commit_queued = [
...deltas.commit_queued,
{
cells: selections
.filter(({ region }) => region === "main")
.map((sel) => ({
coords: sel.coords,
value_initial: sel.original_value,
value_updated: editor_value_scoped,
})),
},
];
selections = selections.map((sel) => ({
...sel,
original_value: editor_value_scoped,
}));
}
} }
} }
function try_commit_edit() { async function commit_delta(delta: Delta) {
(async function () { // Copy `lazy_data` so that it can be used intuitively within closures.
if (lazy_data && editing && editor_state && selections.length === 1) { const lazy_data_scoped = lazy_data;
const [sel] = selections; if (!lazy_data_scoped) {
const field = lazy_data.fields[sel.coords[1]]; console.warn("sync_delta() preconditions not met");
const parsed = datum_from_editor_state( return;
editor_state, }
field.field.presentation, deltas.commit_pending = [...deltas.commit_pending, delta];
); const resp = await fetch("update-values", {
if (parsed !== undefined) {
if (sel.region === "main") {
const pkey = JSON.parse(
lazy_data.rows[sel.coords[0]].key as string,
) as ParsedPkey;
const resp = await fetch("update-value", {
method: "post", method: "post",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
column: field.field.name, cells: delta.cells.map((cell) => ({
pkeys: pkey, pkey: JSON.parse(lazy_data_scoped.rows[cell.coords[0]].key as string),
value: parsed, column: lazy_data_scoped.fields[cell.coords[1]].field.name,
value: cell.value_updated,
})),
}), }),
}); });
if (resp.status >= 200 && resp.status < 300) { if (resp.status >= 200 && resp.status < 300) {
committed_changes.push([ deltas.commit_pending = deltas.commit_pending.filter((x) => x !== delta);
{ deltas.committed = [...deltas.committed, delta];
coords_initial: sel.coords,
coords_updated: sel.coords, // TODO: this assumes no inserted/deleted rows
value_initial: sel.original_value,
value_updated: parsed,
},
]);
editing = false;
selections = [{ ...sel, original_value: parsed }];
table_element?.focus();
} else { } else {
// TODO display feedback to user // TODO display feedback to user
console.error(resp); console.error(resp);
console.error(await resp.text()); console.error(await resp.text());
} }
} else if (sel.region === "inserter") {
table_element?.focus();
editing = false;
selections = [{ ...sel, original_value: parsed }];
} else {
throw new Error("Unknown region");
} }
} else {
// TODO function tick_delta_queue() {
const front_of_queue: Delta | undefined = deltas.commit_queued[0];
if (front_of_queue) {
deltas.commit_queued = deltas.commit_queued.filter(
(x) => x !== front_of_queue,
);
commit_delta(front_of_queue).catch(console.error);
} }
} }
})().catch(console.error);
}
function cancel_edit() { function cancel_edit() {
selections.forEach(({ coords, original_value, region }) => { selections.forEach(({ coords, original_value, region }) => {
@ -313,7 +312,6 @@
}); });
// Reset editor input value // Reset editor input value
set_selections(selections); set_selections(selections);
editing = false;
table_element?.focus(); table_element?.focus();
} }
@ -323,7 +321,7 @@
if (lazy_data) { if (lazy_data) {
const arrow_direction = arrow_key_direction(ev.key); const arrow_direction = arrow_key_direction(ev.key);
if (arrow_direction) { if (arrow_direction) {
try_move_selection(arrow_direction); move_selection(arrow_direction);
ev.preventDefault(); ev.preventDefault();
} }
if (ev.key === "Enter") { if (ev.key === "Enter") {
@ -347,26 +345,15 @@
]; ];
} }
} else { } else {
try_start_edit(); datum_editor?.focus();
} }
} }
} }
} }
function handle_table_cell_dblclick(_: Coords) {
try_start_edit();
}
function handle_table_focus() {
if (selections.length === 0 && (lazy_data?.rows[0]?.data.length ?? 0) > 0) {
set_selections([{ region: "main", coords: [0, 0] }]);
}
}
// -------- Event Handlers: Main Table -------- // // -------- Event Handlers: Main Table -------- //
function handle_main_cell_click(ev: MouseEvent, coords: Coords) { function handle_main_cell_click(ev: MouseEvent, coords: Coords) {
if (!editing) {
if (ev.metaKey || ev.ctrlKey) { if (ev.metaKey || ev.ctrlKey) {
// TODO // TODO
// selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords]; // selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords];
@ -375,12 +362,10 @@
set_selections([{ region: "main", coords }]); set_selections([{ region: "main", coords }]);
} }
} }
}
// -------- Event Handlers: Inserter Table -------- // // -------- Event Handlers: Inserter Table -------- //
function handle_inserter_cell_click(ev: MouseEvent, coords: Coords) { function handle_inserter_cell_click(ev: MouseEvent, coords: Coords) {
if (!editing) {
if (ev.metaKey || ev.ctrlKey) { if (ev.metaKey || ev.ctrlKey) {
// TODO // TODO
// selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords]; // selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords];
@ -389,29 +374,6 @@
set_selections([{ region: "inserter", coords }]); set_selections([{ region: "inserter", coords }]);
} }
} }
}
// -------- Event Handlers: Editor -------- //
function handle_editor_blur() {
try_commit_edit();
}
function handle_editor_focus() {
try_start_edit();
}
function handle_editor_input() {
try_sync_edit_to_cells();
}
function handle_editor_keydown(ev: KeyboardEvent) {
if (ev.key === "Enter") {
try_commit_edit();
} else if (ev.key === "Escape") {
cancel_edit();
}
}
// -------- Initial API Fetch -------- // // -------- Initial API Fetch -------- //
@ -440,6 +402,8 @@
}, },
]; ];
})().catch(console.error); })().catch(console.error);
setInterval(tick_delta_queue, 500);
</script> </script>
{#snippet table_region({ {#snippet table_region({
@ -474,24 +438,21 @@
aria-selected={cell_selected} aria-selected={cell_selected}
class="lens-table__cell" class="lens-table__cell"
onmousedown={(ev) => on_cell_click(ev, cell_coords)} onmousedown={(ev) => on_cell_click(ev, cell_coords)}
ondblclick={() => handle_table_cell_dblclick(cell_coords)} ondblclick={() => {
datum_editor?.focus();
}}
role="gridcell" role="gridcell"
style:width={`${field.field.table_width_px}px`} style:width={`${field.field.table_width_px}px`}
tabindex="-1" tabindex="-1"
> >
<div <div
class={[ class="lens-cell__container"
"lens-cell__container", class:lens-cell__container--selected={cell_selected}
cell_selected && "lens-cell__container--selected",
]}
> >
{#if cell_data.t === "Text"} {#if cell_data.t === "Text"}
<div <div
class={[ class="lens-cell__content lens-cell__content--text"
"lens-cell__content", class:lens-cell__content--null={cell_data.c === undefined}
"lens-cell__content--text",
cell_data.c === undefined && "lens-cell__content--null",
]}
> >
{#if cell_data.c === undefined} {#if cell_data.c === undefined}
{@html null_value_html} {@html null_value_html}
@ -501,11 +462,8 @@
</div> </div>
{:else if cell_data.t === "Uuid"} {:else if cell_data.t === "Uuid"}
<div <div
class={[ class="lens-cell__content lens-cell__content--uuid"
"lens-cell__content", class:lens-cell__content--null={cell_data.c === undefined}
"lens-cell__content--uuid",
cell_data.c === undefined && "lens-cell__content--null",
]}
> >
{#if cell_data.c === undefined} {#if cell_data.c === undefined}
{@html null_value_html} {@html null_value_html}
@ -514,9 +472,7 @@
{/if} {/if}
</div> </div>
{:else} {:else}
<div <div class="lens-cell__content lens-cell__content--unknown">
class={["lens-cell__content", "lens-cell__content--unknown"]}
>
<div>UNKNOWN</div> <div>UNKNOWN</div>
</div> </div>
{/if} {/if}
@ -538,7 +494,9 @@
<div <div
bind:this={table_element} bind:this={table_element}
class="lens-table" class="lens-table"
onfocus={handle_table_focus} onfocus={() => {
try_queue_delta();
}}
onkeydown={handle_table_keydown} onkeydown={handle_table_keydown}
role="grid" role="grid"
tabindex="0" tabindex="0"
@ -593,28 +551,17 @@
{/each} {/each}
</form> </form>
</div> </div>
<div class="datum-editor"> <div class="table-viewer__datum-editor">
{#if selections.length === 1 && editor_state} {#if selections.length === 1}
<DatumEditor <DatumEditor
bind:editor_state bind:this={datum_editor}
bind:value={editor_value}
field_info={lazy_data.fields[selections[0].coords[1]]} field_info={lazy_data.fields[selections[0].coords[1]]}
on_change={() => {
try_sync_edit_to_cells();
}}
/> />
{/if} {/if}
<!--
<input
bind:this={editor_input_element}
bind:value={editor_input_value}
class={[
"lens-editor__input",
selections.length !== 1 && "lens-editor__input--hidden",
]}
onblur={handle_editor_blur}
onfocus={handle_editor_focus}
oninput={handle_editor_input}
onkeydown={handle_editor_keydown}
tabindex="-1"
/>
-->
</div> </div>
{/if} {/if}
</div> </div>