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,
|
||||
presentation jsonb not null,
|
||||
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 --
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ pub struct Field {
|
|||
|
||||
/// Width of UI table column in pixels.
|
||||
pub table_width_px: i32,
|
||||
|
||||
/// Position of the field relative to others. Smaller values appear earlier.
|
||||
pub ordinality: f64,
|
||||
}
|
||||
|
||||
impl Field {
|
||||
|
|
@ -57,6 +60,7 @@ impl Field {
|
|||
table_label: None,
|
||||
presentation: sqlx::types::Json(presentation),
|
||||
table_width_px: 200,
|
||||
ordinality: 1.0,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -106,9 +110,11 @@ select
|
|||
name,
|
||||
table_label,
|
||||
presentation as "presentation: Json<Presentation>",
|
||||
table_width_px
|
||||
table_width_px,
|
||||
ordinality
|
||||
from fields
|
||||
where portal_id = $1
|
||||
order by ordinality
|
||||
"#,
|
||||
self.portal_id
|
||||
)
|
||||
|
|
@ -133,7 +139,8 @@ select
|
|||
name,
|
||||
table_label,
|
||||
presentation as "presentation: Json<Presentation>",
|
||||
table_width_px
|
||||
table_width_px,
|
||||
ordinality
|
||||
from fields
|
||||
where portal_id = $1 and id = $2
|
||||
"#,
|
||||
|
|
@ -162,14 +169,31 @@ impl InsertableField {
|
|||
Field,
|
||||
r#"
|
||||
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
|
||||
id,
|
||||
name,
|
||||
table_label,
|
||||
presentation as "presentation: Json<Presentation>",
|
||||
table_width_px
|
||||
table_width_px,
|
||||
ordinality
|
||||
"#,
|
||||
self.portal_id,
|
||||
self.name,
|
||||
|
|
@ -201,6 +225,8 @@ pub struct Update {
|
|||
presentation: Option<Presentation>,
|
||||
#[builder(default, setter(strip_option))]
|
||||
table_width_px: Option<i32>,
|
||||
#[builder(default, setter(strip_option))]
|
||||
ordinality: Option<f64>,
|
||||
}
|
||||
|
||||
impl Update {
|
||||
|
|
@ -235,6 +261,15 @@ impl Update {
|
|||
.execute(&mut *tx)
|
||||
.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?;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ mod set_filter_handler;
|
|||
mod settings_handler;
|
||||
mod settings_invite_handler;
|
||||
mod update_field_handler;
|
||||
mod update_field_ordinality_handler;
|
||||
mod update_form_transitions_handler;
|
||||
mod update_portal_name_handler;
|
||||
mod update_prompts_handler;
|
||||
|
|
@ -44,6 +45,10 @@ pub(super) fn new_router() -> Router<App> {
|
|||
"/p/{portal_id}/update-field",
|
||||
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}/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 = {
|
||||
field: FieldInfo;
|
||||
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;
|
||||
|
||||
|
|
@ -24,8 +37,15 @@
|
|||
<div
|
||||
aria-colindex={index}
|
||||
class="field-header__container"
|
||||
draggable={true}
|
||||
{ondragenter}
|
||||
{ondragleave}
|
||||
{ondragover}
|
||||
{ondragstart}
|
||||
{ondrop}
|
||||
role="columnheader"
|
||||
style:width={`${field.field.table_width_px}px`}
|
||||
tabindex={0}
|
||||
>
|
||||
<div class="field-header__label">
|
||||
{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),
|
||||
presentation: presentation_schema,
|
||||
table_width_px: z.number(),
|
||||
ordinality: z.number(),
|
||||
});
|
||||
|
||||
export type Field = z.infer<typeof field_schema>;
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@
|
|||
let focus_cursor = $state<(() => unknown) | undefined>();
|
||||
let inserter_rows = $state<Row[]>([]);
|
||||
let lazy_data = $state<LazyData | undefined>();
|
||||
let dragged_header = $state<number | undefined>();
|
||||
|
||||
// -------- 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 -------- //
|
||||
|
||||
function set_selections(arr: Omit<Selection, "original_value">[]) {
|
||||
|
|
@ -450,6 +507,22 @@
|
|||
<FieldHeader
|
||||
bind:field={lazy_data.fields[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}
|
||||
<div class="lens-table__header-actions">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue