diff --git a/deno.lock b/deno.lock index 5293913..1a523ef 100644 --- a/deno.lock +++ b/deno.lock @@ -9,9 +9,11 @@ "jsr:@std/path@^1.1.1": "1.1.1", "jsr:@std/uuid@*": "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:@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:date-fns@^4.1.0": "4.1.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-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": { "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": { "integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==", "dependencies": [ @@ -687,6 +692,9 @@ "source-map-js" ] }, + "date-fns@4.1.0": { + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" + }, "debug@4.4.1": { "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dependencies": [ @@ -1351,9 +1359,11 @@ ], "packageJson": { "dependencies": [ + "npm:@date-fns/utc@^2.1.1", "npm:@deno/vite-plugin@^1.0.5", "npm:@sveltejs/vite-plugin-svelte@^6.1.1", "npm:@tsconfig/svelte@^5.0.4", + "npm:date-fns@^4.1.0", "npm:sass-embedded@^1.91.0", "npm:svelte-check@^4.3.1", "npm:svelte-language-server@~0.17.19", diff --git a/interim-models/src/presentation.rs b/interim-models/src/presentation.rs index b61b117..754b166 100644 --- a/interim-models/src/presentation.rs +++ b/interim-models/src/presentation.rs @@ -2,7 +2,7 @@ use interim_pgtypes::pg_attribute::PgAttribute; use serde::{Deserialize, Serialize}; 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. #[derive(Clone, Debug, Deserialize, EnumIter, EnumString, PartialEq, Serialize, strum::Display)] @@ -18,6 +18,7 @@ pub enum Presentation { }, Timestamp { format: String, + utc: bool, }, Uuid {}, } @@ -50,6 +51,7 @@ impl Presentation { }), "timestamp" => Some(Self::Timestamp { format: RFC_3339_S.to_owned(), + utc: true, }), "uuid" => Some(Self::Uuid {}), _ => None, diff --git a/interim-server/src/presentation_form.rs b/interim-server/src/presentation_form.rs index 2172a08..0f10aab 100644 --- a/interim-server/src/presentation_form.rs +++ b/interim-server/src/presentation_form.rs @@ -52,6 +52,7 @@ impl TryFrom for Presentation { } else { form_value.timestamp_format }, + utc: true, }, Presentation::Uuid { .. } => Presentation::Uuid {}, }) diff --git a/package.json b/package.json index 9732f23..0c5bd41 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json" }, "dependencies": { + "@date-fns/utc": "^2.1.1", "@deno/vite-plugin": "^1.0.5", "@sveltejs/vite-plugin-svelte": "^6.1.1", + "date-fns": "^4.1.0", "sass-embedded": "^1.91.0", "svelte-language-server": "^0.17.19", "uuid": "^11.1.0", diff --git a/sass/viewer.scss b/sass/viewer.scss index 14e0e76..1544bd9 100644 --- a/sass/viewer.scss +++ b/sass/viewer.scss @@ -103,6 +103,13 @@ $table-border-color: #ccc; padding: 0 8px; } + &--timestamp { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0 8px; + } + &--uuid { font-family: globals.$font-family-mono; overflow: hidden; @@ -331,6 +338,17 @@ $table-border-color: #ccc; 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 { grid-area: helpers; overflow: auto; diff --git a/svelte/src/datum-editor.svelte b/svelte/src/datum-editor.svelte index b72dcc8..7ac2c7d 100644 --- a/svelte/src/datum-editor.svelte +++ b/svelte/src/datum-editor.svelte @@ -55,6 +55,7 @@ HTMLButtonElement | undefined >(); let type_selector_popover_element = $state(); + let date_input_element = $state(); let text_input_element = $state(); let blur_timeout = $state(); @@ -65,7 +66,16 @@ }); export function focus() { - text_input_element?.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(); + } else if (field_info.field.presentation.t === "Timestamp") { + date_input_element?.focus(); + } } function handle_blur(ev: FocusEvent) { @@ -204,16 +214,38 @@ type="text" /> {:else if field_info.field.presentation.t === "Timestamp"} - - +
+ { + 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} + type="date" + /> + { + 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} + step="1" + type="time" + /> +
{/if}
{#if field_info.field.presentation.t === "Dropdown"} diff --git a/svelte/src/editor-state.svelte.ts b/svelte/src/editor-state.svelte.ts index c0c761e..97f6748 100644 --- a/svelte/src/editor-state.svelte.ts +++ b/svelte/src/editor-state.svelte.ts @@ -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 { @@ -36,14 +37,11 @@ export function editor_state_from_datum(value: Datum): EditorState { return { ...DEFAULT_EDITOR_STATE, date_value: value.c - ? `${value.c.getFullYear()}-${ - value.c.getMonth() + 1 - }-${value.c.getDate()}` + // FIXME: time zones + ? format_date(value.c, "yyyy-MM-dd") : "", is_null: value.c === undefined, - time_value: value.c - ? `${value.c.getHours()}:${value.c.getMinutes()}` - : "", + time_value: value.c ? format_date(value.c, "HH:mm:ss") : "", }; } else if (value.t === "Uuid") { return { @@ -76,8 +74,22 @@ export function datum_from_editor_state( return parsed; } if (presentation.t === "Timestamp") { - // FIXME - throw new Error("not yet implemented"); + if (value.is_null) { + 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; throw new Error("this should be unreachable"); diff --git a/svelte/src/presentation.svelte.ts b/svelte/src/presentation.svelte.ts index a59bcad..089b187 100644 --- a/svelte/src/presentation.svelte.ts +++ b/svelte/src/presentation.svelte.ts @@ -81,7 +81,9 @@ export type PresentationText = z.infer; const presentation_timestamp_schema = z.object({ t: z.literal("Timestamp"), - c: z.unknown(), + c: z.object({ + format: z.string(), + }), }); export type PresentationTimestamp = z.infer< diff --git a/svelte/src/table-cell.svelte b/svelte/src/table-cell.svelte index 7495eb6..ef8ae26 100644 --- a/svelte/src/table-cell.svelte +++ b/svelte/src/table-cell.svelte @@ -1,4 +1,7 @@