diff --git a/interim-models/src/field.rs b/interim-models/src/field.rs index 1030538..25f2ded 100644 --- a/interim-models/src/field.rs +++ b/interim-models/src/field.rs @@ -87,6 +87,13 @@ impl Field { }) } + pub async fn delete(&self, app_db: &mut AppDbClient) -> sqlx::Result<()> { + query!("delete from fields where id = $1", self.id) + .execute(app_db.get_conn()) + .await?; + Ok(()) + } + pub fn belonging_to_portal(portal_id: Uuid) -> BelongingToPortalQuery { BelongingToPortalQuery { portal_id } } diff --git a/interim-server/src/routes/relations_single/mod.rs b/interim-server/src/routes/relations_single/mod.rs index 0dbcfff..f87e765 100644 --- a/interim-server/src/routes/relations_single/mod.rs +++ b/interim-server/src/routes/relations_single/mod.rs @@ -13,6 +13,7 @@ mod get_data_handler; mod insert_handler; mod portal_handler; mod portal_settings_handler; +mod remove_field_handler; mod set_filter_handler; mod settings_handler; mod settings_invite_handler; @@ -45,6 +46,10 @@ pub(super) fn new_router() -> Router { "/p/{portal_id}/update-field", post(update_field_handler::post), ) + .route( + "/p/{portal_id}/remove-field", + post(remove_field_handler::post), + ) .route( "/p/{portal_id}/update-field-ordinality", post(update_field_ordinality_handler::post), diff --git a/interim-server/src/routes/relations_single/remove_field_handler.rs b/interim-server/src/routes/relations_single/remove_field_handler.rs new file mode 100644 index 0000000..f2117c1 --- /dev/null +++ b/interim-server/src/routes/relations_single/remove_field_handler.rs @@ -0,0 +1,98 @@ +use axum::{ + debug_handler, + extract::{Path, State}, + response::Response, +}; +use interim_models::{field::Field, portal::Portal}; +use interim_pgtypes::{escape_identifier, pg_class::PgClass}; +use serde::Deserialize; +use sqlx::{postgres::types::Oid, query}; +use uuid::Uuid; +use validator::Validate; + +use crate::{ + app::{App, AppDbConn}, + errors::{AppError, bad_request}, + extractors::ValidatedForm, + navigator::{Navigator, NavigatorPage}, + 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, + + /// Expects "true" for truthy, else falsy. + delete_data: String, +} + +/// HTTP POST handler for removing an existing [`Field`]. +/// +/// 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, + State(mut pooler): State, + CurrentUser(user): CurrentUser, + navigator: Navigator, + Path(PathParams { + portal_id, + rel_oid, + workspace_id, + }): Path, + ValidatedForm(FormBody { + delete_data, + field_id, + }): ValidatedForm, +) -> Result { + // 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. + let field = Field::belonging_to_portal(portal_id) + .with_id(field_id) + .fetch_one(&mut app_db) + .await?; + + if delete_data == "true" && field.name.starts_with('_') { + return Err(bad_request!("cannot delete data for a system column")); + } + + field.delete(&mut app_db).await?; + + if delete_data == "true" { + let mut workspace_client = pooler + .acquire_for(workspace_id, RoleAssignment::User(user.id)) + .await?; + let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; + let rel = PgClass::with_oid(portal.class_oid) + .fetch_one(&mut workspace_client) + .await?; + query(&format!( + "alter table {ident} drop column if exists {col_esc}", + ident = rel.get_identifier(), + col_esc = escape_identifier(&field.name), + )) + .execute(workspace_client.get_conn()) + .await?; + } + + Ok(navigator + .portal_page() + .workspace_id(workspace_id) + .rel_oid(Oid(rel_oid)) + .portal_id(portal_id) + .build()? + .redirect_to()) +} diff --git a/sass/main.scss b/sass/main.scss index da791e1..32b250a 100644 --- a/sass/main.scss +++ b/sass/main.scss @@ -174,62 +174,6 @@ button, input[type="submit"] { } } -// TODO: can this be removed? -.button-menu { - &__toggle-button { - @include globals.button-outline; - align-items: center; - display: flex; - - &-icon { - display: flex; - - svg path { - stroke: currentColor; - } - } - } - - &__popover { - &:popover-open { - @include globals.popover; - width: 16rem; - // FIXME: This makes button border radius work correctly, but also hides - // the outline that appears when each button is focused, particularly - // when there is only one button present. - overflow: hidden; - } - } - - // Palindrome humor! Anyone? No? Okay nvm. - &__unem-nottub { - @include globals.button-clear; - border-radius: 0; - padding: 1rem; - text-align: left; - } -} - -.combobox { - &__popover:popover-open { - @include globals.popover; - padding: 0; - } - - &__completion { - @include globals.reset-button; - display: block; - padding: 0.5rem; - font-weight: normal; - text-align: left; - width: 100%; - - &:hover, &:focus { - background: #0000001f; - } - } -} - .table { border-collapse: collapse; @@ -248,7 +192,11 @@ button, input[type="submit"] { } } -.dialog:popover-open { +.phono-popover:popover-open { + @include globals.popover; +} + +.dialog:popover-open, .dialog:open { @include globals.rounded; background: #fff; diff --git a/sass/viewer.scss b/sass/viewer.scss index 1544bd9..995bef4 100644 --- a/sass/viewer.scss +++ b/sass/viewer.scss @@ -27,9 +27,12 @@ $table-border-color: #ccc; width: 100%; &__headers { + align-items: stretch; display: flex; grid-area: headers; - align-items: stretch; + // Ensure that there will be enough space on the right for popovers to + // render without overflowing the container. + padding-right: 480px; } &__main { @@ -73,7 +76,6 @@ $table-border-color: #ccc; } &--cursor { - background: transparent; outline: 3px solid #37f; outline-offset: -2px; } diff --git a/svelte/src/basic-dropdown.webc.svelte b/svelte/src/basic-dropdown.webc.svelte index 6657939..0c5b2ff 100644 --- a/svelte/src/basic-dropdown.webc.svelte +++ b/svelte/src/basic-dropdown.webc.svelte @@ -1,5 +1,6 @@ + + - -
- -
- {#each options as option} - - {/each} -
-
diff --git a/svelte/src/combobox.svelte b/svelte/src/combobox.svelte index 7c61aee..90b69ad 100644 --- a/svelte/src/combobox.svelte +++ b/svelte/src/combobox.svelte @@ -1,3 +1,8 @@ + +
{ + popover_element?.showPopover(); + }} + onkeydown={(ev) => { + if (ev.key === "Escape") { + popover_element?.hidePopover(); + } + }} type="text" />
{#each completions as completion}
+ + diff --git a/svelte/src/field-adder.svelte b/svelte/src/field-adder.svelte index 1099012..841b2ed 100644 --- a/svelte/src/field-adder.svelte +++ b/svelte/src/field-adder.svelte @@ -1,5 +1,6 @@ -
-
-
- name) - .filter((name) => - name - .toLocaleLowerCase("en-US") - .includes(label_value.toLocaleLowerCase("en-US")), - )} - search_input_class="field-adder__label-input" - /> -
- -
- - -
-
- +
- - {#if presentation_value} - - {/if} - + { + popover_element?.showPopover(); + }} + oninput={() => { + popover_element?.showPopover(); + }} + onkeydown={(ev) => { + if (ev.key === "Escape") { + toggle_expanded(); + } + }} + type="text" + /> + +
+
+ {#each columns + .map(({ name }) => name) + .filter((name) => name + .toLocaleLowerCase("en-US") + .includes(label_value.toLocaleLowerCase("en-US")) || name + .toLocaleLowerCase("en-US") + .includes(name_value.toLocaleLowerCase("en-US"))) as completion} + + {/each} +
+
+ {#if presentation_value} + { + presentation_customized = true; + }} + /> + {/if} + +
+
+
- + +
+ +
+
+ + diff --git a/svelte/src/field-header.svelte b/svelte/src/field-header.svelte index 4133138..179aacb 100644 --- a/svelte/src/field-header.svelte +++ b/svelte/src/field-header.svelte @@ -26,6 +26,8 @@ const original_label_value = field.field.table_label; let type_indicator_element = $state(); + let remove_field_dialog_element = $state(); + let field_config_dialog_element = $state(); let name_value = $state(field.field.name); let label_value = $state(field.field.table_label ?? ""); @@ -53,6 +55,7 @@
{ if (ev.newState === "closed") { @@ -64,7 +67,7 @@ > {#if field.field.presentation.t === "Dropdown"} - + {:else if field.field.presentation.t === "Numeric"} {:else if field.field.presentation.t === "Text"} @@ -75,18 +78,71 @@ {/if} -
-
- - - - -
+ +
  • + +
  • +
  • + +
  • +
    + + +
    +
    + + +
    +
    + + + +
    +
    +
    + + +
    +
    + +
    + +
    + +
    +
    +
    diff --git a/svelte/src/table-viewer.webc.svelte b/svelte/src/table-viewer.webc.svelte index 96cd581..784d2cb 100644 --- a/svelte/src/table-viewer.webc.svelte +++ b/svelte/src/table-viewer.webc.svelte @@ -601,7 +601,7 @@
    {#if lazy_data}
    -
    +
    {#each lazy_data.fields as _, field_index}