Compare commits
2 commits
81f9396490
...
fc8e3d6b99
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc8e3d6b99 | ||
|
|
610b902ac1 |
6 changed files with 217 additions and 13 deletions
|
|
@ -17,6 +17,7 @@ mod set_filter_handler;
|
||||||
mod settings_handler;
|
mod settings_handler;
|
||||||
mod update_field_handler;
|
mod update_field_handler;
|
||||||
mod update_field_ordinality_handler;
|
mod update_field_ordinality_handler;
|
||||||
|
mod update_field_table_width_px_handler;
|
||||||
mod update_portal_name_handler;
|
mod update_portal_name_handler;
|
||||||
mod update_rel_name_handler;
|
mod update_rel_name_handler;
|
||||||
mod update_values_handler;
|
mod update_values_handler;
|
||||||
|
|
@ -49,6 +50,10 @@ pub(super) fn new_router() -> Router<App> {
|
||||||
"/p/{portal_id}/update-field-ordinality",
|
"/p/{portal_id}/update-field-ordinality",
|
||||||
post(update_field_ordinality_handler::post),
|
post(update_field_ordinality_handler::post),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/p/{portal_id}/update-field-table-width-px",
|
||||||
|
post(update_field_table_width_px_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",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
use axum::{debug_handler, extract::Path, response::Response};
|
use axum::{
|
||||||
use phono_models::field::Field;
|
debug_handler,
|
||||||
|
extract::{Path, State},
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
use phono_models::{
|
||||||
|
accessors::{Accessor as _, Actor, portal::PortalAccessor},
|
||||||
|
field::Field,
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::postgres::types::Oid;
|
use sqlx::postgres::types::Oid;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -11,6 +18,7 @@ use crate::{
|
||||||
extractors::ValidatedForm,
|
extractors::ValidatedForm,
|
||||||
navigator::{Navigator, NavigatorPage},
|
navigator::{Navigator, NavigatorPage},
|
||||||
user::CurrentUser,
|
user::CurrentUser,
|
||||||
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -36,8 +44,9 @@ pub(super) struct FormBody {
|
||||||
/// [`PathParams`].
|
/// [`PathParams`].
|
||||||
#[debug_handler(state = crate::app::App)]
|
#[debug_handler(state = crate::app::App)]
|
||||||
pub(super) async fn post(
|
pub(super) async fn post(
|
||||||
|
State(mut pooler): State<WorkspacePooler>,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
Path(PathParams {
|
Path(PathParams {
|
||||||
portal_id,
|
portal_id,
|
||||||
|
|
@ -51,11 +60,23 @@ pub(super) async fn post(
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME CSRF
|
// FIXME CSRF
|
||||||
|
|
||||||
// FIXME ensure workspace corresponds to rel/portal, and that user has
|
let mut workspace_client = pooler
|
||||||
// permission to access/alter both as needed.
|
.acquire_for(workspace_id, RoleAssignment::User(user.id))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let portal = PortalAccessor::new()
|
||||||
|
.id(portal_id)
|
||||||
|
.as_actor(Actor::User(user.id))
|
||||||
|
.verify_workspace_id(workspace_id)
|
||||||
|
.verify_rel_oid(Oid(rel_oid))
|
||||||
|
.verify_rel_ownership()
|
||||||
|
.using_app_db(&mut app_db)
|
||||||
|
.using_workspace_client(&mut workspace_client)
|
||||||
|
.fetch_one()
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Ensure field exists and belongs to portal.
|
// Ensure field exists and belongs to portal.
|
||||||
Field::belonging_to_portal(portal_id)
|
Field::belonging_to_portal(portal.id)
|
||||||
.with_id(field_id)
|
.with_id(field_id)
|
||||||
.fetch_one(&mut app_db)
|
.fetch_one(&mut app_db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -67,11 +88,12 @@ pub(super) async fn post(
|
||||||
.execute(&mut app_db)
|
.execute(&mut app_db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// TODO: Redirect with subfilter query intact.
|
||||||
Ok(navigator
|
Ok(navigator
|
||||||
.portal_page()
|
.portal_page()
|
||||||
.workspace_id(workspace_id)
|
.workspace_id(workspace_id)
|
||||||
.rel_oid(Oid(rel_oid))
|
.rel_oid(Oid(rel_oid))
|
||||||
.portal_id(portal_id)
|
.portal_id(portal.id)
|
||||||
.build()?
|
.build()?
|
||||||
.redirect_to())
|
.redirect_to())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
use axum::{
|
||||||
|
Json, debug_handler,
|
||||||
|
extract::{Path, State},
|
||||||
|
response::{IntoResponse as _, Response},
|
||||||
|
};
|
||||||
|
use phono_models::{
|
||||||
|
accessors::{Accessor as _, Actor, portal::PortalAccessor},
|
||||||
|
field::Field,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use sqlx::postgres::types::Oid;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::AppDbConn,
|
||||||
|
errors::AppError,
|
||||||
|
extractors::ValidatedForm,
|
||||||
|
user::CurrentUser,
|
||||||
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[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 = 160, max = 800))]
|
||||||
|
table_width_px: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTTP POST handler for updating the display width of a [`Field`] in the table
|
||||||
|
/// view. 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`].
|
||||||
|
///
|
||||||
|
/// Note that unlike [`super::update_field_ordinality_handler::post`], this
|
||||||
|
/// endpoint expects to be called asynchronously and thus returns an HTTP 200
|
||||||
|
/// response instead of a redirect.
|
||||||
|
#[debug_handler(state = crate::app::App)]
|
||||||
|
pub(super) async fn post(
|
||||||
|
State(mut pooler): State<WorkspacePooler>,
|
||||||
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
|
CurrentUser(user): CurrentUser,
|
||||||
|
Path(PathParams {
|
||||||
|
portal_id,
|
||||||
|
rel_oid,
|
||||||
|
workspace_id,
|
||||||
|
}): Path<PathParams>,
|
||||||
|
ValidatedForm(FormBody {
|
||||||
|
field_id,
|
||||||
|
table_width_px,
|
||||||
|
}): ValidatedForm<FormBody>,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
// FIXME CSRF
|
||||||
|
|
||||||
|
let mut workspace_client = pooler
|
||||||
|
.acquire_for(workspace_id, RoleAssignment::User(user.id))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let portal = PortalAccessor::new()
|
||||||
|
.id(portal_id)
|
||||||
|
.as_actor(Actor::User(user.id))
|
||||||
|
.verify_workspace_id(workspace_id)
|
||||||
|
.verify_rel_oid(Oid(rel_oid))
|
||||||
|
.verify_rel_ownership()
|
||||||
|
.using_app_db(&mut app_db)
|
||||||
|
.using_workspace_client(&mut workspace_client)
|
||||||
|
.fetch_one()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
.table_width_px(table_width_px.into())
|
||||||
|
.build()?
|
||||||
|
.execute(&mut app_db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(json!({ "ok": true })).into_response())
|
||||||
|
}
|
||||||
|
|
@ -57,16 +57,27 @@
|
||||||
.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-bottom: solid 1px var(--table-header-border-color);
|
||||||
border-top: none;
|
cursor: grab;
|
||||||
border-left: none;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: none;
|
flex: none;
|
||||||
font-family: var(--default-font-family--data);
|
font-family: var(--default-font-family--data);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.25rem;
|
padding: 4px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
width: calc(var(--column-width) - 8px);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-left: 12px;
|
||||||
|
width: calc(var(--column-width) - 4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-adder__header-lookalike {
|
||||||
|
border-right: solid 1px var(--table-header-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-header .basic-dropdown__button {
|
.field-header .basic-dropdown__button {
|
||||||
|
|
@ -78,7 +89,7 @@
|
||||||
|
|
||||||
.field-header__label {
|
.field-header__label {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 4px;
|
padding: 4px 0;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +115,25 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-resize-handle {
|
||||||
|
background: #0001;
|
||||||
|
border-bottom: solid 1px var(--table-header-border-color);
|
||||||
|
cursor: col-resize;
|
||||||
|
height: 100%;
|
||||||
|
width: 8px;
|
||||||
|
|
||||||
|
.field-resize-handle__v-border {
|
||||||
|
border-left: solid 1px var(--table-header-border-color);
|
||||||
|
height: 100%;
|
||||||
|
margin-left: 3px;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-last-child(1 of .field-resize-handle) {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ======== Component: Field Adder ======== */
|
/* ======== Component: Field Adder ======== */
|
||||||
|
|
||||||
.field-adder {
|
.field-adder {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
import { type FieldInfo } from "./field.svelte";
|
import { type FieldInfo } from "./field.svelte";
|
||||||
import FieldDetails from "./field-details.svelte";
|
import FieldDetails from "./field-details.svelte";
|
||||||
|
|
||||||
|
const MAX_COL_WIDTH_PX = 800;
|
||||||
|
const MIN_COL_WIDTH_PX = 160;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
field: FieldInfo;
|
field: FieldInfo;
|
||||||
index: number;
|
index: number;
|
||||||
|
|
@ -11,6 +14,7 @@
|
||||||
ondragover?(ev: DragEvent): unknown;
|
ondragover?(ev: DragEvent): unknown;
|
||||||
ondragstart?(ev: DragEvent): unknown;
|
ondragstart?(ev: DragEvent): unknown;
|
||||||
ondrop?(ev: DragEvent): unknown;
|
ondrop?(ev: DragEvent): unknown;
|
||||||
|
onresize?(width_px: number): unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -21,6 +25,7 @@
|
||||||
ondragover,
|
ondragover,
|
||||||
ondragstart,
|
ondragstart,
|
||||||
ondrop,
|
ondrop,
|
||||||
|
onresize,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const original_label_value = field.field.table_label;
|
const original_label_value = field.field.table_label;
|
||||||
|
|
@ -30,6 +35,7 @@
|
||||||
let field_config_dialog_element = $state<HTMLDialogElement | undefined>();
|
let field_config_dialog_element = $state<HTMLDialogElement | undefined>();
|
||||||
let name_value = $state(field.field.name);
|
let name_value = $state(field.field.name);
|
||||||
let label_value = $state(field.field.table_label ?? "");
|
let label_value = $state(field.field.table_label ?? "");
|
||||||
|
let resize_drag_x_start = $state<number | undefined>();
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
field.field.table_label = label_value === "" ? undefined : label_value;
|
field.field.table_label = label_value === "" ? undefined : label_value;
|
||||||
|
|
@ -46,7 +52,7 @@
|
||||||
{ondragstart}
|
{ondragstart}
|
||||||
{ondrop}
|
{ondrop}
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
style:width={`${field.field.table_width_px}px`}
|
style:--column-width={`${field.field.table_width_px}px`}
|
||||||
tabindex={0}
|
tabindex={0}
|
||||||
>
|
>
|
||||||
<div class="field-header__label">
|
<div class="field-header__label">
|
||||||
|
|
@ -100,6 +106,33 @@
|
||||||
</menu>
|
</menu>
|
||||||
</BasicDropdown>
|
</BasicDropdown>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="field-resize-handle"
|
||||||
|
draggable={true}
|
||||||
|
ondragstart={(ev) => {
|
||||||
|
resize_drag_x_start = ev.layerX;
|
||||||
|
}}
|
||||||
|
ondragend={(ev) => {
|
||||||
|
if (resize_drag_x_start !== undefined) {
|
||||||
|
onresize?.(
|
||||||
|
Math.min(
|
||||||
|
MAX_COL_WIDTH_PX,
|
||||||
|
Math.max(
|
||||||
|
MIN_COL_WIDTH_PX,
|
||||||
|
field.field.table_width_px + ev.layerX - resize_drag_x_start,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
resize_drag_x_start = undefined;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="separator"
|
||||||
|
aria-valuenow={field.field.table_width_px}
|
||||||
|
aria-valuemin={MIN_COL_WIDTH_PX}
|
||||||
|
aria-valuemax={MAX_COL_WIDTH_PX}
|
||||||
|
>
|
||||||
|
<div class="field-resize-handle__v-border"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<dialog bind:this={remove_field_dialog_element} class="dialog">
|
<dialog bind:this={remove_field_dialog_element} class="dialog">
|
||||||
<form method="post" action="remove-field">
|
<form method="post" action="remove-field">
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,22 @@
|
||||||
form.submit();
|
form.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update_field_table_width_px(
|
||||||
|
field_index: number,
|
||||||
|
new_width_px: number,
|
||||||
|
) {
|
||||||
|
if (lazy_data?.fields[field_index]) {
|
||||||
|
lazy_data.fields[field_index].field.table_width_px = new_width_px;
|
||||||
|
fetch("update-field-table-width-px", {
|
||||||
|
method: "post",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: `field_id=${encodeURIComponent(lazy_data.fields[field_index].field.id)}&table_width_px=${new_width_px}`,
|
||||||
|
}).catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------- Updates and Effects -------- //
|
// -------- Updates and Effects -------- //
|
||||||
|
|
||||||
function set_selections(arr: Coords[]) {
|
function set_selections(arr: Coords[]) {
|
||||||
|
|
@ -635,6 +651,9 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onresize={(new_width_px) => {
|
||||||
|
update_field_table_width_px(field_index, new_width_px);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="table-viewer__header-actions">
|
<div class="table-viewer__header-actions">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue