add support for numeric fields
This commit is contained in:
parent
95a4165163
commit
68ca114d5e
19 changed files with 171 additions and 69 deletions
31
Cargo.lock
generated
31
Cargo.lock
generated
|
|
@ -397,6 +397,21 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bigdecimal"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
|
|
@ -1739,6 +1754,7 @@ dependencies = [
|
|||
name = "interim-models"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bigdecimal",
|
||||
"chrono",
|
||||
"derive_builder",
|
||||
"interim-pgtypes",
|
||||
|
|
@ -1786,6 +1802,7 @@ dependencies = [
|
|||
"async-session",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"bigdecimal",
|
||||
"chrono",
|
||||
"clap",
|
||||
"config",
|
||||
|
|
@ -2079,6 +2096,16 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.4"
|
||||
|
|
@ -3203,6 +3230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bigdecimal",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
|
|
@ -3280,6 +3308,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
|||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
"bigdecimal",
|
||||
"bitflags 2.9.4",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
|
|
@ -3324,6 +3353,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
|||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
"bigdecimal",
|
||||
"bitflags 2.9.4",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
|
|
@ -3341,6 +3371,7 @@ dependencies = [
|
|||
"log",
|
||||
"md-5",
|
||||
"memchr",
|
||||
"num-bigint",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ edition = "2024"
|
|||
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.91", features = ["backtrace"] }
|
||||
bigdecimal = { version = "0.4.9", features = ["serde-json"] }
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
derive_builder = "0.20.2"
|
||||
futures = "0.3.31"
|
||||
|
|
@ -22,7 +23,7 @@ regex = "1.11.1"
|
|||
reqwest = { version = "0.12.8", features = ["json"] }
|
||||
serde = { version = "1.0.213", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "tls-rustls-ring-native-roots", "postgres", "derive", "uuid", "chrono", "json", "macros"] }
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "tls-rustls-ring-native-roots", "postgres", "derive", "uuid", "chrono", "json", "macros", "bigdecimal"] }
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.42.0", features = ["full"] }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ edition.workspace = true
|
|||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bigdecimal = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
derive_builder = { workspace = true }
|
||||
interim-pgtypes = { path = "../interim-pgtypes" }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use bigdecimal::BigDecimal;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::Postgres;
|
||||
|
|
@ -6,6 +7,14 @@ use uuid::Uuid;
|
|||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum Datum {
|
||||
// BigDecimal is used because a user may insert a value directly via SQL
|
||||
// which overflows the representational space of `rust_decimal::Decimal`.
|
||||
// Note that by default, [`BigDecimal`] serializes to JSON as a string. This
|
||||
// behavior can be modified, but it's a pain when paired with the [`Option`]
|
||||
// type. String representation should be acceptable for the UI, as [`Datum`]
|
||||
// values should always be parsed through Zod, which can coerce the value to
|
||||
// a number transparently.
|
||||
Numeric(Option<BigDecimal>),
|
||||
Text(Option<String>),
|
||||
Timestamp(Option<DateTime<Utc>>),
|
||||
Uuid(Option<Uuid>),
|
||||
|
|
@ -19,6 +28,7 @@ impl Datum {
|
|||
query: sqlx::query::Query<'a, Postgres, <sqlx::Postgres as sqlx::Database>::Arguments<'a>>,
|
||||
) -> sqlx::query::Query<'a, Postgres, <sqlx::Postgres as sqlx::Database>::Arguments<'a>> {
|
||||
match self {
|
||||
Self::Numeric(value) => query.bind(value),
|
||||
Self::Text(value) => query.bind(value),
|
||||
Self::Timestamp(value) => query.bind(value),
|
||||
Self::Uuid(value) => query.bind(value),
|
||||
|
|
@ -38,8 +48,10 @@ impl Datum {
|
|||
|
||||
pub fn is_none(&self) -> bool {
|
||||
match self {
|
||||
Self::Text(None) | Self::Timestamp(None) | Self::Uuid(None) => true,
|
||||
Self::Text(_) | Self::Timestamp(_) | Self::Uuid(_) => false,
|
||||
Self::Numeric(None) | Self::Text(None) | Self::Timestamp(None) | Self::Uuid(None) => {
|
||||
true
|
||||
}
|
||||
Self::Numeric(_) | Self::Text(_) | Self::Timestamp(_) | Self::Uuid(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use bigdecimal::BigDecimal;
|
||||
use chrono::{DateTime, Utc};
|
||||
use derive_builder::Builder;
|
||||
use interim_pgtypes::pg_attribute::PgAttribute;
|
||||
|
|
@ -72,6 +73,9 @@ impl Field {
|
|||
let ty = type_info.name();
|
||||
dbg!(&ty);
|
||||
Ok(match ty {
|
||||
"NUMERIC" => {
|
||||
Datum::Numeric(<Option<BigDecimal> as Decode<Postgres>>::decode(value_ref).unwrap())
|
||||
}
|
||||
"TEXT" | "VARCHAR" => {
|
||||
Datum::Text(<Option<String> as Decode<Postgres>>::decode(value_ref).unwrap())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ pub enum Presentation {
|
|||
allow_custom: bool,
|
||||
options: Vec<DropdownOption>,
|
||||
},
|
||||
Numeric {},
|
||||
Text {
|
||||
input_mode: TextInputMode,
|
||||
},
|
||||
|
|
@ -30,11 +31,12 @@ pub struct DropdownOption {
|
|||
impl Presentation {
|
||||
/// Returns a SQL fragment for the default data type for creating or
|
||||
/// altering a backing column, such as "integer", or "timestamptz".
|
||||
pub fn attr_data_type_fragment(&self) -> String {
|
||||
pub fn attr_data_type_fragment(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Dropdown { .. } | Self::Text { .. } => "text".to_owned(),
|
||||
Self::Timestamp { .. } => "timestamptz".to_owned(),
|
||||
Self::Uuid { .. } => "uuid".to_owned(),
|
||||
Self::Dropdown { .. } | Self::Text { .. } => "text",
|
||||
Self::Numeric { .. } => "numeric",
|
||||
Self::Timestamp { .. } => "timestamptz",
|
||||
Self::Uuid { .. } => "uuid",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +44,7 @@ impl Presentation {
|
|||
/// Returns None if no default presentation exists.
|
||||
pub fn default_from_attr(attr: &PgAttribute) -> Option<Self> {
|
||||
match attr.regtype.to_lowercase().as_str() {
|
||||
"numeric" => Some(Self::Numeric {}),
|
||||
"text" => Some(Self::Text {
|
||||
input_mode: TextInputMode::MultiLine {},
|
||||
}),
|
||||
|
|
@ -52,16 +55,6 @@ impl Presentation {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bet the web component tag name to use for rendering a UI cell.
|
||||
pub fn cell_webc_tag(&self) -> String {
|
||||
match self {
|
||||
Self::Dropdown { .. } => "cell-dropdown".to_owned(),
|
||||
Self::Text { .. } => "cell-text".to_owned(),
|
||||
Self::Timestamp { .. } => "cell-timestamp".to_owned(),
|
||||
Self::Uuid {} => "cell-uuid".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Presentation {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ askama = { version = "0.14.0", features = ["serde_json", "urlencode"] }
|
|||
async-session = "3.0.0"
|
||||
axum = { version = "0.8.1", features = ["macros", "ws"] }
|
||||
axum-extra = { version = "0.10.0", features = ["cookie", "form", "typed-header"] }
|
||||
bigdecimal = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { version = "4.5.31", features = ["derive"] }
|
||||
config = "0.14.1"
|
||||
|
|
|
|||
|
|
@ -29,21 +29,21 @@ pub(crate) struct Navigator {
|
|||
}
|
||||
|
||||
impl Navigator {
|
||||
pub(crate) fn workspace_page(&self) -> WorkspacePageBuilder {
|
||||
pub(crate) fn workspace_page(&'_ self) -> WorkspacePageBuilder<'_> {
|
||||
WorkspacePageBuilder {
|
||||
root_path: Some(&self.root_path),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn portal_page(&self) -> PortalPageBuilder {
|
||||
pub(crate) fn portal_page(&'_ self) -> PortalPageBuilder<'_> {
|
||||
PortalPageBuilder {
|
||||
root_path: Some(&self.root_path),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn form_page(&self, portal_id: Uuid) -> FormPageBuilder {
|
||||
pub(crate) fn form_page(&'_ self, portal_id: Uuid) -> FormPageBuilder<'_> {
|
||||
FormPageBuilder {
|
||||
root_path: Some(&self.root_path),
|
||||
portal_id: Some(portal_id),
|
||||
|
|
@ -52,7 +52,7 @@ impl Navigator {
|
|||
|
||||
/// Returns a [`NavigatorPage`] builder for navigating to a relation's
|
||||
/// "settings" page.
|
||||
pub(crate) fn rel_page(&self) -> RelPageBuilder {
|
||||
pub(crate) fn rel_page(&'_ self) -> RelPageBuilder<'_> {
|
||||
RelPageBuilder {
|
||||
root_path: Some(&self.root_path),
|
||||
..Default::default()
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ impl TryFrom<PresentationForm> for Presentation {
|
|||
.map(|(color, value)| DropdownOption { color, value })
|
||||
.collect(),
|
||||
},
|
||||
Presentation::Numeric { .. } => Presentation::Numeric {},
|
||||
Presentation::Text { .. } => Presentation::Text {
|
||||
input_mode: TextInputMode::try_from(form_value.text_input_mode.as_str())?,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -58,14 +58,6 @@ fn default_host() -> String {
|
|||
"127.0.0.1".to_owned()
|
||||
}
|
||||
|
||||
fn default_db_role_prefix() -> String {
|
||||
"__phono__".to_owned()
|
||||
}
|
||||
|
||||
fn default_phono_table_namespace() -> String {
|
||||
"phono".to_owned()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub(crate) struct AuthSettings {
|
||||
pub(crate) client_id: String,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,14 @@ $table-border-color: #ccc;
|
|||
padding: 0 8px;
|
||||
}
|
||||
|
||||
&--numeric {
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
&--text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@
|
|||
<i class="ti ti-cube"></i>
|
||||
{/if}
|
||||
</button>
|
||||
{#if field_info.field.presentation.t === "Dropdown" || field_info.field.presentation.t === "Text" || field_info.field.presentation.t === "Uuid"}
|
||||
{#if ["Dropdown", "Numeric", "Text", "Uuid"].includes(field_info.field.presentation.t)}
|
||||
<input
|
||||
{...interactive_handlers}
|
||||
bind:this={text_input_element}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,37 @@
|
|||
import * as uuid from "uuid";
|
||||
import { z } from "zod";
|
||||
import { get_empty_datum_for, Presentation } from "./presentation.svelte.ts";
|
||||
|
||||
import {
|
||||
get_empty_datum_for,
|
||||
type Presentation,
|
||||
} from "./presentation.svelte.ts";
|
||||
|
||||
type Assert<_T extends true> = void;
|
||||
|
||||
export const all_datum_tags = [
|
||||
"Numeric",
|
||||
"Text",
|
||||
"Timestamp",
|
||||
"Uuid",
|
||||
] as const;
|
||||
|
||||
// Type checking to ensure that all valid enum tags are included.
|
||||
type _ = Assert<
|
||||
type _DatumTagsAssertionA = Assert<
|
||||
Datum["t"] extends (typeof all_datum_tags)[number] ? true : false
|
||||
>;
|
||||
type _DatumTagsAssertionB = Assert<
|
||||
(typeof all_datum_tags)[number] extends Datum["t"] ? true : false
|
||||
>;
|
||||
|
||||
const NUMERIC_REGEX = /^-?[0-9]+(\.[0-9]+)?$/;
|
||||
|
||||
const datum_numeric_schema = z.object({
|
||||
t: z.literal("Numeric"),
|
||||
// The `bigdecimal` Rust library serializes values to strings by default.
|
||||
// Doing the same here helps to avoid weird floating point conversion errors
|
||||
// when displaying.
|
||||
c: z.string().regex(NUMERIC_REGEX).nullish().transform((x) => x ?? undefined),
|
||||
});
|
||||
|
||||
const datum_text_schema = z.object({
|
||||
t: z.literal("Text"),
|
||||
|
|
@ -30,6 +49,7 @@ const datum_uuid_schema = z.object({
|
|||
});
|
||||
|
||||
export const datum_schema = z.union([
|
||||
datum_numeric_schema,
|
||||
datum_text_schema,
|
||||
datum_timestamp_schema,
|
||||
datum_uuid_schema,
|
||||
|
|
@ -37,7 +57,11 @@ export const datum_schema = z.union([
|
|||
|
||||
export type Datum = z.infer<typeof datum_schema>;
|
||||
|
||||
export function parse_clipboard_value(
|
||||
/**
|
||||
* Attempt to parse a Datum from a textual representation, for example when
|
||||
* pasting values from the clipboard. If unsuccessful, returns an empty Datum.
|
||||
*/
|
||||
export function parse_datum_from_text(
|
||||
presentation: Presentation,
|
||||
input: string,
|
||||
): Datum {
|
||||
|
|
@ -50,6 +74,22 @@ export function parse_clipboard_value(
|
|||
} else {
|
||||
return get_empty_datum_for(presentation);
|
||||
}
|
||||
} else if (presentation.t === "Numeric") {
|
||||
// For now, commas and spaces are accepted as thousands-separators, but full
|
||||
// stops are not. Only full stops are allowed as the decimal separator. This
|
||||
// is about as flexible as parsing can be with regards to international
|
||||
// standards from an English-centric perspective, without resorting to
|
||||
// troublesome heuristics to disambiguate between thousands-separators and
|
||||
// decimal separators.
|
||||
const input_cleaned = input.trim().replace(/^\+/, "").replace(
|
||||
/(?:,|\s+)([0-9]{3})(,|.|\s|$)/g,
|
||||
"$1$2",
|
||||
);
|
||||
console.log(NUMERIC_REGEX.test(input_cleaned));
|
||||
return {
|
||||
t: "Numeric",
|
||||
c: NUMERIC_REGEX.test(input_cleaned) ? input_cleaned : undefined,
|
||||
};
|
||||
} else if (presentation.t === "Text") {
|
||||
return { t: "Text", c: input };
|
||||
} else if (presentation.t === "Timestamp") {
|
||||
|
|
@ -57,10 +97,16 @@ export function parse_clipboard_value(
|
|||
console.warn("parsing timestamps from clipboard is not yet supported");
|
||||
return get_empty_datum_for(presentation);
|
||||
} else if (presentation.t === "Uuid") {
|
||||
// TODO: implement
|
||||
console.warn("parsing uuids from clipboard is not yet supported");
|
||||
try {
|
||||
return {
|
||||
t: "Uuid",
|
||||
c: uuid.stringify(uuid.parse(input)),
|
||||
};
|
||||
} catch {
|
||||
// uuid.parse() throws a TypeError if unsuccessful.
|
||||
return get_empty_datum_for(presentation);
|
||||
}
|
||||
}
|
||||
type _ = Assert<typeof presentation["t"] extends never ? true : false>;
|
||||
throw new Error("should be unreachable");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import * as uuid from "uuid";
|
||||
|
||||
import { type Datum } from "./datum.svelte.ts";
|
||||
import { type Presentation } from "./presentation.svelte.ts";
|
||||
import { type Datum, parse_datum_from_text } from "./datum.svelte.ts";
|
||||
import {
|
||||
get_empty_datum_for,
|
||||
type Presentation,
|
||||
} from "./presentation.svelte.ts";
|
||||
|
||||
type Assert<_T extends true> = void;
|
||||
|
||||
|
|
@ -23,7 +26,7 @@ export const DEFAULT_EDITOR_STATE: EditorState = {
|
|||
};
|
||||
|
||||
export function editor_state_from_datum(value: Datum): EditorState {
|
||||
if (value.t === "Text") {
|
||||
if (value.t === "Numeric" || value.t === "Text") {
|
||||
return {
|
||||
...DEFAULT_EDITOR_STATE,
|
||||
text_value: value.c ?? "",
|
||||
|
|
@ -57,43 +60,25 @@ export function datum_from_editor_state(
|
|||
value: EditorState,
|
||||
presentation: Presentation,
|
||||
): Datum | undefined {
|
||||
if (presentation.t === "Dropdown") {
|
||||
if (value.is_null) {
|
||||
return { t: "Text", c: undefined };
|
||||
}
|
||||
if (
|
||||
!presentation.c.allow_custom &&
|
||||
presentation.c.options.every((option) =>
|
||||
option.value !== value.text_value
|
||||
)
|
||||
presentation.t === "Dropdown" || presentation.t === "Numeric" ||
|
||||
presentation.t === "Text" || presentation.t === "Uuid"
|
||||
) {
|
||||
// Presentations which derive values directly from the editor's text input
|
||||
// can all be handled with the same logic.
|
||||
if (value.is_null) {
|
||||
return get_empty_datum_for(presentation);
|
||||
}
|
||||
const parsed = parse_datum_from_text(presentation, value.text_value);
|
||||
if (parsed.c === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
t: "Text",
|
||||
c: value.text_value,
|
||||
};
|
||||
}
|
||||
if (presentation.t === "Text") {
|
||||
return { t: "Text", c: value.is_null ? undefined : value.text_value };
|
||||
return parsed;
|
||||
}
|
||||
if (presentation.t === "Timestamp") {
|
||||
// FIXME
|
||||
throw new Error("not yet implemented");
|
||||
}
|
||||
if (presentation.t === "Uuid") {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
}
|
||||
type _ = Assert<typeof presentation extends never ? true : false>;
|
||||
throw new Error("this should be unreachable");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,9 @@ field. This is typically rendered within a popover component, and within an HTML
|
|||
if (tag === "Dropdown") {
|
||||
return { t: "Dropdown", c: { allow_custom: false, options: [] } };
|
||||
}
|
||||
if (tag === "Numeric") {
|
||||
return { t: "Numeric", c: {} };
|
||||
}
|
||||
if (tag === "Text") {
|
||||
return { t: "Text", c: { input_mode: { t: "SingleLine", c: {} } } };
|
||||
}
|
||||
|
|
@ -153,6 +156,7 @@ field. This is typically rendered within a popover component, and within an HTML
|
|||
value={option.value}
|
||||
/>
|
||||
<button
|
||||
aria-label="Remove option"
|
||||
class="button--clear"
|
||||
onclick={() => {
|
||||
if (presentation?.t !== "Dropdown") {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@
|
|||
<span slot="button-contents">
|
||||
{#if field.field.presentation.t === "Dropdown"}
|
||||
<i class="ti ti-pointer"></i>
|
||||
{:else if field.field.presentation.t === "Numeric"}
|
||||
<i class="ti ti-decimal"></i>
|
||||
{:else if field.field.presentation.t === "Text"}
|
||||
<i class="ti ti-file-text"></i>
|
||||
{:else if field.field.presentation.t === "Timestamp"}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ type Assert<_T extends true> = void;
|
|||
|
||||
export const all_presentation_tags = [
|
||||
"Dropdown",
|
||||
"Numeric",
|
||||
"Text",
|
||||
"Timestamp",
|
||||
"Uuid",
|
||||
|
|
@ -36,6 +37,11 @@ type _TextInputModesAssertionB = Assert<
|
|||
: false
|
||||
>;
|
||||
|
||||
const presentation_numeric_schema = z.object({
|
||||
t: z.literal("Numeric"),
|
||||
c: z.object({}),
|
||||
});
|
||||
|
||||
const text_input_mode_schema = z.union([
|
||||
z.object({
|
||||
t: z.literal("SingleLine"),
|
||||
|
|
@ -91,6 +97,7 @@ export type PresentationUuid = z.infer<typeof presentation_uuid_schema>;
|
|||
|
||||
export const presentation_schema = z.union([
|
||||
presentation_dropdown_schema,
|
||||
presentation_numeric_schema,
|
||||
presentation_text_schema,
|
||||
presentation_timestamp_schema,
|
||||
presentation_uuid_schema,
|
||||
|
|
@ -102,6 +109,9 @@ export function get_empty_datum_for(presentation: Presentation): Datum {
|
|||
if (presentation.t === "Dropdown") {
|
||||
return { t: "Text", c: undefined };
|
||||
}
|
||||
if (presentation.t === "Numeric") {
|
||||
return { t: "Numeric", c: undefined };
|
||||
}
|
||||
if (presentation.t === "Text") {
|
||||
return { t: "Text", c: undefined };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,17 @@
|
|||
{:else}
|
||||
UNKNOWN
|
||||
{/if}
|
||||
{:else if field.field.presentation.t === "Numeric"}
|
||||
<div
|
||||
class="lens-cell__content lens-cell__content--numeric"
|
||||
class:lens-cell__content--null={value.c === undefined}
|
||||
>
|
||||
{#if value.c === undefined}
|
||||
<i class={["ti", null_value_class]}></i>
|
||||
{:else}
|
||||
{value.c}
|
||||
{/if}
|
||||
</div>
|
||||
{:else if field.field.presentation.t === "Text"}
|
||||
<div
|
||||
class="lens-cell__content lens-cell__content--text"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
import {
|
||||
type Datum,
|
||||
datum_schema,
|
||||
parse_clipboard_value,
|
||||
parse_datum_from_text,
|
||||
} from "./datum.svelte";
|
||||
import DatumEditor from "./datum-editor.svelte";
|
||||
import { type Row, type FieldInfo, field_info_schema } from "./field.svelte";
|
||||
|
|
@ -455,7 +455,7 @@
|
|||
const parsed_tsv: Datum[][] = paste_text.split("\n").map((line) => {
|
||||
const raw_values = line.split("\t");
|
||||
return fields.map(({ field: { presentation } }, i) =>
|
||||
parse_clipboard_value(presentation, raw_values[i] ?? ""),
|
||||
parse_datum_from_text(presentation, raw_values[i] ?? ""),
|
||||
);
|
||||
});
|
||||
if (selections.length === 1) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue