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!(
"select {0} from {1}.{2}",
"select {0} from {1}.{2} order by _id",
pkey_attrs
.iter()
.chain(attrs.iter())

View file

@ -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<App> {
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}/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))

View file

@ -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<CellInfo>,
}
#[derive(Debug, Deserialize)]
pub(super) struct CellInfo {
column: String,
pkeys: HashMap<String, Datum>,
pkey: HashMap<String, 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
/// [`PathParams`].
@ -55,7 +57,7 @@ pub(super) async fn post(
rel_oid,
workspace_id,
}): Path<PathParams>,
Form(form): Form<FormBody>,
Json(form): Json<FormBody>,
) -> Result<Response, AppError> {
// 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())
}

View file

@ -26,6 +26,5 @@
</main>
</div>
<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>
{% endblock %}

View file

@ -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;
}
}

View file

@ -1,23 +1,64 @@
<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";
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>;
editor_state: EditorState;
field_info: FieldInfo;
on_change?(value?: Datum): void;
value?: Datum;
};
let {
assignable_fields = [],
editor_state = $bindable(),
field_info = $bindable(),
on_change,
value = $bindable(),
}: Props = $props();
let editor_state = $state<EditorState | undefined>();
let type_selector_menu_button_element = $state<
HTMLButtonElement | 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() {
type_selector_popover_element?.togglePopover();
@ -30,40 +71,76 @@
}
</script>
<div class="datum-editor__container">
{#if assignable_fields.length > 0}
<div class="datum-editor__type-selector">
<button
bind:this={type_selector_menu_button_element}
class="datum-editor__type-selector-menu-button"
onclick={handle_type_selector_menu_button_click}
type="button"
>
{field_info.field.presentation.t}
</button>
<div
bind:this={type_selector_popover_element}
class="datum-editor__type-selector-popover"
popover="auto"
>
{#each assignable_fields as assignable_field_info}
<button
onclick={() =>
handle_type_selector_field_button_click(assignable_field_info)}
type="button"
>
{assignable_field_info.field.presentation.t}
</button>
{/each}
<div
class="datum-editor__container"
class:datum-editor__container--incomplete={!value}
>
{#if editor_state}
{#if assignable_fields?.length > 0}
<div class="datum-editor__type-selector">
<button
bind:this={type_selector_menu_button_element}
class="datum-editor__type-selector-menu-button"
onclick={handle_type_selector_menu_button_click}
type="button"
>
{field_info.field.presentation.t}
</button>
<div
bind:this={type_selector_popover_element}
class="datum-editor__type-selector-popover"
popover="auto"
>
{#each assignable_fields as assignable_field_info}
<button
onclick={() =>
handle_type_selector_field_button_click(assignable_field_info)}
type="button"
>
{assignable_field_info.field.presentation.t}
</button>
{/each}
</div>
</div>
</div>
{/if}
<div class="datum-editor__content">
{#if field_info.field.presentation.t === "Text" || field_info.field.presentation.t === "Uuid"}
<input bind:value={editor_state.text_value} type="text" />
{:else if field_info.field.presentation.t === "Timestamp"}
<input bind:value={editor_state.date_value} type="date" />
<input bind:value={editor_state.time_value} type="time" />
{/if}
</div>
<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"}
<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"}
<input value={editor_state.date_value} type="date" />
<input value={editor_state.time_value} type="time" />
{/if}
<div class="datum-editor__helpers"></div>
{/if}
</div>

View file

@ -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;

View file

@ -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<EditorState>(
value?.t === "Literal"
? editor_state_from_datum(value.c)
: DEFAULT_EDITOR_STATE,
);
// Dynamic state to bind to datum editor.
let editor_value = $state<Datum | undefined>();
let editor_field_info = $state<FieldInfo>(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;
}
}
</script>
<div class="expression-editor__container">
@ -102,9 +92,10 @@
</select>
{:else if value.t === "Literal"}
<DatumEditor
bind:editor_state
bind:field_info={editor_field_info}
bind:value={editor_value}
assignable_fields={ASSIGNABLE_FIELDS}
on_change={handle_editor_change}
/>
{/if}
</div>

View file

@ -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<string, Datum>;
let selections = $state<Selection[]>([]);
let editing = $state(false);
let editor_state = $state<EditorState>(DEFAULT_EDITOR_STATE);
let committed_changes = $state<CommittedChange[][]>([]);
let reverted_changes = $state<CommittedChange[][]>([]);
let editor_input_element = $state<HTMLInputElement | undefined>();
// 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<Datum | undefined>(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 inserter_rows = $state<Row[]>([]);
let lazy_data = $state<LazyData | undefined>();
@ -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);
</script>
{#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"
>
<div
class={[
"lens-cell__container",
cell_selected && "lens-cell__container--selected",
]}
class="lens-cell__container"
class:lens-cell__container--selected={cell_selected}
>
{#if cell_data.t === "Text"}
<div
class={[
"lens-cell__content",
"lens-cell__content--text",
cell_data.c === undefined && "lens-cell__content--null",
]}
class="lens-cell__content lens-cell__content--text"
class:lens-cell__content--null={cell_data.c === undefined}
>
{#if cell_data.c === undefined}
{@html null_value_html}
@ -501,11 +462,8 @@
</div>
{:else if cell_data.t === "Uuid"}
<div
class={[
"lens-cell__content",
"lens-cell__content--uuid",
cell_data.c === undefined && "lens-cell__content--null",
]}
class="lens-cell__content lens-cell__content--uuid"
class:lens-cell__content--null={cell_data.c === undefined}
>
{#if cell_data.c === undefined}
{@html null_value_html}
@ -514,9 +472,7 @@
{/if}
</div>
{:else}
<div
class={["lens-cell__content", "lens-cell__content--unknown"]}
>
<div class="lens-cell__content lens-cell__content--unknown">
<div>UNKNOWN</div>
</div>
{/if}
@ -538,7 +494,9 @@
<div
bind:this={table_element}
class="lens-table"
onfocus={handle_table_focus}
onfocus={() => {
try_queue_delta();
}}
onkeydown={handle_table_keydown}
role="grid"
tabindex="0"
@ -593,28 +551,17 @@
{/each}
</form>
</div>
<div class="datum-editor">
{#if selections.length === 1 && editor_state}
<div class="table-viewer__datum-editor">
{#if selections.length === 1}
<DatumEditor
bind:editor_state
bind:this={datum_editor}
bind:value={editor_value}
field_info={lazy_data.fields[selections[0].coords[1]]}
on_change={() => {
try_sync_edit_to_cells();
}}
/>
{/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>
{/if}
</div>