reordering fields
This commit is contained in:
parent
55c58158cc
commit
d934d4ad90
7 changed files with 219 additions and 7 deletions
|
|
@ -82,7 +82,8 @@ create table if not exists fields (
|
||||||
name text not null,
|
name text not null,
|
||||||
presentation jsonb not null,
|
presentation jsonb not null,
|
||||||
table_label text,
|
table_label text,
|
||||||
table_width_px int not null default 200
|
table_width_px int not null default 200,
|
||||||
|
ordinality float not null
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Service Credentials --
|
-- Service Credentials --
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ pub struct Field {
|
||||||
|
|
||||||
/// Width of UI table column in pixels.
|
/// Width of UI table column in pixels.
|
||||||
pub table_width_px: i32,
|
pub table_width_px: i32,
|
||||||
|
|
||||||
|
/// Position of the field relative to others. Smaller values appear earlier.
|
||||||
|
pub ordinality: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Field {
|
impl Field {
|
||||||
|
|
@ -57,6 +60,7 @@ impl Field {
|
||||||
table_label: None,
|
table_label: None,
|
||||||
presentation: sqlx::types::Json(presentation),
|
presentation: sqlx::types::Json(presentation),
|
||||||
table_width_px: 200,
|
table_width_px: 200,
|
||||||
|
ordinality: 1.0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,9 +110,11 @@ select
|
||||||
name,
|
name,
|
||||||
table_label,
|
table_label,
|
||||||
presentation as "presentation: Json<Presentation>",
|
presentation as "presentation: Json<Presentation>",
|
||||||
table_width_px
|
table_width_px,
|
||||||
|
ordinality
|
||||||
from fields
|
from fields
|
||||||
where portal_id = $1
|
where portal_id = $1
|
||||||
|
order by ordinality
|
||||||
"#,
|
"#,
|
||||||
self.portal_id
|
self.portal_id
|
||||||
)
|
)
|
||||||
|
|
@ -133,7 +139,8 @@ select
|
||||||
name,
|
name,
|
||||||
table_label,
|
table_label,
|
||||||
presentation as "presentation: Json<Presentation>",
|
presentation as "presentation: Json<Presentation>",
|
||||||
table_width_px
|
table_width_px,
|
||||||
|
ordinality
|
||||||
from fields
|
from fields
|
||||||
where portal_id = $1 and id = $2
|
where portal_id = $1 and id = $2
|
||||||
"#,
|
"#,
|
||||||
|
|
@ -162,14 +169,31 @@ impl InsertableField {
|
||||||
Field,
|
Field,
|
||||||
r#"
|
r#"
|
||||||
insert into fields
|
insert into fields
|
||||||
(portal_id, name, table_label, presentation, table_width_px)
|
(
|
||||||
values ($1, $2, $3, $4, $5)
|
portal_id,
|
||||||
|
name,
|
||||||
|
table_label,
|
||||||
|
presentation,
|
||||||
|
table_width_px,
|
||||||
|
ordinality
|
||||||
|
) (
|
||||||
|
select
|
||||||
|
$1 as portal_id,
|
||||||
|
$2 as name,
|
||||||
|
$3 as table_label,
|
||||||
|
$4 as presentation,
|
||||||
|
$5 as table_width_px,
|
||||||
|
coalesce(max(prev.ordinality), 0) + 1 as ordinality
|
||||||
|
from fields as prev
|
||||||
|
where prev.portal_id = $1
|
||||||
|
)
|
||||||
returning
|
returning
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
table_label,
|
table_label,
|
||||||
presentation as "presentation: Json<Presentation>",
|
presentation as "presentation: Json<Presentation>",
|
||||||
table_width_px
|
table_width_px,
|
||||||
|
ordinality
|
||||||
"#,
|
"#,
|
||||||
self.portal_id,
|
self.portal_id,
|
||||||
self.name,
|
self.name,
|
||||||
|
|
@ -201,6 +225,8 @@ pub struct Update {
|
||||||
presentation: Option<Presentation>,
|
presentation: Option<Presentation>,
|
||||||
#[builder(default, setter(strip_option))]
|
#[builder(default, setter(strip_option))]
|
||||||
table_width_px: Option<i32>,
|
table_width_px: Option<i32>,
|
||||||
|
#[builder(default, setter(strip_option))]
|
||||||
|
ordinality: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Update {
|
impl Update {
|
||||||
|
|
@ -235,6 +261,15 @@ impl Update {
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
if let Some(ordinality) = self.ordinality {
|
||||||
|
query!(
|
||||||
|
"update fields set ordinality = $1 where id = $2",
|
||||||
|
ordinality,
|
||||||
|
self.id
|
||||||
|
)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ mod set_filter_handler;
|
||||||
mod settings_handler;
|
mod settings_handler;
|
||||||
mod settings_invite_handler;
|
mod settings_invite_handler;
|
||||||
mod update_field_handler;
|
mod update_field_handler;
|
||||||
|
mod update_field_ordinality_handler;
|
||||||
mod update_form_transitions_handler;
|
mod update_form_transitions_handler;
|
||||||
mod update_portal_name_handler;
|
mod update_portal_name_handler;
|
||||||
mod update_prompts_handler;
|
mod update_prompts_handler;
|
||||||
|
|
@ -44,6 +45,10 @@ pub(super) fn new_router() -> Router<App> {
|
||||||
"/p/{portal_id}/update-field",
|
"/p/{portal_id}/update-field",
|
||||||
post(update_field_handler::post),
|
post(update_field_handler::post),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/p/{portal_id}/update-field-ordinality",
|
||||||
|
post(update_field_ordinality_handler::post),
|
||||||
|
)
|
||||||
.route("/p/{portal_id}/insert", post(insert_handler::post))
|
.route("/p/{portal_id}/insert", post(insert_handler::post))
|
||||||
.route(
|
.route(
|
||||||
"/p/{portal_id}/update-values",
|
"/p/{portal_id}/update-values",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
use axum::{debug_handler, extract::Path, response::Response};
|
||||||
|
use interim_models::field::Field;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::postgres::types::Oid;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{App, AppDbConn},
|
||||||
|
errors::AppError,
|
||||||
|
extractors::ValidatedForm,
|
||||||
|
navigator::{Navigator, NavigatorPage},
|
||||||
|
user::CurrentUser,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(super) struct PathParams {
|
||||||
|
portal_id: Uuid,
|
||||||
|
rel_oid: u32,
|
||||||
|
workspace_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Validate)]
|
||||||
|
pub(super) struct FormBody {
|
||||||
|
field_id: Uuid,
|
||||||
|
#[validate(range(min = 0.0))]
|
||||||
|
ordinality: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTTP POST handler for updating the ordinal position of an existing
|
||||||
|
/// [`Field`]. This is distinct from `./update-field`, because it is initiated
|
||||||
|
/// in the UI by a "drag" interaction. It may make sense to fold this into the
|
||||||
|
/// aforementioned endpoint later, if it is modified to handle partial updates.
|
||||||
|
///
|
||||||
|
/// This handler expects 3 path parameters with the structure described by
|
||||||
|
/// [`PathParams`].
|
||||||
|
#[debug_handler(state = App)]
|
||||||
|
pub(super) async fn post(
|
||||||
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
|
CurrentUser(_user): CurrentUser,
|
||||||
|
navigator: Navigator,
|
||||||
|
Path(PathParams {
|
||||||
|
portal_id,
|
||||||
|
rel_oid,
|
||||||
|
workspace_id,
|
||||||
|
}): Path<PathParams>,
|
||||||
|
ValidatedForm(FormBody {
|
||||||
|
field_id,
|
||||||
|
ordinality,
|
||||||
|
}): ValidatedForm<FormBody>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
// FIXME CSRF
|
||||||
|
|
||||||
|
// FIXME ensure workspace corresponds to rel/portal, and that user has
|
||||||
|
// permission to access/alter both as needed.
|
||||||
|
|
||||||
|
// Ensure field exists and belongs to portal.
|
||||||
|
Field::belonging_to_portal(portal_id)
|
||||||
|
.with_id(field_id)
|
||||||
|
.fetch_one(&mut app_db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Field::update()
|
||||||
|
.id(field_id)
|
||||||
|
.ordinality(ordinality)
|
||||||
|
.build()?
|
||||||
|
.execute(&mut app_db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(navigator
|
||||||
|
.portal_page()
|
||||||
|
.workspace_id(workspace_id)
|
||||||
|
.rel_oid(Oid(rel_oid))
|
||||||
|
.portal_id(portal_id)
|
||||||
|
.build()?
|
||||||
|
.redirect_to())
|
||||||
|
}
|
||||||
|
|
@ -6,9 +6,22 @@
|
||||||
type Props = {
|
type Props = {
|
||||||
field: FieldInfo;
|
field: FieldInfo;
|
||||||
index: number;
|
index: number;
|
||||||
|
ondragenter?(ev: DragEvent): unknown;
|
||||||
|
ondragleave?(ev: DragEvent): unknown;
|
||||||
|
ondragover?(ev: DragEvent): unknown;
|
||||||
|
ondragstart?(ev: DragEvent): unknown;
|
||||||
|
ondrop?(ev: DragEvent): unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { field = $bindable(), index }: Props = $props();
|
let {
|
||||||
|
field = $bindable(),
|
||||||
|
index,
|
||||||
|
ondragenter,
|
||||||
|
ondragleave,
|
||||||
|
ondragover,
|
||||||
|
ondragstart,
|
||||||
|
ondrop,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
const original_label_value = field.field.table_label;
|
const original_label_value = field.field.table_label;
|
||||||
|
|
||||||
|
|
@ -24,8 +37,15 @@
|
||||||
<div
|
<div
|
||||||
aria-colindex={index}
|
aria-colindex={index}
|
||||||
class="field-header__container"
|
class="field-header__container"
|
||||||
|
draggable={true}
|
||||||
|
{ondragenter}
|
||||||
|
{ondragleave}
|
||||||
|
{ondragover}
|
||||||
|
{ondragstart}
|
||||||
|
{ondrop}
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
style:width={`${field.field.table_width_px}px`}
|
style:width={`${field.field.table_width_px}px`}
|
||||||
|
tabindex={0}
|
||||||
>
|
>
|
||||||
<div class="field-header__label">
|
<div class="field-header__label">
|
||||||
{field.field.table_label ?? field.field.name}
|
{field.field.table_label ?? field.field.name}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export const field_schema = z.object({
|
||||||
table_label: z.string().nullish().transform((x) => x ?? undefined),
|
table_label: z.string().nullish().transform((x) => x ?? undefined),
|
||||||
presentation: presentation_schema,
|
presentation: presentation_schema,
|
||||||
table_width_px: z.number(),
|
table_width_px: z.number(),
|
||||||
|
ordinality: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Field = z.infer<typeof field_schema>;
|
export type Field = z.infer<typeof field_schema>;
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@
|
||||||
let focus_cursor = $state<(() => unknown) | undefined>();
|
let focus_cursor = $state<(() => unknown) | undefined>();
|
||||||
let inserter_rows = $state<Row[]>([]);
|
let inserter_rows = $state<Row[]>([]);
|
||||||
let lazy_data = $state<LazyData | undefined>();
|
let lazy_data = $state<LazyData | undefined>();
|
||||||
|
let dragged_header = $state<number | undefined>();
|
||||||
|
|
||||||
// -------- Helper Functions -------- //
|
// -------- Helper Functions -------- //
|
||||||
|
|
||||||
|
|
@ -100,6 +101,62 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update_field_ordinality({
|
||||||
|
field_index,
|
||||||
|
beyond_index,
|
||||||
|
}: {
|
||||||
|
field_index: number;
|
||||||
|
beyond_index: number;
|
||||||
|
}) {
|
||||||
|
if (!lazy_data) {
|
||||||
|
console.warn("preconditions for update_field_ordinality() not met");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let target_ordinality: number | undefined;
|
||||||
|
const ordinality_near = lazy_data.fields[beyond_index].field.ordinality;
|
||||||
|
if (beyond_index > field_index) {
|
||||||
|
// Field is moving towards the end.
|
||||||
|
const ordinality_far =
|
||||||
|
lazy_data.fields[beyond_index + 1]?.field.ordinality;
|
||||||
|
if (ordinality_far) {
|
||||||
|
target_ordinality = (ordinality_near + ordinality_far) / 2;
|
||||||
|
} else {
|
||||||
|
target_ordinality = ordinality_near + 1;
|
||||||
|
}
|
||||||
|
} else if (beyond_index < field_index) {
|
||||||
|
// Field is moving towards the start.
|
||||||
|
const ordinality_far =
|
||||||
|
lazy_data.fields[beyond_index - 1]?.field.ordinality;
|
||||||
|
if (ordinality_far) {
|
||||||
|
target_ordinality = (ordinality_near + ordinality_far) / 2;
|
||||||
|
} else {
|
||||||
|
// Avoid setting ordinality <= 0.
|
||||||
|
target_ordinality = ordinality_near / 2;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No movement.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imperatively submit HTML form.
|
||||||
|
const form = document.createElement("form");
|
||||||
|
form.setAttribute("action", "update-field-ordinality");
|
||||||
|
form.setAttribute("method", "post");
|
||||||
|
form.style.display = "none";
|
||||||
|
const field_id_input = document.createElement("input");
|
||||||
|
field_id_input.type = "hidden";
|
||||||
|
field_id_input.name = "field_id";
|
||||||
|
field_id_input.value = lazy_data.fields[field_index].field.id;
|
||||||
|
const ordinality_input = document.createElement("input");
|
||||||
|
ordinality_input.name = "ordinality";
|
||||||
|
ordinality_input.type = "hidden";
|
||||||
|
ordinality_input.value = `${target_ordinality}`;
|
||||||
|
form.appendChild(field_id_input);
|
||||||
|
form.appendChild(ordinality_input);
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
|
||||||
// -------- Updates and Effects -------- //
|
// -------- Updates and Effects -------- //
|
||||||
|
|
||||||
function set_selections(arr: Omit<Selection, "original_value">[]) {
|
function set_selections(arr: Omit<Selection, "original_value">[]) {
|
||||||
|
|
@ -450,6 +507,22 @@
|
||||||
<FieldHeader
|
<FieldHeader
|
||||||
bind:field={lazy_data.fields[field_index]}
|
bind:field={lazy_data.fields[field_index]}
|
||||||
index={field_index}
|
index={field_index}
|
||||||
|
ondragstart={() => {
|
||||||
|
dragged_header = field_index;
|
||||||
|
}}
|
||||||
|
ondragover={(ev) => {
|
||||||
|
// Enable element as drop target
|
||||||
|
ev.preventDefault();
|
||||||
|
}}
|
||||||
|
ondrop={(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (dragged_header !== undefined) {
|
||||||
|
update_field_ordinality({
|
||||||
|
field_index: dragged_header,
|
||||||
|
beyond_index: field_index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="lens-table__header-actions">
|
<div class="lens-table__header-actions">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue