diff --git a/interim-server/src/routes/relations_single/get_data_handler.rs b/interim-server/src/routes/relations_single/get_data_handler.rs index cf3538b..e9c4355 100644 --- a/interim-server/src/routes/relations_single/get_data_handler.rs +++ b/interim-server/src/routes/relations_single/get_data_handler.rs @@ -72,7 +72,7 @@ pub(super) async fn get( }; let mut sql_raw = format!( - "select {0} from {1}.{2}", + "select {0} from {1}.{2} order by _id", pkey_attrs .iter() .chain(attrs.iter()) diff --git a/interim-server/src/routes/relations_single/mod.rs b/interim-server/src/routes/relations_single/mod.rs index 6e93166..bce33fb 100644 --- a/interim-server/src/routes/relations_single/mod.rs +++ b/interim-server/src/routes/relations_single/mod.rs @@ -21,7 +21,7 @@ mod update_form_transitions_handler; mod update_portal_name_handler; mod update_prompts_handler; mod update_rel_name_handler; -mod update_value_handler; +mod update_values_handler; pub(super) fn new_router() -> Router { Router::::new() @@ -46,8 +46,8 @@ pub(super) fn new_router() -> Router { ) .route("/p/{portal_id}/insert", post(insert_handler::post)) .route( - "/p/{portal_id}/update-value", - post(update_value_handler::post), + "/p/{portal_id}/update-values", + post(update_values_handler::post), ) .route("/p/{portal_id}/set-filter", post(set_filter_handler::post)) .route_with_tsr("/p/{portal_id}/form/", get(form_handler::get)) diff --git a/interim-server/src/routes/relations_single/update_value_handler.rs b/interim-server/src/routes/relations_single/update_values_handler.rs similarity index 70% rename from interim-server/src/routes/relations_single/update_value_handler.rs rename to interim-server/src/routes/relations_single/update_values_handler.rs index 71471f4..7c0278f 100644 --- a/interim-server/src/routes/relations_single/update_value_handler.rs +++ b/interim-server/src/routes/relations_single/update_values_handler.rs @@ -5,9 +5,6 @@ use axum::{ extract::{Path, State}, 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::{ datum::Datum, portal::Portal, @@ -17,7 +14,7 @@ use interim_models::{ use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass}; use serde::Deserialize; use serde_json::json; -use sqlx::{postgres::types::Oid, query}; +use sqlx::{Acquire as _, postgres::types::Oid, query}; use uuid::Uuid; use crate::{ @@ -36,12 +33,17 @@ pub(super) struct PathParams { #[derive(Debug, Deserialize)] pub(super) struct FormBody { + cells: Vec, +} + +#[derive(Debug, Deserialize)] +pub(super) struct CellInfo { column: String, - pkeys: HashMap, + pkey: HashMap, 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 /// [`PathParams`]. @@ -55,7 +57,7 @@ pub(super) async fn post( rel_oid, workspace_id, }): Path, - Form(form): Form, + Json(form): Json, ) -> Result { // Check workspace authorization. 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. // 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")); } @@ -91,19 +93,24 @@ pub(super) async fn post( .fetch_all(&mut workspace_client) .await?; - // TODO: simplify pkey management - form.pkeys - .get(&pkey_attrs.first().unwrap().attname) - .unwrap() - .clone() - .bind_onto(form.value.bind_onto(query(&format!( - "update {ident} set {value_col} = $1 where {pkey_col} = $2", - ident = rel.get_identifier(), - value_col = escape_identifier(&form.column), - pkey_col = escape_identifier(&pkey_attrs.first().unwrap().attname), - )))) - .execute(workspace_client.get_conn()) - .await?; + let conn = workspace_client.get_conn(); + let mut txn = conn.begin().await?; + for cell in form.cells { + // TODO: simplify pkey management + cell.pkey + .get(&pkey_attrs.first().unwrap().attname) + .unwrap() + .clone() + .bind_onto(cell.value.bind_onto(query(&format!( + "update {ident} set {value_col} = $1 where {pkey_col} = $2", + ident = rel.get_identifier(), + value_col = escape_identifier(&cell.column), + pkey_col = escape_identifier(&pkey_attrs.first().unwrap().attname), + )))) + .execute(&mut *txn) + .await?; + } + txn.commit().await?; Ok(Json(json!({ "ok": true })).into_response()) } diff --git a/interim-server/templates/portal_table.html b/interim-server/templates/portal_table.html index 1b72765..da4efa3 100644 --- a/interim-server/templates/portal_table.html +++ b/interim-server/templates/portal_table.html @@ -26,6 +26,5 @@ - {% endblock %} diff --git a/sass/viewer.scss b/sass/viewer.scss index 63b3a58..adfe201 100644 --- a/sass/viewer.scss +++ b/sass/viewer.scss @@ -71,6 +71,7 @@ $table-border-color: #ccc; align-items: center; display: flex; flex: none; + user-select: none; width: 100%; &--selected { @@ -203,18 +204,51 @@ $table-border-color: #ccc; } } -.datum-editor { - align-items: stretch; +.table-viewer__datum-editor { border-top: globals.$default-border; display: flex; grid-area: editor; - height: 12rem; + height: 6rem; +} - &__input { - @include globals.reset_input; - padding: 0.75rem 0.5rem; - font-family: globals.$font-family-data; +.datum-editor { + &__container { + border-left: solid 4px transparent; + display: grid; 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; } } diff --git a/svelte/src/datum-editor.svelte b/svelte/src/datum-editor.svelte index ca88cfe..24ecb13 100644 --- a/svelte/src/datum-editor.svelte +++ b/svelte/src/datum-editor.svelte @@ -1,23 +1,64 @@ -
- {#if assignable_fields.length > 0} -
- -
- {#each assignable_fields as assignable_field_info} - - {/each} +
+ {#if editor_state} + {#if assignable_fields?.length > 0} +
+ +
+ {#each assignable_fields as assignable_field_info} + + {/each} +
-
- {/if} -
- {#if field_info.field.presentation.t === "Text" || field_info.field.presentation.t === "Uuid"} - - {:else if field_info.field.presentation.t === "Timestamp"} - - {/if} -
+ + {#if field_info.field.presentation.t === "Text" || field_info.field.presentation.t === "Uuid"} + { + 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"} + + + {/if} +
+ {/if}
diff --git a/svelte/src/editor-state.svelte.ts b/svelte/src/editor-state.svelte.ts index 358a617..ab15e3d 100644 --- a/svelte/src/editor-state.svelte.ts +++ b/svelte/src/editor-state.svelte.ts @@ -57,8 +57,11 @@ export function datum_from_editor_state( value: EditorState, presentation: Presentation, ): Datum | undefined { + if (presentation.t === "Dropdown") { + return { t: "Text", c: value.is_null ? undefined : value.text_value }; + } 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") { // FIXME @@ -66,7 +69,12 @@ export function datum_from_editor_state( } if (presentation.t === "Uuid") { 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 { // uuid.parse() throws a TypeError if unsuccessful. return undefined; diff --git a/svelte/src/expression-editor.webc.svelte b/svelte/src/expression-editor.webc.svelte index 16083d8..612c571 100644 --- a/svelte/src/expression-editor.webc.svelte +++ b/svelte/src/expression-editor.webc.svelte @@ -14,14 +14,9 @@ import ExpressionSelector from "./expression-selector.svelte"; import { type PgExpressionAny } from "./expression.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 Presentation } from "./presentation.svelte"; + import type { Datum } from "./datum.svelte"; const ASSIGNABLE_PRESENTATIONS: Presentation[] = [ { t: "Text", c: { input_mode: { t: "MultiLine", c: {} } } }, @@ -49,23 +44,12 @@ let { identifier_hints = [], value = $bindable() }: Props = $props(); - let editor_state = $state( - value?.t === "Literal" - ? editor_state_from_datum(value.c) - : DEFAULT_EDITOR_STATE, - ); + // Dynamic state to bind to datum editor. + let editor_value = $state(); let editor_field_info = $state(ASSIGNABLE_FIELDS[0]); $effect(() => { - if (value?.t === "Literal" && editor_field_info) { - const datum_value = datum_from_editor_state( - editor_state, - editor_field_info.field.presentation, - ); - if (datum_value) { - value.c = datum_value; - } - } + editor_value = value?.t === "Literal" ? value.c : undefined; }); function handle_identifier_selector_change( @@ -75,6 +59,12 @@ value.c.parts_raw = [ev.currentTarget.value]; } } + + function handle_editor_change(datum_value: Datum) { + if (value?.t === "Literal") { + value.c = datum_value; + } + }
@@ -102,9 +92,10 @@ {:else if value.t === "Literal"} {/if}
diff --git a/svelte/src/table-viewer.webc.svelte b/svelte/src/table-viewer.webc.svelte index c00071b..93437f2 100644 --- a/svelte/src/table-viewer.webc.svelte +++ b/svelte/src/table-viewer.webc.svelte @@ -17,11 +17,6 @@ import icon_sparkles from "../assets/heroicons/20/solid/sparkles.svg?raw"; import { type Datum, datum_schema } from "./datum.svelte"; import DatumEditor from "./datum-editor.svelte"; - import { - DEFAULT_EDITOR_STATE, - datum_from_editor_state, - type EditorState, - } from "./editor-state.svelte"; import { type Coords, type Row, @@ -42,15 +37,18 @@ let { columns = [] }: Props = $props(); - type CommittedChange = { - coords_initial: Coords; - // This will be identical to coords_initial, unless the change altered a - // primary key. - coords_updated: Coords; + type CellDelta = { + // Assumes that primary keys are immutable and that rows are only added or + // removed upon a refresh. + coords: Coords; value_initial: Datum; value_updated: Datum; }; + type Delta = { + cells: CellDelta[]; + }; + type LazyData = { rows: Row[]; fields: FieldInfo[]; @@ -62,14 +60,27 @@ original_value: Datum; }; - type ParsedPkey = Record; - let selections = $state([]); - let editing = $state(false); - let editor_state = $state(DEFAULT_EDITOR_STATE); - let committed_changes = $state([]); - let reverted_changes = $state([]); - let editor_input_element = $state(); + // While the datum editor is focused and while updated values are being pushed + // to the server, other actions such as changing the set of selected cells are + // restricted. + let editor_value = $state(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(); let table_element = $state(); let inserter_rows = $state([]); let lazy_data = $state(); @@ -117,120 +128,103 @@ } else if (sel.region === "inserter") { cell_data = inserter_rows[sel.coords[0]].data[sel.coords[1]]; } - if (cell_data?.t === "Text" || cell_data?.t === "Uuid") { - editor_state.text_value = cell_data.c ?? ""; - } else { - editor_state.text_value = ""; - } + editor_value = cell_data; } else { - editor_state.text_value = ""; + editor_value = undefined; } } - function try_move_selection(direction: "Down" | "Left" | "Right" | "Up") { - if (lazy_data && !editing && selections.length > 0) { - const last_selection = selections[selections.length - 1]; - if ( - direction === "Right" && - last_selection.coords[1] < lazy_data.fields.length - 1 - ) { - set_selections([ - { - region: last_selection.region, - coords: [last_selection.coords[0], last_selection.coords[1] + 1], - }, - ]); - } else if (direction === "Left" && last_selection.coords[1] > 0) { - set_selections([ - { - region: last_selection.region, - coords: [last_selection.coords[0], last_selection.coords[1] - 1], - }, - ]); - } else if (direction === "Down") { - if (last_selection.region === "main") { - if (last_selection.coords[0] < lazy_data.rows.length - 1) { - set_selections([ - { - region: "main", - coords: [ - last_selection.coords[0] + 1, - last_selection.coords[1], - ], - }, - ]); - } else { - // At bottom of main table. - set_selections([ - { - region: "inserter", - coords: [0, last_selection.coords[1]], - }, - ]); - } - } else if (last_selection.region === "inserter") { - if (last_selection.coords[0] < inserter_rows.length - 1) { - set_selections([ - { - region: "inserter", - coords: [ - last_selection.coords[0] + 1, - last_selection.coords[1], - ], - }, - ]); - } + function move_selection(direction: "Down" | "Left" | "Right" | "Up") { + if (!lazy_data || selections.length === 0) { + console.warn("move_selection() preconditions not met"); + return; + } + + const last_selection = selections[selections.length - 1]; + if ( + direction === "Right" && + last_selection.coords[1] < lazy_data.fields.length - 1 + ) { + set_selections([ + { + region: last_selection.region, + coords: [last_selection.coords[0], last_selection.coords[1] + 1], + }, + ]); + } else if (direction === "Left" && last_selection.coords[1] > 0) { + set_selections([ + { + region: last_selection.region, + coords: [last_selection.coords[0], last_selection.coords[1] - 1], + }, + ]); + } else if (direction === "Down") { + if (last_selection.region === "main") { + if (last_selection.coords[0] < lazy_data.rows.length - 1) { + set_selections([ + { + region: "main", + coords: [last_selection.coords[0] + 1, last_selection.coords[1]], + }, + ]); + } else { + // At bottom of main table. + set_selections([ + { + region: "inserter", + coords: [0, last_selection.coords[1]], + }, + ]); } - } else if (direction === "Up") { - if (last_selection.region === "main") { - if (last_selection.coords[0] > 0) { - set_selections([ - { - region: "main", - coords: [ - last_selection.coords[0] - 1, - last_selection.coords[1], - ], - }, - ]); - } - } else if (last_selection.region === "inserter") { - if (last_selection.coords[0] > 0) { - set_selections([ - { - region: "inserter", - coords: [ - last_selection.coords[0] - 1, - last_selection.coords[1], - ], - }, - ]); - } else { - // At top of inserter table. - set_selections([ - { - region: "main", - coords: [lazy_data.rows.length - 1, last_selection.coords[1]], - }, - ]); - } + } else if (last_selection.region === "inserter") { + if (last_selection.coords[0] < inserter_rows.length - 1) { + set_selections([ + { + region: "inserter", + coords: [last_selection.coords[0] + 1, last_selection.coords[1]], + }, + ]); + } + } + } else if (direction === "Up") { + if (last_selection.region === "main") { + if (last_selection.coords[0] > 0) { + set_selections([ + { + region: "main", + coords: [last_selection.coords[0] - 1, last_selection.coords[1]], + }, + ]); + } + } else if (last_selection.region === "inserter") { + if (last_selection.coords[0] > 0) { + set_selections([ + { + region: "inserter", + coords: [last_selection.coords[0] - 1, last_selection.coords[1]], + }, + ]); + } else { + // At top of inserter table. + set_selections([ + { + region: "main", + coords: [lazy_data.rows.length - 1, last_selection.coords[1]], + }, + ]); } } } } function try_sync_edit_to_cells() { - if (lazy_data && editing && selections.length === 1) { + if (lazy_data && selections.length === 1) { const [sel] = selections; - const parsed = datum_from_editor_state( - editor_state, - lazy_data.fields[sel.coords[1]].field.presentation, - ); - if (parsed !== undefined) { + if (editor_value !== undefined) { 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") { - inserter_rows[sel.coords[0]].data[sel.coords[1]] = parsed; + inserter_rows[sel.coords[0]].data[sel.coords[1]] = editor_value; } else { throw new Error("Unknown region"); } @@ -238,65 +232,70 @@ } } - function try_start_edit() { - if (!editing) { - editing = true; - editor_input_element?.focus(); + function try_queue_delta() { + // Copy `editor_value` so that it can be used intuitively within closures. + const editor_value_scoped = editor_value; + 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 () { - if (lazy_data && editing && editor_state && selections.length === 1) { - const [sel] = selections; - const field = lazy_data.fields[sel.coords[1]]; - const parsed = datum_from_editor_state( - editor_state, - field.field.presentation, - ); - 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", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - column: field.field.name, - pkeys: pkey, - value: parsed, - }), - }); - if (resp.status >= 200 && resp.status < 300) { - committed_changes.push([ - { - 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 { - // TODO display feedback to user - console.error(resp); - 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 - } - } - })().catch(console.error); + async function commit_delta(delta: Delta) { + // Copy `lazy_data` so that it can be used intuitively within closures. + const lazy_data_scoped = lazy_data; + if (!lazy_data_scoped) { + console.warn("sync_delta() preconditions not met"); + return; + } + deltas.commit_pending = [...deltas.commit_pending, delta]; + const resp = await fetch("update-values", { + method: "post", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + cells: delta.cells.map((cell) => ({ + pkey: JSON.parse(lazy_data_scoped.rows[cell.coords[0]].key as string), + column: lazy_data_scoped.fields[cell.coords[1]].field.name, + value: cell.value_updated, + })), + }), + }); + if (resp.status >= 200 && resp.status < 300) { + deltas.commit_pending = deltas.commit_pending.filter((x) => x !== delta); + deltas.committed = [...deltas.committed, delta]; + } else { + // TODO display feedback to user + console.error(resp); + console.error(await resp.text()); + } + } + + 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); + } } function cancel_edit() { @@ -313,7 +312,6 @@ }); // Reset editor input value set_selections(selections); - editing = false; table_element?.focus(); } @@ -323,7 +321,7 @@ if (lazy_data) { const arrow_direction = arrow_key_direction(ev.key); if (arrow_direction) { - try_move_selection(arrow_direction); + move_selection(arrow_direction); ev.preventDefault(); } if (ev.key === "Enter") { @@ -347,69 +345,33 @@ ]; } } 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 -------- // function handle_main_cell_click(ev: MouseEvent, coords: Coords) { - if (!editing) { - if (ev.metaKey || ev.ctrlKey) { - // TODO - // selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords]; - // editor_input_value = ""; - } else { - set_selections([{ region: "main", coords }]); - } + if (ev.metaKey || ev.ctrlKey) { + // TODO + // selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords]; + // editor_input_value = ""; + } else { + set_selections([{ region: "main", coords }]); } } // -------- Event Handlers: Inserter Table -------- // function handle_inserter_cell_click(ev: MouseEvent, coords: Coords) { - if (!editing) { - if (ev.metaKey || ev.ctrlKey) { - // TODO - // selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords]; - // editor_input_value = ""; - } else { - 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(); + if (ev.metaKey || ev.ctrlKey) { + // TODO + // selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords]; + // editor_input_value = ""; + } else { + set_selections([{ region: "inserter", coords }]); } } @@ -440,6 +402,8 @@ }, ]; })().catch(console.error); + + setInterval(tick_delta_queue, 500); {#snippet table_region({ @@ -474,24 +438,21 @@ aria-selected={cell_selected} class="lens-table__cell" onmousedown={(ev) => on_cell_click(ev, cell_coords)} - ondblclick={() => handle_table_cell_dblclick(cell_coords)} + ondblclick={() => { + datum_editor?.focus(); + }} role="gridcell" style:width={`${field.field.table_width_px}px`} tabindex="-1" >
{#if cell_data.t === "Text"}
{#if cell_data.c === undefined} {@html null_value_html} @@ -501,11 +462,8 @@
{:else if cell_data.t === "Uuid"}
{#if cell_data.c === undefined} {@html null_value_html} @@ -514,9 +472,7 @@ {/if}
{:else} -
+
UNKNOWN
{/if} @@ -538,7 +494,9 @@
{ + try_queue_delta(); + }} onkeydown={handle_table_keydown} role="grid" tabindex="0" @@ -593,28 +551,17 @@ {/each}
-
- {#if selections.length === 1 && editor_state} +
+ {#if selections.length === 1} { + try_sync_edit_to_cells(); + }} /> {/if} -
{/if}