implement timestamp editing (awkwardly)

This commit is contained in:
Brent Schroeter 2025-11-11 03:36:31 +00:00
parent 68ca114d5e
commit 275129933d
9 changed files with 115 additions and 22 deletions

10
deno.lock generated
View file

@ -9,9 +9,11 @@
"jsr:@std/path@^1.1.1": "1.1.1", "jsr:@std/path@^1.1.1": "1.1.1",
"jsr:@std/uuid@*": "1.0.9", "jsr:@std/uuid@*": "1.0.9",
"jsr:@std/uuid@^1.0.9": "1.0.9", "jsr:@std/uuid@^1.0.9": "1.0.9",
"npm:@date-fns/utc@^2.1.1": "2.1.1",
"npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0", "npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0",
"npm:@sveltejs/vite-plugin-svelte@^6.1.1": "6.1.1_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0", "npm:@sveltejs/vite-plugin-svelte@^6.1.1": "6.1.1_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3_sass-embedded@1.91.0",
"npm:@tsconfig/svelte@^5.0.4": "5.0.4", "npm:@tsconfig/svelte@^5.0.4": "5.0.4",
"npm:date-fns@^4.1.0": "4.1.0",
"npm:sass-embedded@^1.91.0": "1.91.0", "npm:sass-embedded@^1.91.0": "1.91.0",
"npm:svelte-check@^4.3.1": "4.3.1_svelte@5.38.1__acorn@8.15.0_typescript@5.8.3", "npm:svelte-check@^4.3.1": "4.3.1_svelte@5.38.1__acorn@8.15.0_typescript@5.8.3",
"npm:svelte-language-server@~0.17.19": "0.17.19_prettier@3.3.3_svelte@4.2.20_typescript@5.9.2", "npm:svelte-language-server@~0.17.19": "0.17.19_prettier@3.3.3_svelte@4.2.20_typescript@5.9.2",
@ -65,6 +67,9 @@
"@bufbuild/protobuf@2.7.0": { "@bufbuild/protobuf@2.7.0": {
"integrity": "sha512-qn6tAIZEw5i/wiESBF4nQxZkl86aY4KoO0IkUa2Lh+rya64oTOdJQFlZuMwI1Qz9VBJQrQC4QlSA2DNek5gCOA==" "integrity": "sha512-qn6tAIZEw5i/wiESBF4nQxZkl86aY4KoO0IkUa2Lh+rya64oTOdJQFlZuMwI1Qz9VBJQrQC4QlSA2DNek5gCOA=="
}, },
"@date-fns/utc@2.1.1": {
"integrity": "sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA=="
},
"@deno/vite-plugin@1.0.5_vite@7.1.2__picomatch@4.0.3": { "@deno/vite-plugin@1.0.5_vite@7.1.2__picomatch@4.0.3": {
"integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==", "integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==",
"dependencies": [ "dependencies": [
@ -687,6 +692,9 @@
"source-map-js" "source-map-js"
] ]
}, },
"date-fns@4.1.0": {
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
},
"debug@4.4.1": { "debug@4.4.1": {
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dependencies": [ "dependencies": [
@ -1351,9 +1359,11 @@
], ],
"packageJson": { "packageJson": {
"dependencies": [ "dependencies": [
"npm:@date-fns/utc@^2.1.1",
"npm:@deno/vite-plugin@^1.0.5", "npm:@deno/vite-plugin@^1.0.5",
"npm:@sveltejs/vite-plugin-svelte@^6.1.1", "npm:@sveltejs/vite-plugin-svelte@^6.1.1",
"npm:@tsconfig/svelte@^5.0.4", "npm:@tsconfig/svelte@^5.0.4",
"npm:date-fns@^4.1.0",
"npm:sass-embedded@^1.91.0", "npm:sass-embedded@^1.91.0",
"npm:svelte-check@^4.3.1", "npm:svelte-check@^4.3.1",
"npm:svelte-language-server@~0.17.19", "npm:svelte-language-server@~0.17.19",

View file

@ -2,7 +2,7 @@ use interim_pgtypes::pg_attribute::PgAttribute;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString}; use strum::{EnumIter, EnumString};
pub const RFC_3339_S: &str = "%Y-%m-%dT%H:%M:%S"; pub const RFC_3339_S: &str = "yyyy-MM-dd'T'HH:mm:ssXXX";
/// Struct defining how a field's is displayed and how it accepts input in UI. /// Struct defining how a field's is displayed and how it accepts input in UI.
#[derive(Clone, Debug, Deserialize, EnumIter, EnumString, PartialEq, Serialize, strum::Display)] #[derive(Clone, Debug, Deserialize, EnumIter, EnumString, PartialEq, Serialize, strum::Display)]
@ -18,6 +18,7 @@ pub enum Presentation {
}, },
Timestamp { Timestamp {
format: String, format: String,
utc: bool,
}, },
Uuid {}, Uuid {},
} }
@ -50,6 +51,7 @@ impl Presentation {
}), }),
"timestamp" => Some(Self::Timestamp { "timestamp" => Some(Self::Timestamp {
format: RFC_3339_S.to_owned(), format: RFC_3339_S.to_owned(),
utc: true,
}), }),
"uuid" => Some(Self::Uuid {}), "uuid" => Some(Self::Uuid {}),
_ => None, _ => None,

View file

@ -52,6 +52,7 @@ impl TryFrom<PresentationForm> for Presentation {
} else { } else {
form_value.timestamp_format form_value.timestamp_format
}, },
utc: true,
}, },
Presentation::Uuid { .. } => Presentation::Uuid {}, Presentation::Uuid { .. } => Presentation::Uuid {},
}) })

View file

@ -10,8 +10,10 @@
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json" "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
}, },
"dependencies": { "dependencies": {
"@date-fns/utc": "^2.1.1",
"@deno/vite-plugin": "^1.0.5", "@deno/vite-plugin": "^1.0.5",
"@sveltejs/vite-plugin-svelte": "^6.1.1", "@sveltejs/vite-plugin-svelte": "^6.1.1",
"date-fns": "^4.1.0",
"sass-embedded": "^1.91.0", "sass-embedded": "^1.91.0",
"svelte-language-server": "^0.17.19", "svelte-language-server": "^0.17.19",
"uuid": "^11.1.0", "uuid": "^11.1.0",

View file

@ -103,6 +103,13 @@ $table-border-color: #ccc;
padding: 0 8px; padding: 0 8px;
} }
&--timestamp {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 0 8px;
}
&--uuid { &--uuid {
font-family: globals.$font-family-mono; font-family: globals.$font-family-mono;
overflow: hidden; overflow: hidden;
@ -331,6 +338,17 @@ $table-border-color: #ccc;
padding: 0.75rem 0.5rem; padding: 0.75rem 0.5rem;
} }
&__timestamp-inputs {
align-items: center;
display: flex;
grid-area: value-control;
justify-content: start;
input {
@include globals.reset_input;
}
}
&__helpers { &__helpers {
grid-area: helpers; grid-area: helpers;
overflow: auto; overflow: auto;

View file

@ -55,6 +55,7 @@
HTMLButtonElement | undefined HTMLButtonElement | undefined
>(); >();
let type_selector_popover_element = $state<HTMLDivElement | undefined>(); let type_selector_popover_element = $state<HTMLDivElement | undefined>();
let date_input_element = $state<HTMLInputElement | undefined>();
let text_input_element = $state<HTMLInputElement | undefined>(); let text_input_element = $state<HTMLInputElement | undefined>();
let blur_timeout = $state<number | undefined>(); let blur_timeout = $state<number | undefined>();
@ -65,7 +66,16 @@
}); });
export function focus() { export function focus() {
if (
field_info.field.presentation.t === "Dropdown" ||
field_info.field.presentation.t === "Numeric" ||
field_info.field.presentation.t === "Text" ||
field_info.field.presentation.t === "Uuid"
) {
text_input_element?.focus(); text_input_element?.focus();
} else if (field_info.field.presentation.t === "Timestamp") {
date_input_element?.focus();
}
} }
function handle_blur(ev: FocusEvent) { function handle_blur(ev: FocusEvent) {
@ -204,16 +214,38 @@
type="text" type="text"
/> />
{:else if field_info.field.presentation.t === "Timestamp"} {:else if field_info.field.presentation.t === "Timestamp"}
<div class="datum-editor__timestamp-inputs">
<input <input
{...interactive_handlers} {...interactive_handlers}
bind:this={date_input_element}
oninput={({ currentTarget }) => {
if (!editor_state) {
console.warn("date input oninput() preconditions not met");
return;
}
editor_state.date_value = currentTarget.value;
editor_state.is_null = false;
handle_input();
}}
value={editor_state.date_value} value={editor_state.date_value}
type="date" type="date"
/> />
<input <input
{...interactive_handlers} {...interactive_handlers}
oninput={({ currentTarget }) => {
if (!editor_state) {
console.warn("time input oninput() preconditions not met");
return;
}
editor_state.time_value = currentTarget.value;
editor_state.is_null = false;
handle_input();
}}
value={editor_state.time_value} value={editor_state.time_value}
step="1"
type="time" type="time"
/> />
</div>
{/if} {/if}
<div class="datum-editor__helpers" tabindex="-1"> <div class="datum-editor__helpers" tabindex="-1">
{#if field_info.field.presentation.t === "Dropdown"} {#if field_info.field.presentation.t === "Dropdown"}

View file

@ -1,4 +1,5 @@
import * as uuid from "uuid"; import { format as format_date, parse as parse_date } from "date-fns";
import { utc } from "@date-fns/utc";
import { type Datum, parse_datum_from_text } from "./datum.svelte.ts"; import { type Datum, parse_datum_from_text } from "./datum.svelte.ts";
import { import {
@ -36,14 +37,11 @@ export function editor_state_from_datum(value: Datum): EditorState {
return { return {
...DEFAULT_EDITOR_STATE, ...DEFAULT_EDITOR_STATE,
date_value: value.c date_value: value.c
? `${value.c.getFullYear()}-${ // FIXME: time zones
value.c.getMonth() + 1 ? format_date(value.c, "yyyy-MM-dd")
}-${value.c.getDate()}`
: "", : "",
is_null: value.c === undefined, is_null: value.c === undefined,
time_value: value.c time_value: value.c ? format_date(value.c, "HH:mm:ss") : "",
? `${value.c.getHours()}:${value.c.getMinutes()}`
: "",
}; };
} else if (value.t === "Uuid") { } else if (value.t === "Uuid") {
return { return {
@ -76,8 +74,22 @@ export function datum_from_editor_state(
return parsed; return parsed;
} }
if (presentation.t === "Timestamp") { if (presentation.t === "Timestamp") {
// FIXME if (value.is_null) {
throw new Error("not yet implemented"); return get_empty_datum_for(presentation);
}
if (value.date_value === "" || value.time_value === "") {
return undefined;
}
const parsed = parse_date(
`${value.date_value}T${value.time_value}`,
"yyyy-MM-dd'T'HH:mm:ss",
new Date(),
{ in: utc },
);
if (Number.isNaN(parsed)) {
return undefined;
}
return { t: "Timestamp", c: parsed };
} }
type _ = Assert<typeof presentation extends never ? true : false>; type _ = Assert<typeof presentation extends never ? true : false>;
throw new Error("this should be unreachable"); throw new Error("this should be unreachable");

View file

@ -81,7 +81,9 @@ export type PresentationText = z.infer<typeof presentation_text_schema>;
const presentation_timestamp_schema = z.object({ const presentation_timestamp_schema = z.object({
t: z.literal("Timestamp"), t: z.literal("Timestamp"),
c: z.unknown(), c: z.object({
format: z.string(),
}),
}); });
export type PresentationTimestamp = z.infer< export type PresentationTimestamp = z.infer<

View file

@ -1,4 +1,7 @@
<script lang="ts"> <script lang="ts">
import { utc } from "@date-fns/utc";
import { format as format_date } from "date-fns";
import { type Coords } from "./coords.svelte"; import { type Coords } from "./coords.svelte";
import { type Datum } from "./datum.svelte"; import { type Datum } from "./datum.svelte";
import { type FieldInfo } from "./field.svelte"; import { type FieldInfo } from "./field.svelte";
@ -135,6 +138,17 @@
{value.c} {value.c}
{/if} {/if}
</div> </div>
{:else if field.field.presentation.t === "Timestamp"}
<div
class="lens-cell__content lens-cell__content--timestamp"
class:lens-cell__content--null={value.c === undefined}
>
{#if value.c === undefined}
<i class={["ti", null_value_class]}></i>
{:else}
{format_date(value.c, field.field.presentation.c.format, { in: utc })}
{/if}
</div>
{:else} {:else}
<div class="lens-cell__content lens-cell__content--unknown"> <div class="lens-cell__content lens-cell__content--unknown">
<div>UNKNOWN</div> <div>UNKNOWN</div>