add record creation in ui
This commit is contained in:
parent
08b21ac010
commit
5f73738465
13 changed files with 253 additions and 146 deletions
49
assets/scripts/add-form-row.js
Normal file
49
assets/scripts/add-form-row.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
function templateString(text, args) {
|
||||||
|
for (const argName in args) {
|
||||||
|
text = text.replace(`{${argName}}`, args[argName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFormRow(templateName) {
|
||||||
|
const allRows = document.querySelectorAll(`[data-new-item-template="${templateName}"]`);
|
||||||
|
const templateElement = allRows[0];
|
||||||
|
const nextId = allRows.length + 1;
|
||||||
|
|
||||||
|
const newItem = templateElement.cloneNode(true);
|
||||||
|
|
||||||
|
newItem.querySelectorAll('[data-new-item-skip]')
|
||||||
|
.forEach(node => node.remove());
|
||||||
|
|
||||||
|
newItem.querySelectorAll('input')
|
||||||
|
.forEach(input => { input.value = '' });
|
||||||
|
|
||||||
|
const templatedAttrNodes = newItem.querySelectorAll('[data-new-item-template-attr]');
|
||||||
|
for (const node of templatedAttrNodes) {
|
||||||
|
const attributes = node.dataset.newItemTemplateAttr.split(/\s/);
|
||||||
|
for (const attribute of attributes) {
|
||||||
|
const templatedString = node.getAttribute(`data-template-${attribute}`);
|
||||||
|
node.setAttribute(attribute, templateString(templatedString, {i: nextId}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const templatedNodes = newItem.querySelectorAll('[data-new-item-template-content]');
|
||||||
|
for (const node of templatedNodes) {
|
||||||
|
const templatedString = node.dataset.newItemTemplateContent;
|
||||||
|
node.innerHTML = templateString(templatedString, {i: nextId})
|
||||||
|
}
|
||||||
|
|
||||||
|
allRows[allRows.length - 1].insertAdjacentElement('afterend', newItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUpAddFormRow() {
|
||||||
|
const buttons = document.querySelectorAll('button[data-new-item]');
|
||||||
|
|
||||||
|
for (const button of buttons) {
|
||||||
|
button.addEventListener('click', () => addFormRow(button.dataset.newItem))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', setUpAddFormRow);
|
|
@ -34,15 +34,13 @@ h2 {
|
||||||
|
|
||||||
article.domain {
|
article.domain {
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
}
|
|
||||||
|
|
||||||
article.domain header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 3em;
|
height: 3em;
|
||||||
}
|
|
||||||
|
|
||||||
article.domain header h3.folder-tab {
|
h3.folder-tab {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -53,81 +51,89 @@ article.domain header h3.folder-tab {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
}
|
|
||||||
|
|
||||||
article.domain header h3.folder-tab ~ .sep {
|
~ .sep {
|
||||||
content: '';
|
content: '';
|
||||||
width: 3em;
|
width: 3em;
|
||||||
background-color: #f2e0fd;
|
background-color: #f2e0fd;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
clip-path: url("#corner-folder-tab-right");
|
clip-path: url("#corner-folder-tab-right");
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records {
|
.records {
|
||||||
background: #f2e0fd;
|
background: #f2e0fd;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 0 .3rem .3rem .3rem;
|
border-radius: 0 .3rem .3rem .3rem;
|
||||||
}
|
|
||||||
|
|
||||||
article.domain .records h4 {
|
button,
|
||||||
|
a.button {
|
||||||
|
background-color: #f2e0fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records > ul {
|
> ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records .rrset .rtype {
|
.rrset {
|
||||||
|
.rtype {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records .rrset ul {
|
ul {
|
||||||
padding: 1rem 0 1rem 2rem;
|
padding: 1rem 0 1rem 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
article.domain .records .rrset li {
|
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records .rrset .rdata {
|
.rdata {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .2rem;
|
gap: .2rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records .rrset .rdata-main {
|
.rdata-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .3rem;
|
gap: .3rem;
|
||||||
margin-right: .1rem;
|
margin-right: .1rem;
|
||||||
}
|
|
||||||
|
|
||||||
article.domain .records .rrset .rdata-main .pill {
|
.pill {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
article.domain .records .rrset .rdata-complementary {
|
.rdata-complementary {
|
||||||
font-size: .9em;
|
font-size: .9em;
|
||||||
gap: .2rem;
|
gap: .2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records .rrset .action {
|
.action {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: .15rem;
|
top: .15rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
|
@ -151,36 +157,27 @@ a.button {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color .2s, color .2s;
|
transition: background-color .2s, color .2s;
|
||||||
}
|
|
||||||
|
|
||||||
button svg,
|
svg {
|
||||||
a.button svg {
|
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.records button,
|
&.icon {
|
||||||
.records a.button {
|
|
||||||
background-color: #f2e0fd;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.icon,
|
|
||||||
a.button.icon {
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 2em;
|
width: 2em;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover,
|
&:hover,
|
||||||
button:focus-visible,
|
&:focus-visible {
|
||||||
a.button:hover,
|
|
||||||
a.button:focus-visible {
|
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #850085;
|
background-color: #850085;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
form h3 {
|
form h3 {
|
||||||
margin-top: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
|
@ -208,7 +205,7 @@ textarea {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin-bottom: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row {
|
.form-row {
|
||||||
|
@ -217,3 +214,23 @@ textarea {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-action {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.form-new-item {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
text-decoration: underline;
|
||||||
|
border-radius: 0;
|
||||||
|
color: #850085;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
background: none;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #850085;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -37,3 +37,4 @@ record-input-addresses =
|
||||||
address, like <code>2001:db8:cafe:bc68::2</code>.
|
address, like <code>2001:db8:cafe:bc68::2</code>.
|
||||||
|
|
||||||
button-save-configuration = Save configuration
|
button-save-configuration = Save configuration
|
||||||
|
button-add-address = Add an other address
|
||||||
|
|
|
@ -32,8 +32,9 @@ zone-content-new-record-button = Nouvel enregistrement
|
||||||
|
|
||||||
record-input-addresses =
|
record-input-addresses =
|
||||||
.input-label = Adresse IP #{ $index }
|
.input-label = Adresse IP #{ $index }
|
||||||
.error-record-parse-ip = Format d'adresse IP inconnu. L'adresse IP doit être
|
.error-record-parse-ip = Format d’adresse IP inconnu. L’adresse IP doit être
|
||||||
soit une adresse IPv4, comme <code>198.51.100.3</code>, soit une adresse IPv6,
|
soit une adresse IPv4, comme <code>198.51.100.3</code>, soit une adresse IPv6,
|
||||||
comme <code>2001:db8:cafe:bc68::2</code>.
|
comme <code>2001:db8:cafe:bc68::2</code>.
|
||||||
|
|
||||||
button-save-configuration = Sauvegarder la configuration
|
button-save-configuration = Sauvegarder la configuration
|
||||||
|
button-add-address = Ajouter une autre adresse
|
||||||
|
|
|
@ -111,19 +111,19 @@ impl Node {
|
||||||
}).collect()
|
}).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json_value(self) -> serde_json::Value {
|
pub fn into_json_value(self) -> serde_json::Value {
|
||||||
match self {
|
match self {
|
||||||
Node::Value(value) => serde_json::Value::String(value),
|
Node::Value(value) => serde_json::Value::String(value),
|
||||||
Node::Map(map) => {
|
Node::Map(map) => {
|
||||||
let map = map.into_iter()
|
let map = map.into_iter()
|
||||||
.map(|(key, node)| (key, node.to_json_value()))
|
.map(|(key, node)| (key, node.into_json_value()))
|
||||||
.collect();
|
.collect();
|
||||||
serde_json::Value::Object(map)
|
serde_json::Value::Object(map)
|
||||||
},
|
},
|
||||||
Node::Sequence(list) => {
|
Node::Sequence(list) => {
|
||||||
let array = list.to_vec()
|
let array = list.to_vec()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|node| node.to_json_value())
|
.map(|node| node.into_json_value())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
serde_json::Value::Array(array)
|
serde_json::Value::Array(array)
|
||||||
|
@ -245,6 +245,6 @@ mod tests {
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(parsed_form.to_json_value(), json_value);
|
assert_eq!(parsed_form.into_json_value(), json_value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -458,6 +458,5 @@ impl TryFrom<internal::Record> for RecordImpl {
|
||||||
let ttl = Ttl::from_secs(value.ttl);
|
let ttl = Ttl::from_secs(value.ttl);
|
||||||
let data = value.rdata.try_into()?;
|
let data = value.rdata.try_into()?;
|
||||||
Ok(Record::new(owner, Class::IN, ttl, data))
|
Ok(Record::new(owner, Class::IN, ttl, data))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::validation;
|
use crate::validation;
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
use crate::macros::{append_errors, check_type, get_object_value, push_error};
|
use crate::macros::{append_errors, check_type, get_object_value, push_error};
|
||||||
use crate::resources::dns::internal::{self, Name};
|
use crate::resources::dns::internal::{self, Name, Record};
|
||||||
|
|
||||||
use super::{rdata, FriendlyRType};
|
use super::{rdata, FriendlyRType};
|
||||||
use super::FromValue;
|
use super::FromValue;
|
||||||
|
@ -16,6 +16,21 @@ pub enum ConfigurationType {
|
||||||
Web,
|
Web,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ConfigurationType {
|
||||||
|
pub fn get_records(&self, input_data: serde_json::Value, default_ttl: u32, zone_name: Name) -> Result<Vec<Record>, Vec<Error>> {
|
||||||
|
match self {
|
||||||
|
ConfigurationType::Mail => {
|
||||||
|
NewSectionMail::from_value(input_data.clone())
|
||||||
|
.map(|section| section.internal(default_ttl, zone_name))
|
||||||
|
},
|
||||||
|
ConfigurationType::Web => {
|
||||||
|
NewSectionWeb::from_value(input_data.clone())
|
||||||
|
.map(|section| section.internal(default_ttl, zone_name))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct NewRecordQuery {
|
pub struct NewRecordQuery {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
@ -59,7 +74,7 @@ pub struct NewRecord<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: FromValue> FromValue for NewRecord<T> {
|
impl<T: FromValue> FromValue for NewRecord<T> {
|
||||||
fn from_value(value: tera::Value) -> Result<Self, Vec<crate::errors::Error>> {
|
fn from_value(value: serde_json::Value) -> Result<Self, Vec<crate::errors::Error>> {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
let object = check_type!(value, Object, errors);
|
let object = check_type!(value, Object, errors);
|
||||||
|
@ -113,7 +128,7 @@ pub struct NewRequiredRecord<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: FromValue> FromValue for NewRequiredRecord<T> {
|
impl<T: FromValue> FromValue for NewRequiredRecord<T> {
|
||||||
fn from_value(value: tera::Value) -> Result<Self, Vec<crate::errors::Error>> {
|
fn from_value(value: serde_json::Value) -> Result<Self, Vec<crate::errors::Error>> {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
let object = check_type!(value, Object, errors);
|
let object = check_type!(value, Object, errors);
|
||||||
|
|
|
@ -304,11 +304,10 @@ impl FromValue for Spf {
|
||||||
|
|
||||||
impl ToInternal for Spf {
|
impl ToInternal for Spf {
|
||||||
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record> {
|
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record> {
|
||||||
|
|
||||||
vec![
|
vec![
|
||||||
internal::Record {
|
internal::Record {
|
||||||
ttl,
|
ttl,
|
||||||
name: Name::new(format!("_spf.{node_name}")),
|
name: node_name,
|
||||||
rdata: internal::RData::Txt(internal::Txt::new(self.policy))
|
rdata: internal::RData::Txt(internal::Txt::new(self.policy))
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -14,6 +14,9 @@ pub struct RecordList {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordList {
|
impl RecordList {
|
||||||
|
pub fn new(records: &[Record]) -> Self {
|
||||||
|
RecordList { records: records.into() }
|
||||||
|
}
|
||||||
pub fn sort(&mut self) {
|
pub fn sort(&mut self) {
|
||||||
self.records.sort_by(|r1, r2| {
|
self.records.sort_by(|r1, r2| {
|
||||||
let key1 = (&r1.name, r1.rdata.rtype());
|
let key1 = (&r1.name, r1.rdata.rtype());
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use axum::response::Response;
|
||||||
|
use domain::base::record;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use rusqlite::Error as RusqliteError;
|
use rusqlite::Error as RusqliteError;
|
||||||
|
|
||||||
|
@ -7,7 +9,7 @@ use crate::database::{BoxedDb, sqlite::SqliteDB};
|
||||||
use crate::dns::{BoxedZoneDriver, BoxedRecordDriver, DnsDriverError};
|
use crate::dns::{BoxedZoneDriver, BoxedRecordDriver, DnsDriverError};
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
use crate::macros::push_error;
|
use crate::macros::push_error;
|
||||||
use crate::resources::dns::internal::RecordList;
|
use crate::resources::dns::internal::{self, RecordList};
|
||||||
use crate::validation;
|
use crate::validation;
|
||||||
|
|
||||||
pub enum ZoneError {
|
pub enum ZoneError {
|
||||||
|
@ -48,6 +50,12 @@ impl Zone {
|
||||||
|
|
||||||
Ok(records)
|
Ok(records)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn add_records(&self, record_driver: BoxedRecordDriver, records: internal::RecordList) -> Result<(), Error> {
|
||||||
|
record_driver.add_records(&self.name, records).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
use axum::extract::{Query, Path, State, OriginalUri};
|
use axum::extract::{Query, Path, State, OriginalUri};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use unic_langid::LanguageIdentifier;
|
use unic_langid::LanguageIdentifier;
|
||||||
|
@ -8,7 +10,7 @@ use crate::macros::append_errors;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::errors::{Error, error_map};
|
use crate::errors::{Error, error_map};
|
||||||
use crate::template::Template;
|
use crate::template::Template;
|
||||||
use crate::resources::dns::friendly::{self, NewRecordQuery, ConfigurationType, FromValue, NewSectionMail, NewSectionWeb, ToInternal};
|
use crate::resources::dns::friendly::{self, NewRecordQuery};
|
||||||
use crate::resources::dns::internal;
|
use crate::resources::dns::internal;
|
||||||
|
|
||||||
pub async fn get_records_page(
|
pub async fn get_records_page(
|
||||||
|
@ -68,33 +70,21 @@ pub async fn post_new_record(
|
||||||
OriginalUri(url): OriginalUri,
|
OriginalUri(url): OriginalUri,
|
||||||
Extension(lang): Extension<LanguageIdentifier>,
|
Extension(lang): Extension<LanguageIdentifier>,
|
||||||
form: Node,
|
form: Node,
|
||||||
) -> Result<Template<'static, Value>, Error> {
|
) -> Result<Response, Error> {
|
||||||
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
append_errors!(params.validate(&zone.name), errors);
|
append_errors!(params.validate(&zone.name), errors);
|
||||||
|
|
||||||
if !errors.is_empty() || params.name.is_none() || !(params.config.is_none() ^ params.config.is_some()) {
|
if !errors.is_empty() || params.name.is_none() || !(params.config.is_none() ^ params.config.is_some()) {
|
||||||
// TODO: return 404
|
return Ok(StatusCode::NOT_FOUND.into_response());
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = params.name.clone().unwrap();
|
let name = params.name.clone().map(internal::Name::new).unwrap();
|
||||||
let input_data = form.to_json_value();
|
let input_data = form.into_json_value();
|
||||||
|
|
||||||
let new_records = if errors.is_empty() {
|
|
||||||
let name = internal::Name::new(name);
|
|
||||||
|
|
||||||
|
let new_records = {
|
||||||
let new_records = if let Some(config_type) = params.config.clone() {
|
let new_records = if let Some(config_type) = params.config.clone() {
|
||||||
match config_type {
|
config_type.get_records(input_data.clone(), 3600, name)
|
||||||
ConfigurationType::Mail => {
|
|
||||||
NewSectionMail::from_value(input_data.clone())
|
|
||||||
.map(|section| section.internal(3600, name))
|
|
||||||
},
|
|
||||||
ConfigurationType::Web => {
|
|
||||||
NewSectionWeb::from_value(input_data.clone())
|
|
||||||
.map(|section| section.internal(3600, name))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else if let Some(_rtype) = params.rtype {
|
} else if let Some(_rtype) = params.rtype {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
} else {
|
} else {
|
||||||
|
@ -102,11 +92,16 @@ pub async fn post_new_record(
|
||||||
};
|
};
|
||||||
|
|
||||||
append_errors!(new_records, errors)
|
append_errors!(new_records, errors)
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if !errors.is_empty() {
|
if let Some(new_records) = new_records {
|
||||||
|
zone.add_records(
|
||||||
|
app.records,
|
||||||
|
internal::RecordList::new(&new_records)
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok(().into_response())
|
||||||
|
} else {
|
||||||
Ok(Template::new(
|
Ok(Template::new(
|
||||||
"pages/new_record.html",
|
"pages/new_record.html",
|
||||||
app.template_engine,
|
app.template_engine,
|
||||||
|
@ -120,10 +115,7 @@ pub async fn post_new_record(
|
||||||
"url": url.to_string(),
|
"url": url.to_string(),
|
||||||
"lang": lang.to_string(),
|
"lang": lang.to_string(),
|
||||||
})
|
})
|
||||||
))
|
).into_response())
|
||||||
} else {
|
|
||||||
println!("{:#?}", new_records);
|
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,3 +12,7 @@
|
||||||
{% block main %}{% endblock main %}
|
{% block main %}{% endblock main %}
|
||||||
</main>
|
</main>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="/assets/scripts/add-form-row.js"></script>
|
||||||
|
{% endblock scripts %}
|
||||||
|
|
|
@ -14,19 +14,27 @@
|
||||||
|
|
||||||
{% for address in input_data.addresses.data.addresses | default(value=[""]) %}
|
{% for address in input_data.addresses.data.addresses | default(value=[""]) %}
|
||||||
{% set address_error = errors | get(key="/addresses/data/addresses/" ~ loop.index0 ~ "/address", default="") %}
|
{% set address_error = errors | get(key="/addresses/data/addresses/" ~ loop.index0 ~ "/address", default="") %}
|
||||||
<div class="form-input">
|
<div class="form-input" data-new-item-template="address">
|
||||||
<label for="address-{{ loop.index0 }}">
|
<label
|
||||||
|
for="address-{{ loop.index0 }}"
|
||||||
|
data-new-item-template-attr="for"
|
||||||
|
data-template-for="address-{i}"
|
||||||
|
data-new-item-template-content="{{ tr(msg="record-input-addresses", attr="input-label", index="{i}", lang=lang) }}"
|
||||||
|
>
|
||||||
{{ tr(msg="record-input-addresses", attr="input-label", index=loop.index, lang=lang) }}
|
{{ tr(msg="record-input-addresses", attr="input-label", index=loop.index, lang=lang) }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="addresses[data][addresses][{{ loop.index0 }}][address]"
|
name="addresses[data][addresses][{{ loop.index0 }}][address]"
|
||||||
id="address-{{ loop.index0 }}"
|
id="address-{{ loop.index0 }}"
|
||||||
|
data-new-item-template-attr="name id"
|
||||||
|
data-template-name="addresses[data][addresses][{i}][address]"
|
||||||
|
data-template-id="address-{i}"
|
||||||
{% if domain_error %}aria-invalid="true"{% endif %}
|
{% if domain_error %}aria-invalid="true"{% endif %}
|
||||||
value="{{ address.address | default(value="") }}"
|
value="{{ address.address | default(value="") }}"
|
||||||
>
|
>
|
||||||
{% if address_error %}
|
{% if address_error %}
|
||||||
<p class="error" id="address-{{ loop.index0 }}-error">
|
<p class="error" id="address-{{ loop.index0 }}-error" data-new-item-skip>
|
||||||
{{ tr(
|
{{ tr(
|
||||||
msg="record-input-addresses",
|
msg="record-input-addresses",
|
||||||
attr="error-" ~ address_error.code | replace(from=":", to="-"),
|
attr="error-" ~ address_error.code | replace(from=":", to="-"),
|
||||||
|
@ -34,9 +42,18 @@
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<button class="form-new-item" type="button" data-new-item="address">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-circle" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
|
||||||
|
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
|
||||||
|
</svg>
|
||||||
|
{{ tr(msg="button-add-address", lang=lang) }}
|
||||||
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="form-action">
|
||||||
<button type="submit">{{ tr(msg="button-save-configuration", lang=lang) }}</button>
|
<button type="submit">{{ tr(msg="button-save-configuration", lang=lang) }}</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% elif config == "mail" %}
|
{% elif config == "mail" %}
|
||||||
|
@ -104,6 +121,8 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
|
||||||
<button type="submit">Save configuration</button>
|
<div class="form-action">
|
||||||
|
<button type="submit">{{ tr(msg="button-save-configuration", lang=lang) }}</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
Loading…
Reference in a new issue