export works from web
This commit is contained in:
@@ -566,8 +566,10 @@ analytics-result-meta-truncated = { $rows ->
|
||||
transfer-eyebrow = Přenos dat
|
||||
import-title = Import CSV
|
||||
import-lead = Vyberte soubor v prohlížeči nebo vložte CSV. Řádky se ověřují proti živým strukturám tabulek před hromadným vkládáním přes gRPC.
|
||||
import-profile = Profil
|
||||
import-target-tables = Cílové tabulky
|
||||
import-scope = Rozsah
|
||||
import-choose-scope = Vyberte globální nebo profil
|
||||
import-target-tables = Cílová tabulka
|
||||
import-choose-table = Vyberte tabulku
|
||||
import-csv-file = Soubor CSV
|
||||
import-csv-data = Data CSV
|
||||
import-error-title = CSV se nepodařilo importovat
|
||||
@@ -586,11 +588,11 @@ import-success-message = Vloženo { $inserted ->
|
||||
[few] { $table_count } tabulkách
|
||||
*[other] { $table_count } tabulkách
|
||||
}.
|
||||
import-err-select-profile = Vyberte profil.
|
||||
import-err-tables-required = Zadejte alespoň jednu cílovou tabulku.
|
||||
import-err-select-profile = Vyberte rozsah.
|
||||
import-err-tables-required = Vyberte cílovou tabulku.
|
||||
import-err-csv-required = Vyberte soubor CSV nebo vložte data CSV.
|
||||
import-err-unknown-profile = Neznámý profil.
|
||||
import-err-tables-not-in-profile = Jedna nebo více cílových tabulek nepatří do profilu.
|
||||
import-err-unknown-profile = Neznámý rozsah.
|
||||
import-err-tables-not-in-profile = Cílová tabulka nepatří do vybraného rozsahu.
|
||||
import-err-missing-structure = Backend vynechal požadovanou strukturu tabulky.
|
||||
import-err-no-importable-columns = CSV nemá importovatelné sloupce pro tabulku '{ $table }'.
|
||||
import-err-column-not-importable = Sloupec '{ $column }' není importovatelný pro tabulku '{ $table }'.
|
||||
@@ -609,18 +611,18 @@ import-err-bad-integer = Neplatná celočíselná hodnota '{ $value }'
|
||||
|
||||
export-title = Export CSV
|
||||
export-lead = Soubor se generuje z živých dat tabulek přes gRPC.
|
||||
export-profile = Profil
|
||||
export-choose-profile = Vyberte profil
|
||||
export-tables = Tabulky
|
||||
export-comma-hint = Více tabulek oddělte čárkami.
|
||||
export-scope = Rozsah
|
||||
export-choose-scope = Vyberte globální nebo profil
|
||||
export-tables = Tabulka
|
||||
export-choose-table = Vyberte tabulku
|
||||
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-download = Stáhnout CSV
|
||||
export-err-select-profile = Vyberte profil.
|
||||
export-err-tables-required = Zadejte alespoň jednu tabulku.
|
||||
export-err-unknown-profile = Neznámý profil.
|
||||
export-err-tables-not-in-profile = Jedna nebo více tabulek nepatří do vybraného profilu.
|
||||
export-err-select-profile = Vyberte rozsah.
|
||||
export-err-tables-required = Vyberte tabulku.
|
||||
export-err-unknown-profile = Neznámý rozsah.
|
||||
export-err-tables-not-in-profile = Tabulka nepatří do vybraného rozsahu.
|
||||
export-err-missing-structure = Backend vynechal požadovanou strukturu tabulky.
|
||||
export-err-negative-count = Backend vrátil záporný počet řádků.
|
||||
export-err-range = Export přesahuje podporovaný rozsah pozic řádků.
|
||||
|
||||
@@ -556,8 +556,10 @@ analytics-result-meta-truncated = { $rows ->
|
||||
transfer-eyebrow = Data transfer
|
||||
import-title = Import CSV
|
||||
import-lead = Choose a browser-local file or paste CSV. Rows are validated against live table structures before gRPC bulk insertion.
|
||||
import-profile = Profile
|
||||
import-target-tables = Target tables
|
||||
import-scope = Scope
|
||||
import-choose-scope = Choose global or a profile
|
||||
import-target-tables = Target table
|
||||
import-choose-table = Choose a table
|
||||
import-csv-file = CSV file
|
||||
import-csv-data = CSV data
|
||||
import-error-title = Could not import CSV
|
||||
@@ -573,11 +575,11 @@ import-success-message = Inserted { $inserted ->
|
||||
[one] { $table_count } table
|
||||
*[other] { $table_count } tables
|
||||
}.
|
||||
import-err-select-profile = Select a profile.
|
||||
import-err-tables-required = Enter at least one target table.
|
||||
import-err-select-profile = Select a scope.
|
||||
import-err-tables-required = Select a target table.
|
||||
import-err-csv-required = Choose a CSV file or paste CSV data.
|
||||
import-err-unknown-profile = Unknown profile.
|
||||
import-err-tables-not-in-profile = One or more target tables do not belong to the profile.
|
||||
import-err-unknown-profile = Unknown scope.
|
||||
import-err-tables-not-in-profile = The target table does not belong to the selected scope.
|
||||
import-err-missing-structure = The backend omitted a requested table structure.
|
||||
import-err-no-importable-columns = CSV has no importable columns for table '{ $table }'.
|
||||
import-err-column-not-importable = Column '{ $column }' is not importable for table '{ $table }'.
|
||||
@@ -596,18 +598,18 @@ import-err-bad-integer = Invalid integer value '{ $value }'
|
||||
|
||||
export-title = Export CSV
|
||||
export-lead = The download is generated from live table data through gRPC.
|
||||
export-profile = Profile
|
||||
export-choose-profile = Choose a profile
|
||||
export-tables = Tables
|
||||
export-comma-hint = Separate multiple tables with commas.
|
||||
export-scope = Scope
|
||||
export-choose-scope = Choose global or a profile
|
||||
export-tables = Table
|
||||
export-choose-table = Choose a table
|
||||
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-download = Download CSV
|
||||
export-err-select-profile = Select a profile.
|
||||
export-err-tables-required = Enter at least one table.
|
||||
export-err-unknown-profile = Unknown profile.
|
||||
export-err-tables-not-in-profile = One or more tables do not belong to the selected profile.
|
||||
export-err-select-profile = Select a scope.
|
||||
export-err-tables-required = Select a table.
|
||||
export-err-unknown-profile = Unknown scope.
|
||||
export-err-tables-not-in-profile = The table does not belong to the selected scope.
|
||||
export-err-missing-structure = The backend omitted a requested table structure.
|
||||
export-err-negative-count = The backend returned a negative row count.
|
||||
export-err-range = The export exceeds the supported row-position range.
|
||||
|
||||
@@ -566,8 +566,10 @@ analytics-result-meta-truncated = { $rows ->
|
||||
transfer-eyebrow = Prenos údajov
|
||||
import-title = Import CSV
|
||||
import-lead = Vyberte súbor v prehliadači alebo vložte CSV. Riadky sa overujú proti živým štruktúram tabuliek pred hromadným vkladaním cez gRPC.
|
||||
import-profile = Profil
|
||||
import-target-tables = Cieľové tabuľky
|
||||
import-scope = Rozsah
|
||||
import-choose-scope = Vyberte globálne alebo profil
|
||||
import-target-tables = Cieľová tabuľka
|
||||
import-choose-table = Vyberte tabuľku
|
||||
import-csv-file = Súbor CSV
|
||||
import-csv-data = Údaje CSV
|
||||
import-error-title = CSV sa nepodarilo importovať
|
||||
@@ -586,11 +588,11 @@ import-success-message = Vložené { $inserted ->
|
||||
[few] { $table_count } tabuľkách
|
||||
*[other] { $table_count } tabuľkách
|
||||
}.
|
||||
import-err-select-profile = Vyberte profil.
|
||||
import-err-tables-required = Zadajte aspoň jednu cieľovú tabuľku.
|
||||
import-err-select-profile = Vyberte rozsah.
|
||||
import-err-tables-required = Vyberte cieľovú tabuľku.
|
||||
import-err-csv-required = Vyberte súbor CSV alebo vložte údaje CSV.
|
||||
import-err-unknown-profile = Neznámy profil.
|
||||
import-err-tables-not-in-profile = Jedna alebo viac cieľových tabuliek nepatrí do profilu.
|
||||
import-err-unknown-profile = Neznámy rozsah.
|
||||
import-err-tables-not-in-profile = Cieľová tabuľka nepatrí do vybraného rozsahu.
|
||||
import-err-missing-structure = Backend vynechal požadovanú štruktúru tabuľky.
|
||||
import-err-no-importable-columns = CSV nemá importovateľné stĺpce pre tabuľku '{ $table }'.
|
||||
import-err-column-not-importable = Stĺpec '{ $column }' nie je importovateľný pre tabuľku '{ $table }'.
|
||||
@@ -609,18 +611,18 @@ import-err-bad-integer = Neplatná celočíselná hodnota '{ $value }'
|
||||
|
||||
export-title = Export CSV
|
||||
export-lead = Súbor sa generuje z živých údajov tabuliek cez gRPC.
|
||||
export-profile = Profil
|
||||
export-choose-profile = Vyberte profil
|
||||
export-tables = Tabuľky
|
||||
export-comma-hint = Viacero tabuliek oddeľte čiarkami.
|
||||
export-scope = Rozsah
|
||||
export-choose-scope = Vyberte globálne alebo profil
|
||||
export-tables = Tabuľka
|
||||
export-choose-table = Vyberte tabuľku
|
||||
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-download = Stiahnuť CSV
|
||||
export-err-select-profile = Vyberte profil.
|
||||
export-err-tables-required = Zadajte aspoň jednu tabuľku.
|
||||
export-err-unknown-profile = Neznámy profil.
|
||||
export-err-tables-not-in-profile = Jedna alebo viac tabuliek nepatrí do vybraného profilu.
|
||||
export-err-select-profile = Vyberte rozsah.
|
||||
export-err-tables-required = Vyberte tabuľku.
|
||||
export-err-unknown-profile = Neznámy rozsah.
|
||||
export-err-tables-not-in-profile = Tabuľka nepatrí do vybraného rozsahu.
|
||||
export-err-missing-structure = Backend vynechal požadovanú štruktúru tabuľky.
|
||||
export-err-negative-count = Backend vrátil záporný počet riadkov.
|
||||
export-err-range = Export presahuje podporovaný rozsah pozícií riadkov.
|
||||
|
||||
@@ -3,7 +3,9 @@ use axum::http::HeaderMap;
|
||||
use crate::{
|
||||
AppState,
|
||||
auth::GetAuthorizationRequest,
|
||||
definitions::common::Empty,
|
||||
definitions::{common::Empty, table_definition::GetTableCatalogRequest},
|
||||
i18n::Locale,
|
||||
pages::{GLOBAL_SCOPE, table_scope},
|
||||
services::authenticated_request,
|
||||
};
|
||||
|
||||
@@ -12,8 +14,14 @@ pub(crate) struct Catalog {
|
||||
pub authorization: crate::auth::AuthorizationSnapshot,
|
||||
}
|
||||
|
||||
/// One entry of the scope selector: the global scope, or a profile.
|
||||
pub(crate) struct Profile {
|
||||
/// What the form posts, and what the backend resolves the table in —
|
||||
/// `__global` for the shared tables, the schema name otherwise.
|
||||
pub name: String,
|
||||
/// What the selector shows. The global scope is not a schema a user names,
|
||||
/// so it is labelled in their language instead.
|
||||
pub label: String,
|
||||
pub tables: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -45,35 +53,67 @@ pub(crate) async fn load_catalog(
|
||||
.await
|
||||
.map_err(|error| LoadError::Backend(error.message().to_string()))?
|
||||
.into_inner();
|
||||
let mut profiles = tree
|
||||
.profiles
|
||||
|
||||
// A global table is shared by every profile, so the tree repeats it under
|
||||
// each one. Listing it there would offer the same table once per profile
|
||||
// and make "the tables of this profile" mean two different things; it is
|
||||
// offered under the global scope instead, which — like every other page —
|
||||
// is read from the catalog rather than derived from the tree. See
|
||||
// `crate::pages::table_scope`.
|
||||
let global = table_scope::global_tables(
|
||||
&definitions
|
||||
.get_table_catalog(
|
||||
authenticated_request(headers, GetTableCatalogRequest { profile_name: None })
|
||||
.map_err(|_| LoadError::Unauthenticated)?,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| LoadError::Backend(error.message().to_string()))?
|
||||
.into_inner()
|
||||
.tables,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|table| table.name)
|
||||
.filter(|table| {
|
||||
crate::authz::permits_table(&authorization, GLOBAL_SCOPE, table, required_action)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let locale = Locale::from_headers(headers);
|
||||
let mut profiles = Vec::new();
|
||||
if !global.is_empty() {
|
||||
profiles.push(Profile {
|
||||
name: GLOBAL_SCOPE.to_string(),
|
||||
label: scope_label(locale, GLOBAL_SCOPE),
|
||||
tables: global,
|
||||
});
|
||||
}
|
||||
profiles.extend(tree.profiles.into_iter().filter_map(|profile| {
|
||||
let tables = profile
|
||||
.tables
|
||||
.into_iter()
|
||||
.filter_map(|profile| {
|
||||
let tables = profile
|
||||
.tables
|
||||
.into_iter()
|
||||
.filter(|table| {
|
||||
crate::authz::permits_table(
|
||||
&authorization,
|
||||
&profile.name,
|
||||
&table.name,
|
||||
required_action,
|
||||
)
|
||||
})
|
||||
.map(|table| table.name)
|
||||
.collect::<Vec<_>>();
|
||||
(!tables.is_empty()).then_some(Profile {
|
||||
name: profile.name,
|
||||
tables,
|
||||
})
|
||||
.filter(|table| !table.global)
|
||||
.filter(|table| {
|
||||
crate::authz::permits_table(
|
||||
&authorization,
|
||||
&profile.name,
|
||||
&table.name,
|
||||
required_action,
|
||||
)
|
||||
})
|
||||
.map(|table| table.name)
|
||||
.collect::<Vec<_>>();
|
||||
(!tables.is_empty()).then_some(Profile {
|
||||
label: profile.name.clone(),
|
||||
name: profile.name,
|
||||
tables,
|
||||
})
|
||||
}));
|
||||
|
||||
// An insert-only role is deliberately absent from GetProfileTree because
|
||||
// that listing is filtered by read permission. Exact table grants still
|
||||
// carry enough information to offer their target here. Wildcard-only
|
||||
// insert roles can type a target manually and the backend remains the
|
||||
// authoritative permission check.
|
||||
// carry enough information to offer their target here. A wildcard-only
|
||||
// insert role has no table name to offer, so its scope stays empty and the
|
||||
// backend remains the authoritative permission check.
|
||||
for permission in authorization
|
||||
.permissions
|
||||
.iter()
|
||||
@@ -98,6 +138,7 @@ pub(crate) async fn load_catalog(
|
||||
} else {
|
||||
profiles.push(Profile {
|
||||
name: profile_name.to_string(),
|
||||
label: scope_label(locale, profile_name),
|
||||
tables: vec![table_name.to_string()],
|
||||
});
|
||||
}
|
||||
@@ -108,6 +149,16 @@ pub(crate) async fn load_catalog(
|
||||
})
|
||||
}
|
||||
|
||||
/// A profile shows its schema name; the global scope is not one a user named,
|
||||
/// so it is labelled in their language.
|
||||
fn scope_label(locale: Locale, name: &str) -> String {
|
||||
if name == GLOBAL_SCOPE {
|
||||
crate::tr!(locale, "admin-global-label")
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum LoadError {
|
||||
Unauthenticated,
|
||||
Forbidden,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use axum::{
|
||||
Form,
|
||||
extract::State,
|
||||
http::{HeaderMap, HeaderValue, header},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
// The table checkboxes post `table_names` once per checked box, and
|
||||
// `axum::Form` (serde_urlencoded) cannot decode repeated keys into a `Vec`.
|
||||
use axum_extra::extract::Form;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
|
||||
@@ -4,8 +4,10 @@ use crate::{i18n::Locale, tr};
|
||||
pub(crate) struct ExportForm {
|
||||
#[serde(default)]
|
||||
pub profile_name: String,
|
||||
/// One entry per checked table. The form posts the key once per checked
|
||||
/// box, which only `axum_extra`'s `Form` decodes into a `Vec`.
|
||||
#[serde(default)]
|
||||
pub table_names: String,
|
||||
pub table_names: Vec<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)]
|
||||
@@ -25,8 +27,8 @@ impl ExportForm {
|
||||
}
|
||||
let tables = self
|
||||
.table_names
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.iter()
|
||||
.map(|table| table.trim())
|
||||
.filter(|table| !table.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use axum::{
|
||||
Form,
|
||||
extract::State,
|
||||
http::HeaderMap,
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
// The table checkboxes post `table_names` once per checked box, and
|
||||
// `axum::Form` (serde_urlencoded) cannot decode repeated keys into a `Vec`.
|
||||
use axum_extra::extract::Form;
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
|
||||
@@ -4,8 +4,10 @@ use crate::{i18n::Locale, tr};
|
||||
pub(crate) struct ImportForm {
|
||||
#[serde(default)]
|
||||
pub profile_name: String,
|
||||
/// One entry per checked table. The form posts the key once per checked
|
||||
/// box, which only `axum_extra`'s `Form` decodes into a `Vec`.
|
||||
#[serde(default)]
|
||||
pub table_names: String,
|
||||
pub table_names: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub csv_data: String,
|
||||
}
|
||||
@@ -25,8 +27,8 @@ impl ImportForm {
|
||||
}
|
||||
let tables = self
|
||||
.table_names
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.iter()
|
||||
.map(|table| table.trim())
|
||||
.filter(|table| !table.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -10,17 +10,21 @@
|
||||
{# A real form post, not HTMX: the response is a file download. #}
|
||||
<form method="post" action="/admin/export.csv">
|
||||
<div class="form-grid">
|
||||
<label>{{ nav.tr("export-profile") }}
|
||||
<select name="profile_name" required>
|
||||
<option value="">{{ nav.tr("export-choose-profile") }}</option>
|
||||
<label>{{ nav.tr("export-scope") }}
|
||||
<select name="profile_name" data-profile-select required>
|
||||
<option value="">{{ nav.tr("export-choose-scope") }}</option>
|
||||
{% for profile in page.catalog.profiles %}
|
||||
<option value="{{ profile.name }}">{{ profile.name }}</option>
|
||||
<option value="{{ profile.name }}">{{ profile.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>{{ nav.tr("export-tables") }}
|
||||
<input name="table_names" list="export-tables" required placeholder="invoices, customers">
|
||||
<small>{{ nav.tr("export-comma-hint") }}</small>
|
||||
<select name="table_names" data-table-select required>
|
||||
<option value="">{{ nav.tr("export-choose-table") }}</option>
|
||||
{%- for profile in page.catalog.profiles %}{% for table in profile.tables %}
|
||||
<option value="{{ table }}" data-profile="{{ profile.name }}">{{ table }}</option>
|
||||
{%- endfor %}{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label class="check">
|
||||
@@ -28,12 +32,10 @@
|
||||
{{ nav.tr("export-include-system") }}
|
||||
</label>
|
||||
<small>{{ nav.tr("export-include-system-hint") }}</small>
|
||||
<datalist id="export-tables">
|
||||
{%- for profile in page.catalog.profiles %}{% for table in profile.tables %}<option value="{{ table }}"></option>{% endfor %}{% endfor -%}
|
||||
</datalist>
|
||||
<div class="form-actions">
|
||||
<a href="/">{{ nav.tr("common-cancel") }}</a>
|
||||
<button type="submit">{{ nav.tr("export-download") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% include "pages/import_export/table_picker.html" %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -11,22 +11,28 @@
|
||||
<form hx-post="/admin/import" hx-target="#submission-status" hx-swap="innerHTML"
|
||||
hx-disabled-elt="button[type=submit]">
|
||||
<div class="form-grid">
|
||||
<label>{{ nav.tr("import-profile") }}
|
||||
<input name="profile_name" list="import-profiles" value="{{ page.form.profile_name }}" required placeholder="billing">
|
||||
<label>{{ nav.tr("import-scope") }}
|
||||
<select name="profile_name" data-profile-select required>
|
||||
<option value="">{{ nav.tr("import-choose-scope") }}</option>
|
||||
{% for profile in page.catalog.profiles %}
|
||||
<option value="{{ profile.name }}"{% if page.form.profile_name == profile.name %} selected{% endif %}>{{ profile.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>{{ nav.tr("import-target-tables") }}
|
||||
<select name="table_names" data-table-select required>
|
||||
<option value="">{{ nav.tr("import-choose-table") }}</option>
|
||||
{%- for profile in page.catalog.profiles %}{% for table in profile.tables %}
|
||||
<option value="{{ table }}" data-profile="{{ profile.name }}"{% if page.form.table_names.contains(table) %} selected{% endif %}>{{ table }}</option>
|
||||
{%- endfor %}{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>{{ nav.tr("import-target-tables") }}<input name="table_names" list="import-tables" value="{{ page.form.table_names }}" required placeholder="invoices, customers"></label>
|
||||
<label class="wide">{{ nav.tr("import-csv-file") }}
|
||||
<input type="file" accept=".csv,text/csv"
|
||||
onchange="this.files[0]?.text().then(value => document.getElementById('csv-data').value = value)">
|
||||
</label>
|
||||
<label class="wide">{{ nav.tr("import-csv-data") }}<textarea id="csv-data" name="csv_data" rows="14" required>{{ page.form.csv_data }}</textarea></label>
|
||||
</div>
|
||||
<datalist id="import-profiles">
|
||||
{% for profile in page.catalog.profiles %}<option value="{{ profile.name }}"></option>{% endfor %}
|
||||
</datalist>
|
||||
<datalist id="import-tables">
|
||||
{%- for profile in page.catalog.profiles %}{% for table in profile.tables %}<option value="{{ table }}"></option>{% endfor %}{% endfor -%}
|
||||
</datalist>
|
||||
<div id="submission-status" aria-live="polite">
|
||||
{%- if let Some(message) = page.error %}{% call alert::error(nav.locale, nav.tr("import-error-title"), message) %}{% endcall %}{% endif -%}
|
||||
</div>
|
||||
@@ -35,4 +41,5 @@
|
||||
<button type="submit">{{ nav.tr("import-import-rows") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% include "pages/import_export/table_picker.html" %}
|
||||
{% endblock %}
|
||||
|
||||
33
web/templates/pages/import_export/table_picker.html
Normal file
33
web/templates/pages/import_export/table_picker.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{#
|
||||
Leaves the table dropdown holding only the tables of the selected profile.
|
||||
|
||||
The options are rebuilt rather than hidden: `hidden` on an `<option>` is not
|
||||
honoured everywhere, and a hidden option is still selectable with the
|
||||
keyboard. With scripting off the dropdown lists every table it was rendered
|
||||
with, and the server still refuses a table that is not in the profile.
|
||||
#}
|
||||
<script>
|
||||
(function () {
|
||||
const profileSelect = document.querySelector("[data-profile-select]");
|
||||
const tableSelect = document.querySelector("[data-table-select]");
|
||||
if (!profileSelect || !tableSelect) return;
|
||||
const placeholder = tableSelect.options[0];
|
||||
const all = Array.from(tableSelect.options).slice(1);
|
||||
function sync() {
|
||||
const chosen = tableSelect.value;
|
||||
tableSelect.replaceChildren(placeholder);
|
||||
for (const option of all) {
|
||||
if (option.dataset.profile === profileSelect.value) {
|
||||
tableSelect.append(option);
|
||||
}
|
||||
}
|
||||
tableSelect.value = Array.from(tableSelect.options).some(
|
||||
(option) => option.value === chosen,
|
||||
)
|
||||
? chosen
|
||||
: "";
|
||||
}
|
||||
profileSelect.addEventListener("change", sync);
|
||||
sync();
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user