fixes
This commit is contained in:
@@ -162,12 +162,15 @@ td-add-columns-title = Přidat sloupce
|
|||||||
td-presentation-title = Zobrazení sloupců
|
td-presentation-title = Zobrazení sloupců
|
||||||
td-add-columns-to = Přidat sloupce do
|
td-add-columns-to = Přidat sloupce do
|
||||||
td-append-hint = Sloupce se přidávají na konec. Nic, co už existuje, se nemění a nové sloupce lze indexovat hned při přidání.
|
td-append-hint = Sloupce se přidávají na konec. Nic, co už existuje, se nemění a nové sloupce lze indexovat hned při přidání.
|
||||||
td-rename-column = Aliasy a pořadí sloupců
|
td-rename-column = Aliasy sloupců
|
||||||
td-rename-hint = Přejmenovává se jen zobrazený název, ne fyzický sloupec pod ním, takže uložená data a skripty zůstávají nedotčené.
|
td-rename-hint = Přejmenovává se jen zobrazený název, ne fyzický sloupec pod ním, takže uložená data a skripty zůstávají nedotčené.
|
||||||
td-column-label = Sloupec
|
td-column-label = Sloupec
|
||||||
td-choose-column = Vyberte sloupec
|
td-choose-column = Vyberte sloupec
|
||||||
td-new-name = Nový název
|
td-new-name = Nový název
|
||||||
td-rename-button = Uložit zobrazení
|
td-rename-button = Přejmenovat
|
||||||
|
td-alias-locked = nelze přejmenovat
|
||||||
|
td-order-heading = Pořadí sloupců
|
||||||
|
td-order-hint = Posune sloupec za souseda. Formulář pořadí neposílá názvy, takže nemůže nic přejmenovat.
|
||||||
td-columns-now = Současné sloupce
|
td-columns-now = Současné sloupce
|
||||||
td-col-column = Sloupec
|
td-col-column = Sloupec
|
||||||
td-col-type = Typ
|
td-col-type = Typ
|
||||||
@@ -313,7 +316,8 @@ td-err-profile-gone = Profil `{ $missing }` už neexistuje.
|
|||||||
td-err-not-a-table = `{ $missing }` není tabulka rozsahu { $scope }.
|
td-err-not-a-table = `{ $missing }` není tabulka rozsahu { $scope }.
|
||||||
td-err-describe-column = Před přidáním popište alespoň jeden sloupec.
|
td-err-describe-column = Před přidáním popište alespoň jeden sloupec.
|
||||||
td-err-backend-no-columns = Backend nepřidal sloupce.
|
td-err-backend-no-columns = Backend nepřidal sloupce.
|
||||||
td-err-choose-rename = Vyberte sloupec a zadejte jeho nový název.
|
td-err-choose-rename = Zadejte nový název sloupce.
|
||||||
|
td-err-unknown-column = Tento sloupec už do této tabulky nepatří. Panel níže ukazuje tabulku, jaká je teď.
|
||||||
td-err-backend-no-rename = Backend nepřejmenoval sloupec.
|
td-err-backend-no-rename = Backend nepřejmenoval sloupec.
|
||||||
td-err-type-name = Pro potvrzení smazání napište přesný název tabulky.
|
td-err-type-name = Pro potvrzení smazání napište přesný název tabulky.
|
||||||
td-err-backend-no-delete = Backend nesmazal tabulku.
|
td-err-backend-no-delete = Backend nesmazal tabulku.
|
||||||
@@ -609,6 +613,8 @@ export-profile = Profil
|
|||||||
export-choose-profile = Vyberte profil
|
export-choose-profile = Vyberte profil
|
||||||
export-tables = Tabulky
|
export-tables = Tabulky
|
||||||
export-comma-hint = Více tabulek oddělte čárkami.
|
export-comma-hint = Více tabulek oddělte čárkami.
|
||||||
|
export-include-system = Zahrnout systémové sloupce
|
||||||
|
export-include-system-hint = Do souboru přidá id, deleted a row_revision. Import je ignoruje.
|
||||||
export-error-title = CSV se nepodařilo exportovat
|
export-error-title = CSV se nepodařilo exportovat
|
||||||
export-download = Stáhnout CSV
|
export-download = Stáhnout CSV
|
||||||
export-err-select-profile = Vyberte profil.
|
export-err-select-profile = Vyberte profil.
|
||||||
|
|||||||
@@ -164,12 +164,15 @@ td-add-columns-title = Add columns
|
|||||||
td-presentation-title = Column presentation
|
td-presentation-title = Column presentation
|
||||||
td-add-columns-to = Add columns to
|
td-add-columns-to = Add columns to
|
||||||
td-append-hint = Columns are appended. Nothing that already exists is changed, and the new columns can be indexed as they are added.
|
td-append-hint = Columns are appended. Nothing that already exists is changed, and the new columns can be indexed as they are added.
|
||||||
td-rename-column = Column aliases and order
|
td-rename-column = Column aliases
|
||||||
td-rename-hint = Renames what the column is called, not the physical column underneath, so stored data and scripts are untouched.
|
td-rename-hint = Renames what the column is called, not the physical column underneath, so stored data and scripts are untouched.
|
||||||
td-column-label = Column
|
td-column-label = Column
|
||||||
td-choose-column = Choose a column
|
td-choose-column = Choose a column
|
||||||
td-new-name = New name
|
td-new-name = New name
|
||||||
td-rename-button = Save presentation
|
td-rename-button = Rename
|
||||||
|
td-alias-locked = cannot be renamed
|
||||||
|
td-order-heading = Column order
|
||||||
|
td-order-hint = Moves a column past its neighbour. The order form carries no names, so it cannot rename anything.
|
||||||
td-columns-now = Columns it has now
|
td-columns-now = Columns it has now
|
||||||
td-col-column = Column
|
td-col-column = Column
|
||||||
td-col-type = Type
|
td-col-type = Type
|
||||||
@@ -309,7 +312,8 @@ td-err-profile-gone = Profile `{ $missing }` no longer exists.
|
|||||||
td-err-not-a-table = `{ $missing }` is not a table of { $scope }.
|
td-err-not-a-table = `{ $missing }` is not a table of { $scope }.
|
||||||
td-err-describe-column = Describe at least one column before adding.
|
td-err-describe-column = Describe at least one column before adding.
|
||||||
td-err-backend-no-columns = The backend did not add the columns.
|
td-err-backend-no-columns = The backend did not add the columns.
|
||||||
td-err-choose-rename = Choose a column and type its new name.
|
td-err-choose-rename = Type a new name for the column.
|
||||||
|
td-err-unknown-column = That column is not part of this table any more. The panel below is the table as it now is.
|
||||||
td-err-backend-no-rename = The backend did not rename the column.
|
td-err-backend-no-rename = The backend did not rename the column.
|
||||||
td-err-type-name = Type the table's name exactly to confirm the deletion.
|
td-err-type-name = Type the table's name exactly to confirm the deletion.
|
||||||
td-err-backend-no-delete = The backend did not delete the table.
|
td-err-backend-no-delete = The backend did not delete the table.
|
||||||
@@ -596,6 +600,8 @@ export-profile = Profile
|
|||||||
export-choose-profile = Choose a profile
|
export-choose-profile = Choose a profile
|
||||||
export-tables = Tables
|
export-tables = Tables
|
||||||
export-comma-hint = Separate multiple tables with commas.
|
export-comma-hint = Separate multiple tables with commas.
|
||||||
|
export-include-system = Include system columns
|
||||||
|
export-include-system-hint = Adds id, deleted and row_revision to the file. An import ignores them.
|
||||||
export-error-title = Could not export CSV
|
export-error-title = Could not export CSV
|
||||||
export-download = Download CSV
|
export-download = Download CSV
|
||||||
export-err-select-profile = Select a profile.
|
export-err-select-profile = Select a profile.
|
||||||
|
|||||||
@@ -162,12 +162,15 @@ td-add-columns-title = Pridať stĺpce
|
|||||||
td-presentation-title = Zobrazenie stĺpcov
|
td-presentation-title = Zobrazenie stĺpcov
|
||||||
td-add-columns-to = Pridať stĺpce do
|
td-add-columns-to = Pridať stĺpce do
|
||||||
td-append-hint = Stĺpce sa pridávajú na koniec. Nič, čo už existuje, sa nemení a nové stĺpce možno indexovať hneď pri pridaní.
|
td-append-hint = Stĺpce sa pridávajú na koniec. Nič, čo už existuje, sa nemení a nové stĺpce možno indexovať hneď pri pridaní.
|
||||||
td-rename-column = Aliasy a poradie stĺpcov
|
td-rename-column = Aliasy stĺpcov
|
||||||
td-rename-hint = Premenúva sa len zobrazený názov, nie fyzický stĺpec pod ním, takže uložené údaje a skripty zostávajú nedotknuté.
|
td-rename-hint = Premenúva sa len zobrazený názov, nie fyzický stĺpec pod ním, takže uložené údaje a skripty zostávajú nedotknuté.
|
||||||
td-column-label = Stĺpec
|
td-column-label = Stĺpec
|
||||||
td-choose-column = Vyberte stĺpec
|
td-choose-column = Vyberte stĺpec
|
||||||
td-new-name = Nový názov
|
td-new-name = Nový názov
|
||||||
td-rename-button = Uložiť zobrazenie
|
td-rename-button = Premenovať
|
||||||
|
td-alias-locked = nedá sa premenovať
|
||||||
|
td-order-heading = Poradie stĺpcov
|
||||||
|
td-order-hint = Posunie stĺpec za suseda. Formulár poradia neposiela názvy, takže nemôže nič premenovať.
|
||||||
td-columns-now = Súčasné stĺpce
|
td-columns-now = Súčasné stĺpce
|
||||||
td-col-column = Stĺpec
|
td-col-column = Stĺpec
|
||||||
td-col-type = Typ
|
td-col-type = Typ
|
||||||
@@ -313,7 +316,8 @@ td-err-profile-gone = Profil `{ $missing }` už neexistuje.
|
|||||||
td-err-not-a-table = `{ $missing }` nie je tabuľka rozsahu { $scope }.
|
td-err-not-a-table = `{ $missing }` nie je tabuľka rozsahu { $scope }.
|
||||||
td-err-describe-column = Pred pridaním popíšte aspoň jeden stĺpec.
|
td-err-describe-column = Pred pridaním popíšte aspoň jeden stĺpec.
|
||||||
td-err-backend-no-columns = Backend nepridal stĺpce.
|
td-err-backend-no-columns = Backend nepridal stĺpce.
|
||||||
td-err-choose-rename = Vyberte stĺpec a zadajte jeho nový názov.
|
td-err-choose-rename = Zadajte nový názov stĺpca.
|
||||||
|
td-err-unknown-column = Tento stĺpec už do tejto tabuľky nepatrí. Panel nižšie ukazuje tabuľku, aká je teraz.
|
||||||
td-err-backend-no-rename = Backend nepremenoval stĺpec.
|
td-err-backend-no-rename = Backend nepremenoval stĺpec.
|
||||||
td-err-type-name = Na potvrdenie vymazania napíšte presný názov tabuľky.
|
td-err-type-name = Na potvrdenie vymazania napíšte presný názov tabuľky.
|
||||||
td-err-backend-no-delete = Backend nevymazal tabuľku.
|
td-err-backend-no-delete = Backend nevymazal tabuľku.
|
||||||
@@ -609,6 +613,8 @@ export-profile = Profil
|
|||||||
export-choose-profile = Vyberte profil
|
export-choose-profile = Vyberte profil
|
||||||
export-tables = Tabuľky
|
export-tables = Tabuľky
|
||||||
export-comma-hint = Viacero tabuliek oddeľte čiarkami.
|
export-comma-hint = Viacero tabuliek oddeľte čiarkami.
|
||||||
|
export-include-system = Zahrnúť systémové stĺpce
|
||||||
|
export-include-system-hint = Do súboru pridá id, deleted a row_revision. Import ich ignoruje.
|
||||||
export-error-title = CSV sa nepodarilo exportovať
|
export-error-title = CSV sa nepodarilo exportovať
|
||||||
export-download = Stiahnuť CSV
|
export-download = Stiahnuť CSV
|
||||||
export-err-select-profile = Vyberte profil.
|
export-err-select-profile = Vyberte profil.
|
||||||
|
|||||||
@@ -324,7 +324,8 @@ mod tests {
|
|||||||
for path in [
|
for path in [
|
||||||
"/admin/tables/columns/add",
|
"/admin/tables/columns/add",
|
||||||
"/admin/tables/columns/add/builder",
|
"/admin/tables/columns/add/builder",
|
||||||
"/admin/tables/presentation",
|
"/admin/tables/presentation/alias",
|
||||||
|
"/admin/tables/presentation/order",
|
||||||
"/admin/tables/delete",
|
"/admin/tables/delete",
|
||||||
"/admin/profiles/copy",
|
"/admin/profiles/copy",
|
||||||
"/admin/tables/from-template",
|
"/admin/tables/from-template",
|
||||||
@@ -452,7 +453,14 @@ mod tests {
|
|||||||
),
|
),
|
||||||
("/admin/tables/delete", "profile=billing&table=invoice"),
|
("/admin/tables/delete", "profile=billing&table=invoice"),
|
||||||
("/admin/profiles/copy", "profile=billing&table=invoice"),
|
("/admin/profiles/copy", "profile=billing&table=invoice"),
|
||||||
("/admin/tables/presentation", "profile=billing&table=invoice"),
|
(
|
||||||
|
"/admin/tables/presentation/alias",
|
||||||
|
"profile=billing&table=invoice&column_id=1&alias=amount",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/admin/tables/presentation/order",
|
||||||
|
"profile=billing&table=invoice&column_id=1&direction=up",
|
||||||
|
),
|
||||||
("/admin/tables/columns/add", "profile=billing&table=invoice"),
|
("/admin/tables/columns/add", "profile=billing&table=invoice"),
|
||||||
("/admin/tables/builder", ""),
|
("/admin/tables/builder", ""),
|
||||||
("/admin/tables", ""),
|
("/admin/tables", ""),
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ use crate::{
|
|||||||
use super::{
|
use super::{
|
||||||
loader::{self, load_page},
|
loader::{self, load_page},
|
||||||
state::{
|
state::{
|
||||||
CopyForm, DeleteForm, GeneratedTableView, InvoiceTemplateForm, LoadError, PageInputs,
|
AliasForm, CopyForm, DeleteForm, DetailColumn, GeneratedTableView, InvoiceTemplateForm,
|
||||||
PresentationForm, Selection, TableDefinitionPageState,
|
LoadError, OrderForm, PageInputs, Selection, TableDefinitionPageState,
|
||||||
},
|
},
|
||||||
ui,
|
ui,
|
||||||
};
|
};
|
||||||
@@ -316,67 +316,164 @@ pub(crate) async fn add_columns(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST /admin/tables/presentation — SetColumnPresentation.
|
/// POST /admin/tables/presentation/alias — SetColumnPresentation, renaming one
|
||||||
pub(crate) async fn set_column_presentation(
|
/// column.
|
||||||
|
///
|
||||||
|
/// The request the backend wants is the whole table, so the columns this write
|
||||||
|
/// is not about are filled in from a fresh read rather than from the browser.
|
||||||
|
/// That is the point of the split: the only thing the form contributes is the
|
||||||
|
/// alias and the id of the column it was typed into.
|
||||||
|
pub(crate) async fn set_column_alias(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Form(form): Form<PresentationForm>,
|
Form(form): Form<AliasForm>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Some(rejection) = reject_cross_site(&headers) {
|
if let Some(rejection) = reject_cross_site(&headers) {
|
||||||
return rejection;
|
return rejection;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut inputs = PageInputs::for_selection(Selection {
|
let inputs = PageInputs::for_selection(Selection {
|
||||||
profile: form.profile.clone(),
|
profile: form.profile.clone(),
|
||||||
table: form.table.clone(),
|
table: form.table.clone(),
|
||||||
});
|
});
|
||||||
inputs.presentation = form.clone();
|
|
||||||
|
|
||||||
if form.column_ids.is_empty()
|
let alias = form.alias.trim().to_string();
|
||||||
|| form.column_ids.len() != form.aliases.len()
|
if alias.is_empty() {
|
||||||
|| form.aliases.iter().any(|alias| alias.trim().is_empty())
|
let message = tr!(Locale::from_headers(&headers), "td-err-choose-rename");
|
||||||
{
|
return refuse(state, headers, inputs, Page::Presentation, message).await;
|
||||||
let message = tr!(
|
}
|
||||||
Locale::from_headers(&headers),
|
|
||||||
"td-err-choose-rename"
|
let columns = match current_columns(&state, &headers, &inputs).await {
|
||||||
);
|
Ok(columns) => columns,
|
||||||
return refuse(
|
Err(response) => return response,
|
||||||
|
};
|
||||||
|
if !columns.iter().any(|column| column.column_id == form.column_id) {
|
||||||
|
let message = tr!(Locale::from_headers(&headers), "td-err-unknown-column");
|
||||||
|
return refuse(state, headers, inputs, Page::Presentation, message).await;
|
||||||
|
}
|
||||||
|
let presentation = columns
|
||||||
|
.iter()
|
||||||
|
.map(|column| ColumnPresentation {
|
||||||
|
column_id: column.column_id,
|
||||||
|
alias: if column.column_id == form.column_id {
|
||||||
|
alias.clone()
|
||||||
|
} else {
|
||||||
|
column.name.clone()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
apply_presentation(
|
||||||
state,
|
state,
|
||||||
headers,
|
headers,
|
||||||
inputs,
|
inputs,
|
||||||
Page::Presentation,
|
form.profile,
|
||||||
message,
|
form.table,
|
||||||
|
form.expected_row_version,
|
||||||
|
presentation,
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut columns = form
|
/// POST /admin/tables/presentation/order — SetColumnPresentation, moving one
|
||||||
.column_ids
|
/// column past its neighbour.
|
||||||
|
///
|
||||||
|
/// Every alias in the request is the name the backend just reported, so this
|
||||||
|
/// write cannot rename a column even when the browser's copy of the table is
|
||||||
|
/// stale. Only the order it sends comes from the form.
|
||||||
|
pub(crate) async fn set_column_order(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Form(form): Form<OrderForm>,
|
||||||
|
) -> Response {
|
||||||
|
if let Some(rejection) = reject_cross_site(&headers) {
|
||||||
|
return rejection;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputs = PageInputs::for_selection(Selection {
|
||||||
|
profile: form.profile.clone(),
|
||||||
|
table: form.table.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let columns = match current_columns(&state, &headers, &inputs).await {
|
||||||
|
Ok(columns) => columns,
|
||||||
|
Err(response) => return response,
|
||||||
|
};
|
||||||
|
let Some(index) = columns
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.position(|column| column.column_id == form.column_id)
|
||||||
.zip(form.aliases.iter())
|
else {
|
||||||
.map(|(column_id, alias)| ColumnPresentation {
|
let message = tr!(Locale::from_headers(&headers), "td-err-unknown-column");
|
||||||
column_id,
|
return refuse(state, headers, inputs, Page::Presentation, message).await;
|
||||||
alias: alias.trim().to_string(),
|
};
|
||||||
})
|
// A column at the end of the table has nowhere further to go, and the
|
||||||
.collect::<Vec<_>>();
|
// button that says so is disabled; a request that asks anyway is answered
|
||||||
if let Some((direction, index)) = form.action.split_once(':') {
|
// with the table as it is.
|
||||||
if let Ok(index) = index.parse::<usize>() {
|
let swap_with = match form.direction.as_str() {
|
||||||
let other = match direction {
|
|
||||||
"up" => index.checked_sub(1),
|
"up" => index.checked_sub(1),
|
||||||
"down" if index + 1 < columns.len() => Some(index + 1),
|
"down" if index + 1 < columns.len() => Some(index + 1),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
if let Some(other) = other {
|
let Some(swap_with) = swap_with else {
|
||||||
columns.swap(index, other);
|
return respond(state, headers, inputs, Page::Presentation, StatusCode::OK).await;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
let mut presentation = columns
|
||||||
|
.iter()
|
||||||
|
.map(|column| ColumnPresentation {
|
||||||
|
column_id: column.column_id,
|
||||||
|
alias: column.name.clone(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
presentation.swap(index, swap_with);
|
||||||
|
|
||||||
|
apply_presentation(
|
||||||
|
state,
|
||||||
|
headers,
|
||||||
|
inputs,
|
||||||
|
form.profile,
|
||||||
|
form.table,
|
||||||
|
form.expected_row_version,
|
||||||
|
presentation,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The table's columns as the backend has them now, in their current order.
|
||||||
|
///
|
||||||
|
/// Both presentation writes have to send every column, and the ones they are
|
||||||
|
/// not about must carry the name the server holds this moment -- not the name
|
||||||
|
/// the browser was showing when the page was drawn.
|
||||||
|
async fn current_columns(
|
||||||
|
state: &AppState,
|
||||||
|
headers: &HeaderMap,
|
||||||
|
inputs: &PageInputs,
|
||||||
|
) -> Result<Vec<DetailColumn>, Response> {
|
||||||
|
match load_page(state.clone(), headers, inputs.clone()).await {
|
||||||
|
Ok(page) => Ok(page.detail.map(|detail| detail.columns).unwrap_or_default()),
|
||||||
|
Err(error) => Err(load_error_response(headers, error)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The half both writes share: send the presentation, answer with the page.
|
||||||
|
///
|
||||||
|
/// `expected_row_version` is the browser's, not the one the read above saw, so
|
||||||
|
/// a definition that changed under the user is still refused by the backend
|
||||||
|
/// rather than silently written over.
|
||||||
|
async fn apply_presentation(
|
||||||
|
state: AppState,
|
||||||
|
headers: HeaderMap,
|
||||||
|
inputs: PageInputs,
|
||||||
|
profile: String,
|
||||||
|
table: String,
|
||||||
|
expected_row_version: i64,
|
||||||
|
columns: Vec<ColumnPresentation>,
|
||||||
|
) -> Response {
|
||||||
let request = SetColumnPresentationRequest {
|
let request = SetColumnPresentationRequest {
|
||||||
profile_name: form.profile.clone(),
|
profile_name: profile,
|
||||||
table_name: form.table.clone(),
|
table_name: table,
|
||||||
columns,
|
columns,
|
||||||
expected_row_version: form.expected_row_version,
|
expected_row_version,
|
||||||
};
|
};
|
||||||
let Ok(request) = authenticated_request(&headers, request) else {
|
let Ok(request) = authenticated_request(&headers, request) else {
|
||||||
return Redirect::to("/login").into_response();
|
return Redirect::to("/login").into_response();
|
||||||
@@ -385,28 +482,28 @@ pub(crate) async fn set_column_presentation(
|
|||||||
let mut definitions = state.definitions.clone();
|
let mut definitions = state.definitions.clone();
|
||||||
match definitions.set_column_presentation(request).await {
|
match definitions.set_column_presentation(request).await {
|
||||||
Ok(response) if response.get_ref().success => {
|
Ok(response) if response.get_ref().success => {
|
||||||
|
let mut inputs = inputs;
|
||||||
inputs.status = Some(response.into_inner().message);
|
inputs.status = Some(response.into_inner().message);
|
||||||
inputs.presentation = PresentationForm {
|
|
||||||
profile: form.profile,
|
|
||||||
table: form.table,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
respond(state, headers, inputs, Page::Presentation, StatusCode::OK).await
|
respond(state, headers, inputs, Page::Presentation, StatusCode::OK).await
|
||||||
}
|
}
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
let message = response.into_inner().message;
|
let message = response.into_inner().message;
|
||||||
let message = if message.is_empty() {
|
let message = if message.is_empty() {
|
||||||
tr!(
|
tr!(Locale::from_headers(&headers), "td-err-backend-no-rename")
|
||||||
Locale::from_headers(&headers),
|
|
||||||
"td-err-backend-no-rename"
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
message
|
message
|
||||||
};
|
};
|
||||||
refuse(state, headers, inputs, Page::Presentation, message).await
|
refuse(state, headers, inputs, Page::Presentation, message).await
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
refuse(state, headers, inputs, Page::Presentation, error.message().to_string()).await
|
refuse(
|
||||||
|
state,
|
||||||
|
headers,
|
||||||
|
inputs,
|
||||||
|
Page::Presentation,
|
||||||
|
error.message().to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,18 @@ pub(crate) fn router() -> Router<AppState> {
|
|||||||
"/admin/tables/columns/add/builder",
|
"/admin/tables/columns/add/builder",
|
||||||
post(logic::update_columns),
|
post(logic::update_columns),
|
||||||
)
|
)
|
||||||
|
// Naming a column and ordering the columns are one backend call but two
|
||||||
|
// forms, because a request that carries both is a request in which a
|
||||||
|
// stale alias can ride along with an unrelated edit. See
|
||||||
|
// `state::AliasForm`.
|
||||||
|
.route("/admin/tables/presentation", get(logic::presentation_page))
|
||||||
.route(
|
.route(
|
||||||
"/admin/tables/presentation",
|
"/admin/tables/presentation/alias",
|
||||||
get(logic::presentation_page).post(logic::set_column_presentation),
|
post(logic::set_column_alias),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/admin/tables/presentation/order",
|
||||||
|
post(logic::set_column_order),
|
||||||
)
|
)
|
||||||
.route("/admin/tables/delete", get(logic::delete_page))
|
.route("/admin/tables/delete", get(logic::delete_page))
|
||||||
.route("/admin/tables/delete", post(logic::delete_table))
|
.route("/admin/tables/delete", post(logic::delete_table))
|
||||||
|
|||||||
@@ -173,10 +173,18 @@ pub(crate) struct GeneratedTableView {
|
|||||||
pub parent_table_name: String,
|
pub parent_table_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The rename panel's inputs, kept across a failed submit so the user does not
|
/// Renaming one column.
|
||||||
/// retype them.
|
///
|
||||||
|
/// `SetColumnPresentation` sets names and order in one call and insists on
|
||||||
|
/// being given every column, so a browser that posts the whole table posts a
|
||||||
|
/// list it read at some earlier moment. When the ids and the aliases came from
|
||||||
|
/// two different moments, they still zipped into a valid request -- one that
|
||||||
|
/// renamed `amount` to `note` and `note` to `amount`, and took every validation
|
||||||
|
/// and type along with the names. This form carries one alias and the id of the
|
||||||
|
/// column it was typed into, so there is no list to misalign; the handler fills
|
||||||
|
/// the rest of the table in from the backend.
|
||||||
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
||||||
pub(crate) struct PresentationForm {
|
pub(crate) struct AliasForm {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub profile: String,
|
pub profile: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -184,11 +192,29 @@ pub(crate) struct PresentationForm {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub expected_row_version: i64,
|
pub expected_row_version: i64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub column_ids: Vec<i64>,
|
pub column_id: i64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub aliases: Vec<String>,
|
pub alias: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moving one column past its neighbour.
|
||||||
|
///
|
||||||
|
/// It carries no alias at all -- not even the one it is moving -- so a reorder
|
||||||
|
/// cannot rename anything, whatever the browser still had on screen. See
|
||||||
|
/// [`AliasForm`] for why that separation is worth two forms.
|
||||||
|
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
||||||
|
pub(crate) struct OrderForm {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub action: String,
|
pub profile: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub table: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub expected_row_version: i64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub column_id: i64,
|
||||||
|
/// `up` or `down`. Anything else moves nothing.
|
||||||
|
#[serde(default)]
|
||||||
|
pub direction: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The copy-profile panel. An empty `table_names` copies the whole profile,
|
/// The copy-profile panel. An empty `table_names` copies the whole profile,
|
||||||
@@ -248,7 +274,6 @@ pub(crate) struct PageInputs {
|
|||||||
pub selection: Selection,
|
pub selection: Selection,
|
||||||
/// The columns staged for `AddTableColumns`.
|
/// The columns staged for `AddTableColumns`.
|
||||||
pub columns: ColumnDraft,
|
pub columns: ColumnDraft,
|
||||||
pub presentation: PresentationForm,
|
|
||||||
pub copy: CopyForm,
|
pub copy: CopyForm,
|
||||||
pub invoice: InvoiceTemplateForm,
|
pub invoice: InvoiceTemplateForm,
|
||||||
pub status: Option<String>,
|
pub status: Option<String>,
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ mod tests {
|
|||||||
assert!(html.contains("/admin/tables/delete"));
|
assert!(html.contains("/admin/tables/delete"));
|
||||||
assert!(html.contains("Type <code>invoice</code> to confirm"));
|
assert!(html.contains("Type <code>invoice</code> to confirm"));
|
||||||
// The other writes are links in the switcher, not forms on the page.
|
// The other writes are links in the switcher, not forms on the page.
|
||||||
assert!(!html.contains(r#"hx-post="/admin/tables/presentation""#));
|
assert!(!html.contains(r#"hx-post="/admin/tables/presentation/alias""#));
|
||||||
assert!(!html.contains("/admin/profiles/copy?profile=billing\" method"));
|
assert!(!html.contains("/admin/profiles/copy?profile=billing\" method"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,28 +404,54 @@ mod tests {
|
|||||||
assert!(!html.contains(r#"id="column-form""#));
|
assert!(!html.contains(r#"id="column-form""#));
|
||||||
|
|
||||||
let html = render_presentation_page(&state);
|
let html = render_presentation_page(&state);
|
||||||
assert!(!html.contains(r#"name="aliases""#));
|
assert!(!html.contains(r#"name="alias""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Renaming and reordering are two forms, and the split is what the page
|
||||||
|
/// has to keep: an alias is posted with the id of the column it was typed
|
||||||
|
/// into, and the order form posts no alias at all, so moving a column
|
||||||
|
/// cannot carry a stale name along with it.
|
||||||
#[test]
|
#[test]
|
||||||
fn column_presentation_posts_stable_ids_aliases_and_order_controls() {
|
fn renaming_and_reordering_are_separate_forms() {
|
||||||
let html = render_presentation_page(&page());
|
let html = render_presentation_page(&page());
|
||||||
|
|
||||||
assert!(html.contains(r#"hx-post="/admin/tables/presentation""#), "{html}");
|
assert!(
|
||||||
assert!(html.contains(r#"name="column_ids" value="1""#), "{html}");
|
html.contains(r#"hx-post="/admin/tables/presentation/alias""#),
|
||||||
|
"{html}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
html.contains(r#"hx-post="/admin/tables/presentation/order""#),
|
||||||
|
"{html}"
|
||||||
|
);
|
||||||
|
assert!(html.contains(r#"name="column_id" value="1""#), "{html}");
|
||||||
assert!(html.contains(r#"name="expected_row_version" value="1""#), "{html}");
|
assert!(html.contains(r#"name="expected_row_version" value="1""#), "{html}");
|
||||||
assert!(html.contains(r#"name="aliases" value="number""#), "{html}");
|
assert!(html.contains(r#"name="alias" value="number""#), "{html}");
|
||||||
assert!(html.contains(r#"name="action" value="save""#), "{html}");
|
assert!(html.contains(r#"name="direction" value="up""#), "{html}");
|
||||||
|
assert!(html.contains(r#"name="direction" value="down""#), "{html}");
|
||||||
|
|
||||||
|
// The old fused form is gone: no list of ids paired positionally with a
|
||||||
|
// list of aliases, and no single save that posts both at once.
|
||||||
|
assert!(!html.contains(r#"name="column_ids""#), "{html}");
|
||||||
|
assert!(!html.contains(r#"name="aliases""#), "{html}");
|
||||||
|
assert!(!html.contains(r#"name="action""#), "{html}");
|
||||||
|
|
||||||
|
// And the order form's own markup names no alias.
|
||||||
|
let order_form = html
|
||||||
|
.split(r#"hx-post="/admin/tables/presentation/order""#)
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|rest| rest.split("</form>").next())
|
||||||
|
.expect("the order form is rendered");
|
||||||
|
assert!(!order_form.contains(r#"name="alias""#), "{order_form}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn adding_columns_and_presentation_are_separate_pages() {
|
fn adding_columns_and_presentation_are_separate_pages() {
|
||||||
let add_html = render_add_columns_page(&page());
|
let add_html = render_add_columns_page(&page());
|
||||||
assert!(add_html.contains(r#"id="column-form""#), "{add_html}");
|
assert!(add_html.contains(r#"id="column-form""#), "{add_html}");
|
||||||
assert!(!add_html.contains(r#"name="aliases""#), "{add_html}");
|
assert!(!add_html.contains(r#"name="alias""#), "{add_html}");
|
||||||
|
|
||||||
let presentation_html = render_presentation_page(&page());
|
let presentation_html = render_presentation_page(&page());
|
||||||
assert!(presentation_html.contains(r#"name="aliases""#), "{presentation_html}");
|
assert!(presentation_html.contains(r#"name="alias""#), "{presentation_html}");
|
||||||
assert!(!presentation_html.contains(r#"id="column-form""#), "{presentation_html}");
|
assert!(!presentation_html.contains(r#"id="column-form""#), "{presentation_html}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ use crate::{i18n::Locale, tr};
|
|||||||
|
|
||||||
use crate::definitions::table_structure::TableStructureResponse;
|
use crate::definitions::table_structure::TableStructureResponse;
|
||||||
|
|
||||||
/// The columns a CSV carries, for an export and for the import that reads one
|
/// The columns an import can write, and the export's default header.
|
||||||
/// back.
|
|
||||||
///
|
///
|
||||||
/// Both ends use this list, so a file the export writes is a file the import
|
/// Both ends use this list, so a file the default export writes is a file the
|
||||||
/// accepts. That only holds if every system column is left out: a row is
|
/// import accepts. That only holds if every system column is left out: a row is
|
||||||
/// inserted with `post_table_data`, which takes user columns and nothing else,
|
/// inserted with `post_table_data`, which takes user columns and nothing else,
|
||||||
/// so exporting `row_revision` or `created_at` produced a file whose own
|
/// so exporting `row_revision` or `created_at` produced a file whose own
|
||||||
/// re-import the server answered with `Invalid column`. The names come from
|
/// re-import the server answered with `Invalid column`. The names come from
|
||||||
@@ -25,16 +24,48 @@ pub(crate) fn exportable_columns(schema: &TableStructureResponse) -> Vec<String>
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every column of a row the server will actually hand over: the user's
|
||||||
|
/// columns, plus `id`, `deleted` and `row_revision`. This is what the export
|
||||||
|
/// writes when asked for the whole row.
|
||||||
|
///
|
||||||
|
/// "Everything" is bounded twice, and both bounds are the server's. Internal
|
||||||
|
/// columns -- the link version columns, `version`, `closed_by_user_id` -- are
|
||||||
|
/// dropped inside `get_table_structure` and never reach this crate. And
|
||||||
|
/// `created_at` is in the structure but not in the row: a read selects the
|
||||||
|
/// leading system columns and the user's, so asking for it would write a header
|
||||||
|
/// over a column of blanks. Filtering by what a read returns keeps the file
|
||||||
|
/// honest about that.
|
||||||
|
///
|
||||||
|
/// Such a file is a record of the table, not a file to load back: the system
|
||||||
|
/// columns in it are the server's to write, and an import ignores them.
|
||||||
|
pub(crate) fn all_columns(schema: &TableStructureResponse) -> Vec<String> {
|
||||||
|
schema
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.filter(|column| !is_read_omitted_column(&column.name))
|
||||||
|
.map(|column| column.name.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether `name` is one of the columns the server puts on every managed
|
/// Whether `name` is one of the columns the server puts on every managed
|
||||||
/// table. The virtual `account` name is deliberately not checked: it is an
|
/// table. The virtual `account` name is deliberately not checked: it is an
|
||||||
/// alias a user may write to, not a column the server fills in.
|
/// alias a user may write to, not a column the server fills in.
|
||||||
fn is_system_column(name: &str) -> bool {
|
pub(crate) fn is_system_column(name: &str) -> bool {
|
||||||
crate::system_column::LEADING_SYSTEM_COLUMNS
|
crate::system_column::LEADING_SYSTEM_COLUMNS
|
||||||
.iter()
|
.iter()
|
||||||
.chain(crate::system_column::TRAILING_SYSTEM_COLUMNS.iter())
|
.chain(crate::system_column::TRAILING_SYSTEM_COLUMNS.iter())
|
||||||
.any(|column| column.name == name)
|
.any(|column| column.name == name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the structure lists `name` but a row read leaves it out. The
|
||||||
|
/// trailing system columns are declared after the user's and are not part of
|
||||||
|
/// what `get_table_data` selects.
|
||||||
|
fn is_read_omitted_column(name: &str) -> bool {
|
||||||
|
crate::system_column::TRAILING_SYSTEM_COLUMNS
|
||||||
|
.iter()
|
||||||
|
.any(|column| column.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn column_types(schema: &TableStructureResponse) -> HashMap<String, String> {
|
pub(crate) fn column_types(schema: &TableStructureResponse) -> HashMap<String, String> {
|
||||||
schema
|
schema
|
||||||
.columns
|
.columns
|
||||||
@@ -115,4 +146,30 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(exportable_columns(&schema), vec!["number".to_string()]);
|
assert_eq!(exportable_columns(&schema), vec!["number".to_string()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The whole-row export keeps the system columns the server hands over,
|
||||||
|
/// and drops `created_at`, which the structure lists but a read does not
|
||||||
|
/// return.
|
||||||
|
#[test]
|
||||||
|
fn the_whole_row_is_what_a_read_returns() {
|
||||||
|
let schema = TableStructureResponse {
|
||||||
|
columns: vec![
|
||||||
|
column("id", true),
|
||||||
|
column("deleted", false),
|
||||||
|
column("row_revision", false),
|
||||||
|
column("number", false),
|
||||||
|
column("created_at", false),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
all_columns(&schema),
|
||||||
|
vec![
|
||||||
|
"id".to_string(),
|
||||||
|
"deleted".to_string(),
|
||||||
|
"row_revision".to_string(),
|
||||||
|
"number".to_string(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use super::{
|
|||||||
super::common::{
|
super::common::{
|
||||||
csv::write_record,
|
csv::write_record,
|
||||||
loader::LoadError,
|
loader::LoadError,
|
||||||
schema::exportable_columns,
|
schema::{all_columns, exportable_columns},
|
||||||
},
|
},
|
||||||
loader::load_page,
|
loader::load_page,
|
||||||
state::ExportForm,
|
state::ExportForm,
|
||||||
@@ -55,6 +55,14 @@ pub(crate) async fn export_csv(
|
|||||||
Ok(targets) => targets,
|
Ok(targets) => targets,
|
||||||
Err(message) => return reject(&headers, message),
|
Err(message) => return reject(&headers, message),
|
||||||
};
|
};
|
||||||
|
// Everything the structure exposes, or only what an import could write
|
||||||
|
// back. The first is a record of the table, the second a file that round
|
||||||
|
// trips.
|
||||||
|
let columns_of = if form.include_system_columns() {
|
||||||
|
all_columns
|
||||||
|
} else {
|
||||||
|
exportable_columns
|
||||||
|
};
|
||||||
let Some(profile) = catalog.profiles.iter().find(|profile| profile.name == profile_name) else {
|
let Some(profile) = catalog.profiles.iter().find(|profile| profile.name == profile_name) else {
|
||||||
return reject(
|
return reject(
|
||||||
&headers,
|
&headers,
|
||||||
@@ -118,7 +126,7 @@ pub(crate) async fn export_csv(
|
|||||||
&tr!(Locale::from_headers(&headers), "export-err-negative-count"),
|
&tr!(Locale::from_headers(&headers), "export-err-negative-count"),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
tables.push(ExportTable { name: table_name.clone(), columns: exportable_columns(&structure), count });
|
tables.push(ExportTable { name: table_name.clone(), columns: columns_of(&structure), count });
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut csv = String::new();
|
let mut csv = String::new();
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ pub(crate) struct ExportForm {
|
|||||||
pub profile_name: String,
|
pub profile_name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub table_names: String,
|
pub table_names: String,
|
||||||
|
/// An unchecked checkbox is not posted at all, so its absence is the
|
||||||
|
/// `false` and any value it does carry is the `true`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub include_system_columns: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct ExportPageState {
|
pub(crate) struct ExportPageState {
|
||||||
@@ -31,4 +35,9 @@ impl ExportForm {
|
|||||||
}
|
}
|
||||||
Ok((profile.to_string(), tables))
|
Ok((profile.to_string(), tables))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the download carries the system columns as well.
|
||||||
|
pub(crate) fn include_system_columns(&self) -> bool {
|
||||||
|
self.include_system_columns.is_some()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use super::{
|
|||||||
super::common::{
|
super::common::{
|
||||||
csv::parse_csv,
|
csv::parse_csv,
|
||||||
loader::LoadError,
|
loader::LoadError,
|
||||||
schema::{column_types, csv_value, exportable_columns},
|
schema::{column_types, csv_value, exportable_columns, is_system_column},
|
||||||
},
|
},
|
||||||
loader::load_page,
|
loader::load_page,
|
||||||
state::ImportForm,
|
state::ImportForm,
|
||||||
@@ -134,6 +134,12 @@ pub(crate) async fn import_csv(
|
|||||||
}
|
}
|
||||||
for (index, column) in columns.iter().enumerate() {
|
for (index, column) in columns.iter().enumerate() {
|
||||||
let belongs = table_headers.as_ref().map_or(table_names.len() == 1, |headers| headers.get(index).is_some_and(|name| name == &table.name));
|
let belongs = table_headers.as_ref().map_or(table_names.len() == 1, |headers| headers.get(index).is_some_and(|name| name == &table.name));
|
||||||
|
// A system column is the server's to write, so a file that carries
|
||||||
|
// one -- an export taken with the system columns included -- loads
|
||||||
|
// with that column left where it is, not refused.
|
||||||
|
if belongs && is_system_column(column) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if belongs && !table.columns.contains(column) {
|
if belongs && !table.columns.contains(column) {
|
||||||
return reject(
|
return reject(
|
||||||
&headers,
|
&headers,
|
||||||
|
|||||||
@@ -3,30 +3,55 @@
|
|||||||
|
|
||||||
{% if page.table_is_writable() %}
|
{% if page.table_is_writable() %}
|
||||||
{% if let Some(detail) = page.detail %}
|
{% if let Some(detail) = page.detail %}
|
||||||
|
{# One form per column, each carrying its own column id: an alias is never
|
||||||
|
posted apart from the column it was typed into. #}
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>{{ nav.tr("td-rename-column") }}</h2>
|
<h2>{{ nav.tr("td-rename-column") }}</h2>
|
||||||
<p class="hint">{{ nav.tr("td-rename-hint") }}</p>
|
<p class="hint">{{ nav.tr("td-rename-hint") }}</p>
|
||||||
<form hx-post="/admin/tables/presentation"
|
{% for column in detail.columns %}
|
||||||
|
{% if column.renameable %}
|
||||||
|
<form hx-post="/admin/tables/presentation/alias"
|
||||||
hx-target="#table-panel" hx-swap="innerHTML">
|
hx-target="#table-panel" hx-swap="innerHTML">
|
||||||
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
|
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
|
||||||
<input type="hidden" name="table" value="{{ page.selection.table }}">
|
<input type="hidden" name="table" value="{{ page.selection.table }}">
|
||||||
<input type="hidden" name="expected_row_version" value="{{ detail.row_version }}">
|
<input type="hidden" name="expected_row_version" value="{{ detail.row_version }}">
|
||||||
|
<input type="hidden" name="column_id" value="{{ column.column_id }}">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
{% for column in detail.columns %}
|
|
||||||
<input type="hidden" name="column_ids" value="{{ column.column_id }}">
|
|
||||||
<label>{{ nav.tr("td-column-label") }}
|
<label>{{ nav.tr("td-column-label") }}
|
||||||
<input name="aliases" value="{{ column.name }}" {% if !column.renameable %}readonly{% endif %}>
|
<input name="alias" value="{{ column.name }}">
|
||||||
</label>
|
</label>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" name="action" value="up:{{ loop.index0 }}" {% if loop.first %}disabled{% endif %}>↑</button>
|
<button type="submit">{{ nav.tr("td-rename-button") }}</button>
|
||||||
<button type="submit" name="action" value="down:{{ loop.index0 }}" {% if loop.last %}disabled{% endif %}>↓</button>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" name="action" value="save">{{ nav.tr("td-rename-button") }}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<p class="hint"><code>{{ column.name }}</code> — {{ nav.tr("td-alias-locked") }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{# The order forms carry no alias at all, so moving a column cannot rename
|
||||||
|
one. The names below are labels, not inputs. #}
|
||||||
|
<section class="panel">
|
||||||
|
<h2>{{ nav.tr("td-order-heading") }}</h2>
|
||||||
|
<p class="hint">{{ nav.tr("td-order-hint") }}</p>
|
||||||
|
<ol class="column-order">
|
||||||
|
{% for column in detail.columns %}
|
||||||
|
<li>
|
||||||
|
<code>{{ column.name }}</code>
|
||||||
|
<form hx-post="/admin/tables/presentation/order"
|
||||||
|
hx-target="#table-panel" hx-swap="innerHTML">
|
||||||
|
<input type="hidden" name="profile" value="{{ page.selection.profile }}">
|
||||||
|
<input type="hidden" name="table" value="{{ page.selection.table }}">
|
||||||
|
<input type="hidden" name="expected_row_version" value="{{ detail.row_version }}">
|
||||||
|
<input type="hidden" name="column_id" value="{{ column.column_id }}">
|
||||||
|
<button type="submit" name="direction" value="up" {% if loop.first %}disabled{% endif %}>↑</button>
|
||||||
|
<button type="submit" name="direction" value="down" {% if loop.last %}disabled{% endif %}>↓</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@
|
|||||||
<small>{{ nav.tr("export-comma-hint") }}</small>
|
<small>{{ nav.tr("export-comma-hint") }}</small>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="check">
|
||||||
|
<input type="checkbox" name="include_system_columns" value="true">
|
||||||
|
{{ nav.tr("export-include-system") }}
|
||||||
|
</label>
|
||||||
|
<small>{{ nav.tr("export-include-system-hint") }}</small>
|
||||||
<datalist id="export-tables">
|
<datalist id="export-tables">
|
||||||
{%- for profile in page.catalog.profiles %}{% for table in profile.tables %}<option value="{{ table }}"></option>{% endfor %}{% endfor -%}
|
{%- for profile in page.catalog.profiles %}{% for table in profile.tables %}<option value="{{ table }}"></option>{% endfor %}{% endfor -%}
|
||||||
</datalist>
|
</datalist>
|
||||||
|
|||||||
Reference in New Issue
Block a user