Compare commits

..

No commits in common. "647ce5fc526e8c3d6c27586d76a97d3c4086e289" and "1b4cbe85176a63eb1b2d0a35e39b94a7af573faa" have entirely different histories.

7 changed files with 50 additions and 138 deletions

View file

@ -11,7 +11,7 @@ use phono_backends::{
use phono_models::{ use phono_models::{
accessors::{Accessor, Actor, portal::PortalAccessor}, accessors::{Accessor, Actor, portal::PortalAccessor},
datum::Datum, datum::Datum,
expression::{PgExpressionAny, QueryFragment}, expression::QueryFragment,
field::Field, field::Field,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -19,14 +19,11 @@ use sqlx::{
postgres::{PgRow, types::Oid}, postgres::{PgRow, types::Oid},
query, query,
}; };
use tracing::debug;
use uuid::Uuid; use uuid::Uuid;
use validator::Validate;
use crate::{ use crate::{
app::AppDbConn, app::AppDbConn,
errors::AppError, errors::AppError,
extractors::ValidatedForm,
field_info::TableFieldInfo, field_info::TableFieldInfo,
user::CurrentUser, user::CurrentUser,
workspace_pooler::{RoleAssignment, WorkspacePooler}, workspace_pooler::{RoleAssignment, WorkspacePooler},
@ -39,16 +36,10 @@ pub(super) struct PathParams {
workspace_id: Uuid, workspace_id: Uuid,
} }
#[derive(Debug, Deserialize, Validate)]
pub(super) struct FormBody {
subfilter: Option<String>,
}
const FRONTEND_ROW_LIMIT: i64 = 1000; const FRONTEND_ROW_LIMIT: i64 = 1000;
/// HTTP GET handler for an API endpoint returning a JSON encoding of portal /// HTTP GET handler for an API endpoint returning a JSON encoding of portal
/// data to display in a table or similar form. If the `subfilter` URL parameter /// data to display in a table or similar form.
/// is specified, it is `&&`-ed with the portal's stored filter.
/// ///
/// Only queries up to the first [`FRONTEND_ROW_LIMIT`] rows. /// Only queries up to the first [`FRONTEND_ROW_LIMIT`] rows.
pub(super) async fn get( pub(super) async fn get(
@ -60,7 +51,6 @@ pub(super) async fn get(
rel_oid, rel_oid,
workspace_id, workspace_id,
}): Path<PathParams>, }): Path<PathParams>,
ValidatedForm(form): ValidatedForm<FormBody>,
) -> Result<Response, AppError> { ) -> Result<Response, AppError> {
let mut workspace_client = workspace_pooler let mut workspace_client = workspace_pooler
.acquire_for(workspace_id, RoleAssignment::User(user.id)) .acquire_for(workspace_id, RoleAssignment::User(user.id))
@ -119,31 +109,10 @@ pub(super) async fn get(
.join(", "), .join(", "),
rel.get_identifier(), rel.get_identifier(),
)); ));
if let Some(filter_expr) = portal.table_filter.0.clone() { if let Some(filter_expr) = portal.table_filter.0 {
sql_fragment.push(QueryFragment::from_sql(" where ")); sql_fragment.push(QueryFragment::from_sql(" where "));
sql_fragment.push(filter_expr.into_query_fragment()); sql_fragment.push(filter_expr.into_query_fragment());
} }
if let Some(subfilter_expr) = form.subfilter.and_then(|value| {
if value.is_empty() {
None
} else {
serde_json::from_str::<Option<PgExpressionAny>>(&value)
// Ignore invalid input. A user likely pasted incorrectly
// or made a typo.
.inspect_err(|_| debug!("ignoring invalid subfilter expression"))
.ok()
.flatten()
}
}) {
sql_fragment.push(QueryFragment::from_sql(
if portal.table_filter.0.is_some() {
" and "
} else {
" where "
},
));
sql_fragment.push(subfilter_expr.into_query_fragment());
}
sql_fragment.push(QueryFragment::from_sql(" order by _id limit ")); sql_fragment.push(QueryFragment::from_sql(" order by _id limit "));
sql_fragment.push(QueryFragment::from_param(Datum::Numeric(Some( sql_fragment.push(QueryFragment::from_param(Datum::Numeric(Some(
FRONTEND_ROW_LIMIT.into(), FRONTEND_ROW_LIMIT.into(),

View file

@ -12,12 +12,10 @@ use phono_models::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::postgres::types::Oid; use sqlx::postgres::types::Oid;
use uuid::Uuid; use uuid::Uuid;
use validator::Validate;
use crate::{ use crate::{
app::AppDbConn, app::AppDbConn,
errors::AppError, errors::AppError,
extractors::ValidatedForm,
navigator::Navigator, navigator::Navigator,
settings::Settings, settings::Settings,
user::CurrentUser, user::CurrentUser,
@ -32,12 +30,6 @@ pub(super) struct PathParams {
workspace_id: Uuid, workspace_id: Uuid,
} }
#[derive(Debug, Deserialize, Validate)]
pub(super) struct FormBody {
#[serde(default)]
subfilter: String,
}
/// HTTP GET handler for the table viewer page of a [`Portal`]. This handler /// HTTP GET handler for the table viewer page of a [`Portal`]. This handler
/// performs some relatively simple queries pertaining to table structure, but /// performs some relatively simple queries pertaining to table structure, but
/// the bulk of the query logic resides in the [`super::get_data_handler`] /// the bulk of the query logic resides in the [`super::get_data_handler`]
@ -53,9 +45,6 @@ pub(super) async fn get(
rel_oid, rel_oid,
workspace_id, workspace_id,
}): Path<PathParams>, }): Path<PathParams>,
ValidatedForm(FormBody {
subfilter: subfilter_str,
}): ValidatedForm<FormBody>,
) -> Result<Response, AppError> { ) -> Result<Response, AppError> {
let mut workspace_client = pooler let mut workspace_client = pooler
.acquire_for(workspace_id, RoleAssignment::User(user.id)) .acquire_for(workspace_id, RoleAssignment::User(user.id))
@ -100,7 +89,6 @@ pub(super) async fn get(
attr_names: Vec<String>, attr_names: Vec<String>,
filter: Option<PgExpressionAny>, filter: Option<PgExpressionAny>,
settings: Settings, settings: Settings,
subfilter_str: String,
navbar: WorkspaceNav, navbar: WorkspaceNav,
} }
Ok(Html( Ok(Html(
@ -118,7 +106,6 @@ pub(super) async fn get(
Some(RelLocation::Portal(portal.id)), Some(RelLocation::Portal(portal.id)),
)) ))
.build()?, .build()?,
subfilter_str,
settings, settings,
} }
.render()?, .render()?,

View file

@ -26,12 +26,7 @@
</div> </div>
</div> </div>
<main class="page-grid__main"> <main class="page-grid__main">
<table-viewer <table-viewer columns="{{ columns | json }}"></table-viewer>
columns="{{ columns | json }}"
{%- if subfilter_str != "" %}
subfilter="{{ subfilter_str }}"
{% endif -%}
></table-viewer>
</main> </main>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -8,7 +8,7 @@
:root { :root {
--accent-color: #fc0; --accent-color: #fc0;
--default-border-color: #aaa; --default-border-color: #ccc;
--default-border-radius--rounded: 8px; --default-border-radius--rounded: 8px;
--default-border-radius--rounded-sm: 4px; --default-border-radius--rounded-sm: 4px;
--default-font-family: --default-font-family:
@ -39,7 +39,7 @@
--button-background--primary: var(--accent-color); --button-background--primary: var(--accent-color);
--button-background--secondary: #fff; --button-background--secondary: #fff;
--button-border-color--primary: oklch(from var(--button-background--primary) calc(l * 0.9) c h); --button-border-color--primary: oklch(from var(--button-background--primary) calc(l * 0.9) c h);
--button-border-color--secondary: oklch(from var(--button-background--secondary) calc(l * 0.8) c h); --button-border-color--secondary: oklch(from var(--button-background--secondary) calc(l * 0.85) c h);
--button-border-radius: var(--default-border-radius--rounded); --button-border-radius: var(--default-border-radius--rounded);
--button-color--primary: #000; --button-color--primary: #000;
--button-color--secondary: #000; --button-color--secondary: #000;

View file

@ -1,10 +1,5 @@
@import "./expression-editor.css"; @import "./expression-editor.css";
:root {
--table-header-border-color: var(--default-border-color);
--table-cell-border-color: oklch(from var(--default-border-color) calc(l * 1.15) c h);
}
/* ======== Toolbar ======== */ /* ======== Toolbar ======== */
.filter-menu { .filter-menu {
@ -29,7 +24,6 @@
grid-area: table; grid-area: table;
grid-template: grid-template:
'headers' max-content 'headers' max-content
'subfilter-indicator' max-content
'main' 1fr 'main' 1fr
'inserter' max-content; 'inserter' max-content;
height: 100%; height: 100%;
@ -55,7 +49,7 @@
.field-adder__header-lookalike { .field-adder__header-lookalike {
align-items: center; align-items: center;
background: #0001; background: #0001;
border: solid 1px var(--table-header-border-color); border: solid 1px #ccc;
border-top: none; border-top: none;
border-left: none; border-left: none;
display: flex; display: flex;
@ -75,10 +69,7 @@
} }
.field-header__label { .field-header__label {
overflow: hidden;
padding: 4px; padding: 4px;
text-overflow: ellipsis;
white-space: nowrap;
} }
.field-header__type-indicator { .field-header__type-indicator {
@ -131,7 +122,7 @@
.field-adder__completions { .field-adder__completions {
align-items: stretch; align-items: stretch;
border-right: solid 1px var(--table-header-border-color); border-right: solid 1px var(--default-border-color);
grid-area: completions; grid-area: completions;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -158,13 +149,6 @@
padding: var(--default-padding); padding: var(--default-padding);
} }
/* ======== Subfilter Indicator ======== */
.subfilter-indicator {
grid-area: subfilter-indicator;
padding: 8px;
}
/* ======== Table Body ======== */ /* ======== Table Body ======== */
.table-viewer__main { .table-viewer__main {
@ -176,17 +160,11 @@
align-items: stretch; align-items: stretch;
display: flex; display: flex;
height: 2.25rem; height: 2.25rem;
&:first-child {
.table-viewer__cell {
border-top: solid 1px var(--table-cell-border-color);
}
}
} }
.table-viewer__cell { .table-viewer__cell {
align-items: stretch; align-items: stretch;
border: solid 1px var(--table-cell-border-color); border: solid 1px var(--default-border-color);
border-left: none; border-left: none;
border-top: none; border-top: none;
display: flex; display: flex;
@ -267,10 +245,6 @@
} }
} }
} }
.table-viewer__cell-indicator {
padding-right: 4px;
}
} }
.table-viewer__notice { .table-viewer__notice {

View file

@ -156,12 +156,6 @@
<div>UNKNOWN</div> <div>UNKNOWN</div>
</div> </div>
{/if} {/if}
{#if value.t === "Text" && /^[a-z+.-]+:\/\/\S+$/i.test(value.c ?? "")}
<a class="table-viewer__cell-indicator" href={value.c}
><i class="ti ti-external-link"><div class="sr-only">Open link</div></i
></a
>
{/if}
{#if invalid_value} {#if invalid_value}
<div class="table-viewer__cell-notice"> <div class="table-viewer__cell-notice">
<i class="ti ti-alert-circle"></i> <i class="ti ti-alert-circle"></i>

View file

@ -2,7 +2,6 @@
customElement={{ customElement={{
props: { props: {
columns: { type: "Array" }, columns: { type: "Array" },
subfilter: { type: "String" },
}, },
shadow: "none", shadow: "none",
tag: "table-viewer", tag: "table-viewer",
@ -35,10 +34,9 @@
name: string; name: string;
regtype: string; regtype: string;
}[]; }[];
subfilter?: string;
}; };
let { columns = [], subfilter = "null" }: Props = $props(); let { columns = [] }: Props = $props();
type CellDelta = { type CellDelta = {
// Assumes that primary keys are immutable and that rows are only added or // Assumes that primary keys are immutable and that rows are only added or
@ -535,9 +533,7 @@
), ),
fields: z.array(field_info_schema), fields: z.array(field_info_schema),
}); });
const resp = await fetch( const resp = await fetch("get-data");
`get-data?subfilter=${encodeURIComponent(subfilter)}`,
);
const body = get_data_response_schema.parse(await resp.json()); const body = get_data_response_schema.parse(await resp.json());
lazy_data = { lazy_data = {
fields: body.fields, fields: body.fields,
@ -638,11 +634,6 @@
<FieldAdder {columns}></FieldAdder> <FieldAdder {columns}></FieldAdder>
</div> </div>
</div> </div>
{#if subfilter && subfilter !== "null"}
<div class="subfilter-indicator">
<a href="?">&hellip;</a>
</div>
{/if}
<div class="table-viewer__main"> <div class="table-viewer__main">
{@render table_region({ {@render table_region({
region: "main", region: "main",
@ -663,44 +654,46 @@
}, },
})} })}
</div> </div>
<form method="post" action="insert" class="table-viewer__inserter"> <form method="post" action="insert">
<h3 class="table-viewer__inserter-help"> <div class="table-viewer__inserter">
Insert rows &mdash; press "shift + enter" to jump here or add a row <h3 class="table-viewer__inserter-help">
</h3> Insert rows &mdash; press "shift + enter" to jump here or add a row
<div class="table-viewer__inserter-main"> </h3>
<div class="table-viewer__inserter-rows"> <div class="table-viewer__inserter-main">
{@render table_region({ <div class="table-viewer__inserter-rows">
region: "inserter", {@render table_region({
rows: inserter_rows, region: "inserter",
on_cell_click: (ev: MouseEvent, coords: Coords) => { rows: inserter_rows,
if (ev.metaKey || ev.ctrlKey) { on_cell_click: (ev: MouseEvent, coords: Coords) => {
set_selections([ if (ev.metaKey || ev.ctrlKey) {
coords, set_selections([
...selections coords,
.filter((sel) => !coords_eq(sel.coords, coords)) ...selections
.map((sel) => sel.coords), .filter((sel) => !coords_eq(sel.coords, coords))
]); .map((sel) => sel.coords),
} else if (ev.shiftKey) { ]);
move_cursor(coords, { additive: true }); } else if (ev.shiftKey) {
} else { move_cursor(coords, { additive: true });
move_cursor(coords); } else {
} move_cursor(coords);
}, }
})} },
})}
</div>
<button
aria-label="Insert rows"
class="table-viewer__inserter-submit"
onkeydown={(ev) => {
// Prevent keypress (e.g. pressing Enter on the button to submit
// it) from triggering a table interaction.
ev.stopPropagation();
}}
title="Insert rows"
type="submit"
>
<i class="ti ti-upload"></i>
</button>
</div> </div>
<button
aria-label="Insert rows"
class="table-viewer__inserter-submit"
onkeydown={(ev) => {
// Prevent keypress (e.g. pressing Enter on the button to submit
// it) from triggering a table interaction.
ev.stopPropagation();
}}
title="Insert rows"
type="submit"
>
<i class="ti ti-upload"></i>
</button>
</div> </div>
{#each inserter_rows as row} {#each inserter_rows as row}
{#each lazy_data.fields as field, field_index} {#each lazy_data.fields as field, field_index}