better UX/UI
This commit is contained in:
2
client
2
client
Submodule client updated: bfe8d72e32...faeca44d40
@@ -167,11 +167,18 @@ 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-column-label = Sloupec
|
||||
td-choose-column = Vyberte sloupec
|
||||
td-current-alias = Současný alias
|
||||
td-new-name = Nový název
|
||||
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-order-hint = Přetáhněte sloupce na místo a uložte je, až vám bude pořadí vyhovovat.
|
||||
td-drag-column = Přetažením změnit pořadí
|
||||
td-order-clean = Žádné neuložené změny pořadí
|
||||
td-order-unsaved = Neuložené změny pořadí
|
||||
td-order-reset = Vrátit změny
|
||||
td-order-save = Uložit pořadí
|
||||
td-err-invalid-order = Pořadí sloupců se změnilo nebo není úplné. Načtěte ho znovu a zkuste to znovu.
|
||||
td-columns-now = Současné sloupce
|
||||
td-col-column = Sloupec
|
||||
td-col-type = Typ
|
||||
|
||||
@@ -169,11 +169,18 @@ 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-column-label = Column
|
||||
td-choose-column = Choose a column
|
||||
td-current-alias = Current alias
|
||||
td-new-name = New name
|
||||
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-order-hint = Drag columns into place, then save when the order feels right.
|
||||
td-drag-column = Drag to reorder
|
||||
td-order-clean = No unsaved order changes
|
||||
td-order-unsaved = Unsaved order changes
|
||||
td-order-reset = Revert
|
||||
td-order-save = Save order
|
||||
td-err-invalid-order = The column order changed or is incomplete. Reload it and try again.
|
||||
td-columns-now = Columns it has now
|
||||
td-col-column = Column
|
||||
td-col-type = Type
|
||||
|
||||
@@ -167,11 +167,18 @@ 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-column-label = Stĺpec
|
||||
td-choose-column = Vyberte stĺpec
|
||||
td-current-alias = Súčasný alias
|
||||
td-new-name = Nový názov
|
||||
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-order-hint = Presuňte stĺpce potiahnutím a uložte ich, keď vám poradie vyhovuje.
|
||||
td-drag-column = Potiahnutím zmeniť poradie
|
||||
td-order-clean = Žiadne neuložené zmeny poradia
|
||||
td-order-unsaved = Neuložené zmeny poradia
|
||||
td-order-reset = Vrátiť zmeny
|
||||
td-order-save = Uložiť poradie
|
||||
td-err-invalid-order = Poradie stĺpcov sa zmenilo alebo nie je úplné. Načítajte ho znova a skúste to opäť.
|
||||
td-columns-now = Súčasné stĺpce
|
||||
td-col-column = Stĺpec
|
||||
td-col-type = Typ
|
||||
|
||||
@@ -459,7 +459,7 @@ mod tests {
|
||||
),
|
||||
(
|
||||
"/admin/tables/presentation/order",
|
||||
"profile=billing&table=invoice&column_id=1&direction=up",
|
||||
"profile=billing&table=invoice&column_ids=1&column_ids=2",
|
||||
),
|
||||
("/admin/tables/columns/add", "profile=billing&table=invoice"),
|
||||
("/admin/tables/builder", ""),
|
||||
|
||||
@@ -375,12 +375,12 @@ pub(crate) async fn set_column_alias(
|
||||
.await
|
||||
}
|
||||
|
||||
/// POST /admin/tables/presentation/order — SetColumnPresentation, moving one
|
||||
/// column past its neighbour.
|
||||
/// POST /admin/tables/presentation/order — SetColumnPresentation, saving the
|
||||
/// order staged by the browser.
|
||||
///
|
||||
/// 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.
|
||||
/// write cannot rename a column. The ids from the browser must be an exact
|
||||
/// permutation of the current columns before their order is accepted.
|
||||
pub(crate) async fn set_column_order(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
@@ -399,33 +399,38 @@ pub(crate) async fn set_column_order(
|
||||
Ok(columns) => columns,
|
||||
Err(response) => return response,
|
||||
};
|
||||
let Some(index) = columns
|
||||
let current_ids = columns
|
||||
.iter()
|
||||
.position(|column| column.column_id == form.column_id)
|
||||
else {
|
||||
let message = tr!(Locale::from_headers(&headers), "td-err-unknown-column");
|
||||
.map(|column| column.column_id)
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
let submitted_ids = form
|
||||
.column_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
if form.column_ids.len() != columns.len()
|
||||
|| submitted_ids.len() != form.column_ids.len()
|
||||
|| submitted_ids != current_ids
|
||||
{
|
||||
let message = tr!(Locale::from_headers(&headers), "td-err-invalid-order");
|
||||
return refuse(state, headers, inputs, Page::Presentation, message).await;
|
||||
};
|
||||
// A column at the end of the table has nowhere further to go, and the
|
||||
// button that says so is disabled; a request that asks anyway is answered
|
||||
// with the table as it is.
|
||||
let swap_with = match form.direction.as_str() {
|
||||
"up" => index.checked_sub(1),
|
||||
"down" if index + 1 < columns.len() => Some(index + 1),
|
||||
_ => None,
|
||||
};
|
||||
let Some(swap_with) = swap_with else {
|
||||
return respond(state, headers, inputs, Page::Presentation, StatusCode::OK).await;
|
||||
};
|
||||
}
|
||||
|
||||
let mut presentation = columns
|
||||
let columns_by_id = columns
|
||||
.iter()
|
||||
.map(|column| ColumnPresentation {
|
||||
column_id: column.column_id,
|
||||
.map(|column| (column.column_id, column))
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
let presentation = form
|
||||
.column_ids
|
||||
.iter()
|
||||
.map(|column_id| {
|
||||
let column = columns_by_id[column_id];
|
||||
ColumnPresentation {
|
||||
column_id: *column_id,
|
||||
alias: column.name.clone(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
presentation.swap(index, swap_with);
|
||||
.collect();
|
||||
|
||||
apply_presentation(
|
||||
state,
|
||||
|
||||
@@ -38,10 +38,9 @@ pub(crate) fn router() -> Router<AppState> {
|
||||
"/admin/tables/columns/add/builder",
|
||||
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`.
|
||||
// Naming a column and ordering the columns are one backend call but
|
||||
// separate forms, because a request that carries both lets a stale
|
||||
// alias ride along with an unrelated edit. See `state::AliasForm`.
|
||||
.route("/admin/tables/presentation", get(logic::presentation_page))
|
||||
.route(
|
||||
"/admin/tables/presentation/alias",
|
||||
|
||||
@@ -201,11 +201,12 @@ pub(crate) struct AliasForm {
|
||||
pub alias: String,
|
||||
}
|
||||
|
||||
/// Moving one column past its neighbour.
|
||||
/// Saving a complete, staged column order.
|
||||
///
|
||||
/// 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.
|
||||
/// It carries ids only and no aliases, so a reorder cannot rename anything,
|
||||
/// whatever the browser still had on screen. The handler validates that this
|
||||
/// is an exact permutation of the table's current ids and supplies fresh
|
||||
/// aliases from the backend. See [`AliasForm`] for why that separation matters.
|
||||
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
||||
pub(crate) struct OrderForm {
|
||||
#[serde(default)]
|
||||
@@ -215,10 +216,7 @@ pub(crate) struct OrderForm {
|
||||
#[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,
|
||||
pub column_ids: Vec<i64>,
|
||||
}
|
||||
|
||||
/// The copy-profile panel. An empty `table_names` copies the whole profile,
|
||||
|
||||
@@ -412,10 +412,9 @@ mod tests {
|
||||
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.
|
||||
/// Renaming and reordering are separate forms. An alias is posted with its
|
||||
/// column id, while the staged order posts ids only, so saving one cannot
|
||||
/// carry stale values from the other.
|
||||
#[test]
|
||||
fn renaming_and_reordering_are_separate_forms() {
|
||||
let html = render_presentation_page(&page());
|
||||
@@ -430,13 +429,14 @@ mod tests {
|
||||
);
|
||||
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="alias" value="number""#), "{html}");
|
||||
assert!(html.contains(r#"name="direction" value="up""#), "{html}");
|
||||
assert!(html.contains(r#"name="direction" value="down""#), "{html}");
|
||||
assert!(html.contains(r#"name="alias" aria-label="New name: number""#), "{html}");
|
||||
assert!(!html.contains(r#"name="alias" value="number""#), "{html}");
|
||||
assert!(html.contains(r#"name="column_ids" value="1""#), "{html}");
|
||||
assert!(html.contains(r#"data-order-reset disabled"#), "{html}");
|
||||
assert!(html.contains(r#"data-order-save disabled"#), "{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}");
|
||||
// The old fused form is gone: the order form carries no positionally
|
||||
// paired alias list and no action that can mix the two operations.
|
||||
assert!(!html.contains(r#"name="aliases""#), "{html}");
|
||||
assert!(!html.contains(r#"name="action""#), "{html}");
|
||||
|
||||
|
||||
@@ -220,6 +220,41 @@
|
||||
.panel .count { margin-left: 6px; padding: 1px 7px; border-radius: 9px; font-size: 12px; color: #4b5563; background: #eef1f6; }
|
||||
.panel > button.secondary { margin-top: 16px; padding: 9px 16px; border: 1px solid #c9d2de; border-radius: 6px; color: #24324a; background: #f4f6fa; cursor: pointer; }
|
||||
.panel > button.secondary:hover { background: #e9edf4; }
|
||||
.presentation-layout { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(320px, .9fr); align-items: start; gap: 16px; }
|
||||
.presentation-panel > .hint { min-height: 40px; margin: -7px 0 15px; }
|
||||
.alias-list { overflow: hidden; border: 1px solid #e2e7ee; border-radius: 8px; background: white; }
|
||||
.alias-list-head, .alias-row { display: grid; grid-template-columns: minmax(90px, .8fr) 20px minmax(120px, 1.2fr) auto; align-items: center; gap: 8px; }
|
||||
.alias-list-head { padding: 8px 10px 6px; color: #7b8797; background: #f7f9fb; font-size: 10px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }
|
||||
.alias-row { margin: 0; padding: 8px 10px; border-top: 1px solid #e8ecf1; }
|
||||
.alias-current { min-width: 0; overflow: hidden; color: #33415c; font-size: 13px; text-overflow: ellipsis; }
|
||||
.alias-arrow { color: #9aa5b4; text-align: center; }
|
||||
.alias-row > input { min-width: 0; color: #24324a; font-size: 13px; font-weight: 500; }
|
||||
.alias-row > input:focus { border-color: #6ea1ed; outline: 3px solid rgb(37 99 235 / 12%); }
|
||||
.alias-row > button { padding: 9px 14px; border: 1px solid #b9cdf0; border-radius: 6px; color: #1d4ed8; background: #eef4ff; font-weight: 600; cursor: pointer; }
|
||||
.alias-row > button:hover { border-color: #8eb1eb; background: #deebff; }
|
||||
.alias-row > button:focus-visible { outline: 3px solid rgb(37 99 235 / 18%); outline-offset: 2px; }
|
||||
.alias-row-locked { background: #fafbfc; }
|
||||
.alias-row-locked .locked-label { grid-column: 3 / -1; }
|
||||
.locked-label { color: #8a94a4; font-size: 12px; }
|
||||
.column-order { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
|
||||
.column-order li { display: grid; grid-template-columns: 32px 30px minmax(0, 1fr); align-items: center; gap: 8px; min-height: 52px; padding: 8px 12px 8px 8px; border: 1px solid #e2e7ee; border-radius: 8px; background: #fafbfc; transition: border-color 120ms ease, box-shadow 120ms ease, transform 120ms ease, opacity 120ms ease; }
|
||||
.column-order li:hover { border-color: #b8cae3; box-shadow: 0 3px 10px rgb(31 43 58 / 7%); }
|
||||
.column-order li.is-dragging { z-index: 1; border-color: #7fa5df; box-shadow: 0 9px 22px rgb(31 43 58 / 18%); opacity: .82; transform: scale(1.015); }
|
||||
.drag-handle { display: grid; place-items: center; width: 32px; height: 34px; padding: 0; border: 0; border-radius: 6px; color: #8b97a8; background: transparent; font-size: 22px; line-height: 1; cursor: grab; touch-action: none; user-select: none; }
|
||||
.drag-handle:hover { color: #315f9d; background: #e9f0fb; }
|
||||
.drag-handle:focus-visible { color: #1d4ed8; outline: 3px solid rgb(37 99 235 / 18%); }
|
||||
.drag-handle:active { cursor: grabbing; }
|
||||
.order-position { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; color: #64748b; background: #e9eef5; font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.order-name { min-width: 0; overflow: hidden; color: #33415c; font-size: 13px; text-overflow: ellipsis; }
|
||||
.order-actions { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 8px; margin-top: 14px; padding-top: 14px; border-top: 1px solid #e5e9ef; }
|
||||
.order-status { color: #788495; font-size: 12px; }
|
||||
.order-editor.is-dirty .order-status { color: #9a5a08; font-weight: 600; }
|
||||
.order-actions button { margin: 0; padding: 8px 13px; border: 0; border-radius: 6px; color: white; background: #2563eb; font-weight: 600; cursor: pointer; }
|
||||
.order-actions button.secondary { border: 1px solid #c9d2de; color: #344054; background: white; }
|
||||
.order-actions button:hover:not(:disabled) { background: #1d4ed8; }
|
||||
.order-actions button.secondary:hover:not(:disabled) { background: #eef2f6; }
|
||||
.order-actions button:focus-visible { outline: 3px solid rgb(37 99 235 / 18%); outline-offset: 2px; }
|
||||
.order-actions button:disabled { border-color: #e1e6ed; color: #adb6c3; background: #f3f5f7; cursor: default; }
|
||||
.sql-preview { margin: 12px 0 0; padding: 12px; max-height: 260px; overflow: auto; border: 1px solid #e4e7ec; border-radius: 8px; font: 12px/1.5 ui-monospace, monospace; color: #33415c; background: #fafbfc; white-space: pre-wrap; }
|
||||
.danger-panel { border-color: #eccfcf; }
|
||||
.danger-panel h2 { color: #a12b2b; }
|
||||
@@ -340,6 +375,18 @@
|
||||
.analytics { grid-template-columns: 1fr; }
|
||||
.sidebar { position: static; max-height: none; }
|
||||
.query-row { grid-template-columns: 1fr; }
|
||||
.presentation-layout { grid-template-columns: 1fr; }
|
||||
.presentation-panel > .hint { min-height: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.alias-list-head { display: none; }
|
||||
.alias-row { grid-template-columns: minmax(0, 1fr) 18px minmax(0, 1.3fr); }
|
||||
.alias-row > button { grid-column: 3; width: 100%; }
|
||||
.alias-row-locked .locked-label { grid-column: 3; }
|
||||
.order-actions { grid-template-columns: 1fr 1fr; }
|
||||
.order-status { grid-column: 1 / -1; }
|
||||
.order-actions button { width: 100%; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,113 @@
|
||||
|
||||
{% block title %}{{ nav.tr("td-presentation-title") }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
let activeDrag = null;
|
||||
|
||||
function rows(editor) {
|
||||
return Array.from(editor.querySelectorAll("[data-order-item]"));
|
||||
}
|
||||
|
||||
function ids(editor) {
|
||||
return rows(editor).map(function (row) { return row.dataset.columnId; });
|
||||
}
|
||||
|
||||
function refresh(editor) {
|
||||
rows(editor).forEach(function (row, index) {
|
||||
row.querySelector(".order-position").textContent = index + 1;
|
||||
});
|
||||
|
||||
const dirty = ids(editor).join(",") !== editor.dataset.originalOrder;
|
||||
editor.classList.toggle("is-dirty", dirty);
|
||||
editor.querySelector("[data-order-status]").textContent = dirty
|
||||
? editor.dataset.dirtyText
|
||||
: editor.dataset.cleanText;
|
||||
editor.querySelector("[data-order-reset]").disabled = !dirty;
|
||||
editor.querySelector("[data-order-save]").disabled = !dirty;
|
||||
}
|
||||
|
||||
function initialise(root) {
|
||||
root.querySelectorAll("[data-order-editor]").forEach(function (editor) {
|
||||
editor.dataset.originalOrder = ids(editor).join(",");
|
||||
refresh(editor);
|
||||
});
|
||||
}
|
||||
|
||||
function finishDrag(event) {
|
||||
if (!activeDrag || (event.pointerId !== undefined && event.pointerId !== activeDrag.pointerId)) return;
|
||||
activeDrag.row.classList.remove("is-dragging");
|
||||
if (activeDrag.handle.hasPointerCapture(activeDrag.pointerId)) {
|
||||
activeDrag.handle.releasePointerCapture(activeDrag.pointerId);
|
||||
}
|
||||
refresh(activeDrag.editor);
|
||||
activeDrag = null;
|
||||
}
|
||||
|
||||
document.addEventListener("pointerdown", function (event) {
|
||||
const handle = event.target.closest("[data-drag-handle]");
|
||||
if (!handle || (event.button !== undefined && event.button !== 0)) return;
|
||||
|
||||
const row = handle.closest("[data-order-item]");
|
||||
activeDrag = {
|
||||
editor: handle.closest("[data-order-editor]"),
|
||||
handle: handle,
|
||||
list: row.parentElement,
|
||||
pointerId: event.pointerId,
|
||||
row: row
|
||||
};
|
||||
handle.setPointerCapture(event.pointerId);
|
||||
row.classList.add("is-dragging");
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener("pointermove", function (event) {
|
||||
if (!activeDrag || event.pointerId !== activeDrag.pointerId) return;
|
||||
const hovered = document.elementFromPoint(event.clientX, event.clientY);
|
||||
const target = hovered && hovered.closest("[data-order-item]");
|
||||
if (!target || target === activeDrag.row || target.parentElement !== activeDrag.list) return;
|
||||
|
||||
const before = event.clientY < target.getBoundingClientRect().top + target.offsetHeight / 2;
|
||||
activeDrag.list.insertBefore(activeDrag.row, before ? target : target.nextElementSibling);
|
||||
refresh(activeDrag.editor);
|
||||
});
|
||||
|
||||
document.addEventListener("pointerup", finishDrag);
|
||||
document.addEventListener("pointercancel", finishDrag);
|
||||
|
||||
document.addEventListener("keydown", function (event) {
|
||||
const handle = event.target.closest("[data-drag-handle]");
|
||||
if (!handle || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
|
||||
|
||||
const row = handle.closest("[data-order-item]");
|
||||
const sibling = event.key === "ArrowUp" ? row.previousElementSibling : row.nextElementSibling;
|
||||
if (!sibling) return;
|
||||
if (event.key === "ArrowUp") row.parentElement.insertBefore(row, sibling);
|
||||
else row.parentElement.insertBefore(sibling, row);
|
||||
refresh(handle.closest("[data-order-editor]"));
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener("click", function (event) {
|
||||
const reset = event.target.closest("[data-order-reset]");
|
||||
if (!reset) return;
|
||||
|
||||
const editor = reset.closest("[data-order-editor]");
|
||||
const byId = new Map(rows(editor).map(function (row) { return [row.dataset.columnId, row]; }));
|
||||
const list = editor.querySelector("[data-order-list]");
|
||||
editor.dataset.originalOrder.split(",").forEach(function (id) { list.appendChild(byId.get(id)); });
|
||||
refresh(editor);
|
||||
});
|
||||
|
||||
initialise(document);
|
||||
document.body.addEventListener("htmx:afterSwap", function (event) {
|
||||
initialise(event.detail.target);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
{% include "pages/admin/table_definition/context.html" %}
|
||||
|
||||
@@ -5,54 +5,76 @@
|
||||
{% 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">
|
||||
<div class="presentation-layout">
|
||||
<section class="panel presentation-panel">
|
||||
<h2>{{ nav.tr("td-rename-column") }}</h2>
|
||||
<p class="hint">{{ nav.tr("td-rename-hint") }}</p>
|
||||
<div class="alias-list">
|
||||
<div class="alias-list-head" aria-hidden="true">
|
||||
<span>{{ nav.tr("td-current-alias") }}</span>
|
||||
<span></span>
|
||||
<span>{{ nav.tr("td-new-name") }}</span>
|
||||
<span></span>
|
||||
</div>
|
||||
{% for column in detail.columns %}
|
||||
{% if column.renameable %}
|
||||
<form hx-post="/admin/tables/presentation/alias"
|
||||
<form class="alias-row" hx-post="/admin/tables/presentation/alias"
|
||||
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 }}">
|
||||
<div class="form-grid">
|
||||
<label>{{ nav.tr("td-column-label") }}
|
||||
<input name="alias" value="{{ column.name }}">
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<code class="alias-current">{{ column.name }}</code>
|
||||
<span class="alias-arrow" aria-hidden="true">→</span>
|
||||
<input name="alias" aria-label="{{ nav.tr("td-new-name") }}: {{ column.name }}"
|
||||
placeholder="{{ nav.tr("td-new-name") }}" required>
|
||||
<button type="submit">{{ nav.tr("td-rename-button") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="hint"><code>{{ column.name }}</code> — {{ nav.tr("td-alias-locked") }}</p>
|
||||
<div class="alias-row alias-row-locked">
|
||||
<code class="alias-current">{{ column.name }}</code>
|
||||
<span class="locked-label">{{ nav.tr("td-alias-locked") }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</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">
|
||||
{# Ordering is staged in the browser and submitted as one id-only form.
|
||||
Aliases are still absent, so saving an order cannot rename anything. #}
|
||||
<section class="panel presentation-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"
|
||||
<form class="order-editor" data-order-editor
|
||||
data-clean-text="{{ nav.tr("td-order-clean") }}"
|
||||
data-dirty-text="{{ nav.tr("td-order-unsaved") }}"
|
||||
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>
|
||||
<ol class="column-order" data-order-list>
|
||||
{% for column in detail.columns %}
|
||||
<li data-order-item data-column-id="{{ column.column_id }}">
|
||||
<input type="hidden" name="column_ids" value="{{ column.column_id }}">
|
||||
<button class="drag-handle" type="button" data-drag-handle
|
||||
aria-label="{{ nav.tr("td-drag-column") }}: {{ column.name }}"
|
||||
title="{{ nav.tr("td-drag-column") }}">
|
||||
<span aria-hidden="true">⠿</span>
|
||||
</button>
|
||||
<span class="order-position" aria-hidden="true">{{ loop.index }}</span>
|
||||
<code class="order-name">{{ column.name }}</code>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
<div class="order-actions">
|
||||
<span class="order-status" data-order-status aria-live="polite">{{ nav.tr("td-order-clean") }}</span>
|
||||
<button class="secondary" type="button" data-order-reset disabled>{{ nav.tr("td-order-reset") }}</button>
|
||||
<button type="submit" data-order-save disabled>{{ nav.tr("td-order-save") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user