refactor: break up web/contact
This commit is contained in:
parent
2e1fbd00be
commit
57177612ec
8 changed files with 166 additions and 121 deletions
|
|
@ -1,4 +1,4 @@
|
|||
on: [push, workflow_dispatch]
|
||||
on: [workflow_dispatch]
|
||||
jobs:
|
||||
integration-test--firefox:
|
||||
runs-on: playwright-latest
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ name = "mascarpone"
|
|||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
axum = { version = "0.8.6", features = ["macros", "form"] }
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ playwright:ui() {
|
|||
--env DISPLAY="$DISPLAY" \
|
||||
--volume /tmp/.X11-unix:/tmp/.X11-unix \
|
||||
--volume "$SCRIPT_DIR":/e2e:rw --env BASE_URL="$BASE_URL" \
|
||||
--env ASTRO_TELEMETRY_DISABLED=1 \
|
||||
"mcr.microsoft.com/playwright:$(_playwright_version)" \
|
||||
/bin/bash -c "cd /e2e && ./Taskfile _test --ui $*"
|
||||
}
|
||||
|
|
@ -31,7 +30,6 @@ playwright:ci() {
|
|||
exec docker run \
|
||||
--interactive --tty --rm --ipc=host --net=host \
|
||||
--volume "$SCRIPT_DIR":/e2e:rw --env BASE_URL="$BASE_URL" \
|
||||
--env ASTRO_TELEMETRY_DISABLED=1 \
|
||||
"mcr.microsoft.com/playwright:$(_playwright_version)" \
|
||||
bash -c "cd /e2e && ./Taskfile _test $*"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "playwright test --project=firefox && playwright test"
|
||||
"test": "echo use Taskfile instead"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ type UserFields = {
|
|||
export const verifyCreateUser = async (page: Page, fields: UserFields) => {
|
||||
await page.getByRole('button', { name: /add contact/i }).click();
|
||||
|
||||
// TODO this is stupid but playwright kept filling while alpine was initializing
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const { names, ...simple } = fields;
|
||||
for (const name of (names ?? [])) {
|
||||
await page.getByRole('textbox', { name: 'New name' }).fill(name);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ impl JournalEntry {
|
|||
let date = self.date.to_string();
|
||||
|
||||
Ok(html! {
|
||||
.entry {
|
||||
.entry hx-target="this" {
|
||||
.view ":class"="{ hide: edit }" {
|
||||
.date { (date) }
|
||||
.content { (rendered) }
|
||||
|
|
@ -43,7 +43,6 @@ impl JournalEntry {
|
|||
textarea name="value" x-model="value" {}
|
||||
button title="Delete"
|
||||
hx-delete=(entry_url)
|
||||
hx-target="closest .entry"
|
||||
hx-swap="delete" {
|
||||
svg .icon xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" {
|
||||
path d="M232.7 69.9C237.1 56.8 249.3 48 263.1 48L377 48C390.8 48 403 56.8 407.4 69.9L416 96L512 96C529.7 96 544 110.3 544 128C544 145.7 529.7 160 512 160L128 160C110.3 160 96 145.7 96 128C96 110.3 110.3 96 128 96L224 96L232.7 69.9zM128 208L512 208L512 512C512 547.3 483.3 576 448 576L192 576C156.7 576 128 547.3 128 512L128 208zM216 272C202.7 272 192 282.7 192 296L192 488C192 501.3 202.7 512 216 512C229.3 512 240 501.3 240 488L240 296C240 282.7 229.3 272 216 272zM320 272C306.7 272 296 282.7 296 296L296 488C296 501.3 306.7 512 320 512C333.3 512 344 501.3 344 488L344 296C344 282.7 333.3 272 320 272zM424 272C410.7 272 400 282.7 400 296L400 488C400 501.3 410.7 512 424 512C437.3 512 448 501.3 448 488L448 296C448 282.7 437.3 272 424 272z";
|
||||
|
|
@ -52,7 +51,6 @@ impl JournalEntry {
|
|||
button x-bind:disabled="(date === initial_date) && (value === initial_value)"
|
||||
x-on:click="initial_date = date; initial_value = value"
|
||||
hx-patch=(entry_url)
|
||||
hx-target="closest .entry"
|
||||
hx-swap="outerHTML"
|
||||
title="Save" { "✓" }
|
||||
button x-bind:disabled="(date === initial_date) && (value === initial_value)"
|
||||
|
|
|
|||
131
src/web/contact/fields.rs
Normal file
131
src/web/contact/fields.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
use maud::{Markup, html};
|
||||
use serde_json::json;
|
||||
use sqlx::sqlite::SqlitePool;
|
||||
|
||||
use crate::AppError;
|
||||
use crate::db::DbId;
|
||||
|
||||
pub mod addresses {
|
||||
use super::*;
|
||||
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub struct Address {
|
||||
pub id: DbId,
|
||||
pub contact_id: DbId,
|
||||
pub label: Option<String>,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
async fn all(pool: &SqlitePool, contact_id: DbId) -> Result<Vec<Address>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Address,
|
||||
"select * from addresses where contact_id = $1",
|
||||
contact_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get(pool: &SqlitePool, contact_id: DbId) -> Result<Markup, AppError> {
|
||||
let addresses: Vec<Address> = addresses::all(pool, contact_id).await?;
|
||||
|
||||
Ok(html! {
|
||||
@if addresses.len() == 1 {
|
||||
label { "address" }
|
||||
#addresses {
|
||||
.label {}
|
||||
.value { (addresses[0].value) }
|
||||
}
|
||||
} @else if addresses.len() > 0 {
|
||||
label { "addresses" }
|
||||
#addresses {
|
||||
@for address in addresses {
|
||||
@let lbl = address.label.unwrap_or(String::new());
|
||||
.label data-is-empty=(lbl.len() == 0) {
|
||||
(lbl)
|
||||
}
|
||||
.value { (address.value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn edit(pool: &SqlitePool, contact_id: DbId) -> Result<Markup, AppError> {
|
||||
let addresses: Vec<Address> = addresses::all(pool, contact_id).await?;
|
||||
|
||||
Ok(html! {
|
||||
label { "addresses" }
|
||||
div x-data=(json!({ "addresses": addresses, "new_label": "", "new_address": "" })) {
|
||||
template x-for="(address, index) in addresses" x-bind:key="index" {
|
||||
.address-input {
|
||||
input name="address_label" x-show="addresses.length" x-model="address.label" placeholder="label";
|
||||
.grow-wrap x-bind:data-replicated-value="address.value" {
|
||||
textarea name="address_value" x-model="address.value" placeholder="address" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
.address-input {
|
||||
input x-show="addresses.length" name="address_label" x-model="new_label" placeholder="label";
|
||||
.grow-wrap x-bind:data-replicated-value="new_address" {
|
||||
textarea name="address_value" x-model="new_address" placeholder="new address" {}
|
||||
}
|
||||
}
|
||||
input type="button" value="Add" x-on:click="addresses.push({ label: new_label, value: new_address }); new_label = ''; new_address = ''";
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub mod groups {
|
||||
use super::*;
|
||||
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub struct Group {
|
||||
pub contact_id: DbId,
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
}
|
||||
|
||||
async fn all(pool: &SqlitePool, contact_id: DbId) -> Result<Vec<Group>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Group,
|
||||
"select * from groups where contact_id = $1",
|
||||
contact_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get(pool: &SqlitePool, contact_id: DbId) -> Result<Markup, AppError> {
|
||||
let groups: Vec<Group> = groups::all(pool, contact_id).await?;
|
||||
|
||||
Ok(html! {
|
||||
@if groups.len() > 0 {
|
||||
label { "in groups" }
|
||||
#groups {
|
||||
@for group in groups {
|
||||
a .group href=(format!("/group/{}", group.slug)) {
|
||||
(group.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn edit(pool: &SqlitePool, contact_id: DbId) -> Result<Markup, AppError> {
|
||||
let groups: Vec<Group> = groups::all(pool, contact_id).await?;
|
||||
|
||||
Ok(html! {
|
||||
label { "groups" }
|
||||
#groups x-data=(json!({ "groups": groups, "new_group": "" })) {
|
||||
template x-for="(group, index) in groups" x-bind:key="index" {
|
||||
input name="group" x-model="group.name" placeholder="group name";
|
||||
}
|
||||
input name="group" x-model="new_group" placeholder="group name";
|
||||
input type="button" value="Add" x-on:click="groups.push({ name: new_group }); new_group = ''";
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -22,20 +22,7 @@ use crate::models::{HydratedContact, JournalEntry};
|
|||
use crate::switchboard::{MentionHost, MentionHostType, insert_mentions};
|
||||
use crate::{AppError, AppState};
|
||||
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub struct Address {
|
||||
pub id: DbId,
|
||||
pub contact_id: DbId,
|
||||
pub label: Option<String>,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub struct Group {
|
||||
pub contact_id: DbId,
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
}
|
||||
pub mod fields;
|
||||
|
||||
#[derive(serde::Serialize, Debug)]
|
||||
pub struct PhoneNumber {
|
||||
|
|
@ -104,7 +91,9 @@ mod get {
|
|||
PhoneNumber,
|
||||
"select * from phone_numbers where contact_id = $1",
|
||||
contact_id
|
||||
).fetch_all(pool).await?;
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let lives_with = if contact.lives_with.len() > 1 {
|
||||
let mention_host = MentionHost {
|
||||
|
|
@ -117,22 +106,6 @@ mod get {
|
|||
None
|
||||
};
|
||||
|
||||
let addresses: Vec<Address> = sqlx::query_as!(
|
||||
Address,
|
||||
"select * from addresses where contact_id = $1",
|
||||
contact_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let groups: Vec<Group> = sqlx::query_as!(
|
||||
Group,
|
||||
"select * from groups where contact_id = $1",
|
||||
contact_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let text_body: Option<String> =
|
||||
sqlx::query!("select text_body from contacts where id = $1", contact_id)
|
||||
.fetch_one(pool)
|
||||
|
|
@ -193,35 +166,8 @@ mod get {
|
|||
div { (lives_with) }
|
||||
}
|
||||
|
||||
@if addresses.len() == 1 {
|
||||
label { "address" }
|
||||
#addresses {
|
||||
.label {}
|
||||
.value { (addresses[0].value) }
|
||||
}
|
||||
} @else if addresses.len() > 0 {
|
||||
label { "addresses" }
|
||||
#addresses {
|
||||
@for address in addresses {
|
||||
@let lbl = address.label.unwrap_or(String::new());
|
||||
.label data-is-empty=(lbl.len() == 0) {
|
||||
(lbl)
|
||||
}
|
||||
.value { (address.value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@if groups.len() > 0 {
|
||||
label { "in groups" }
|
||||
#groups {
|
||||
@for group in groups {
|
||||
a .group href=(format!("/group/{}", group.slug)) {
|
||||
(group.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(fields::addresses::get(pool, contact_id).await?)
|
||||
(fields::groups::get(pool, contact_id).await?)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -253,12 +199,6 @@ mod get {
|
|||
PhoneNumber,
|
||||
"select * from phone_numbers where contact_id = $1",
|
||||
contact_id
|
||||
).fetch_all(pool).await?;
|
||||
|
||||
let addresses: Vec<Address> = sqlx::query_as!(
|
||||
Address,
|
||||
"select * from addresses where contact_id = $1",
|
||||
contact_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
|
@ -269,17 +209,6 @@ mod get {
|
|||
.clone()
|
||||
.map_or("".to_string(), |m| m.to_rfc3339());
|
||||
|
||||
let groups: Vec<String> = sqlx::query_as!(
|
||||
Group,
|
||||
"select * from groups where contact_id = $1",
|
||||
contact_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|group| group.name)
|
||||
.collect();
|
||||
|
||||
let text_body: String =
|
||||
sqlx::query!("select text_body from contacts where id = $1", contact_id)
|
||||
.fetch_one(pool)
|
||||
|
|
@ -340,32 +269,8 @@ mod get {
|
|||
div {
|
||||
input name="lives_with" value=(contact.lives_with);
|
||||
}
|
||||
label { "addresses" }
|
||||
div x-data=(json!({ "addresses": addresses, "new_label": "", "new_address": "" })) {
|
||||
template x-for="(address, index) in addresses" x-bind:key="index" {
|
||||
.address-input {
|
||||
input name="address_label" x-show="addresses.length" x-model="address.label" placeholder="label";
|
||||
.grow-wrap x-bind:data-replicated-value="address.value" {
|
||||
textarea name="address_value" x-model="address.value" placeholder="address" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
.address-input {
|
||||
input x-show="addresses.length" name="address_label" x-model="new_label" placeholder="label";
|
||||
.grow-wrap x-bind:data-replicated-value="new_address" {
|
||||
textarea name="address_value" x-model="new_address" placeholder="new address" {}
|
||||
}
|
||||
}
|
||||
input type="button" value="Add" x-on:click="addresses.push({ label: new_label, value: new_address }); new_label = ''; new_address = ''";
|
||||
}
|
||||
label { "groups" }
|
||||
#groups x-data=(json!({ "groups": groups, "new_group": "" })) {
|
||||
template x-for="(group, index) in groups" x-bind:key="index" {
|
||||
input name="group" x-model="group" placeholder="group name";
|
||||
}
|
||||
input name="group" x-model="new_group" placeholder="group name";
|
||||
input type="button" value="Add" x-on:click="groups.push(new_group); new_group = ''";
|
||||
}
|
||||
(fields::addresses::edit(pool, contact_id).await?)
|
||||
(fields::groups::edit(pool, contact_id).await?)
|
||||
}
|
||||
div #text_body {
|
||||
div { "Free text (supports markdown)" }
|
||||
|
|
@ -390,7 +295,7 @@ mod post {
|
|||
let user = auth_session.user.unwrap();
|
||||
let pool = &state.db(&user).pool;
|
||||
|
||||
let contact_id: (u32,) =
|
||||
let contact_id: (DbId,) =
|
||||
sqlx::query_as("insert into contacts (birthday) values (null) returning id")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
|
@ -534,21 +439,27 @@ mod put {
|
|||
.collect::<Vec<(String, String)>>()
|
||||
});
|
||||
|
||||
let old_numbers: Vec<(String, String)> =
|
||||
sqlx::query_as("select label, phone_number from phone_numbers where contact_id = $1")
|
||||
.bind(contact_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
let old_numbers: Vec<(String, String)> = sqlx::query_as(
|
||||
"select label, phone_number from phone_numbers where contact_id = $1",
|
||||
)
|
||||
.bind(contact_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
if new_numbers != old_numbers {
|
||||
sqlx::query!("delete from phone_numbers where contact_id = $1", contact_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"delete from phone_numbers where contact_id = $1",
|
||||
contact_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
// trailing space in query intentional
|
||||
QueryBuilder::new("insert into phone_numbers (contact_id, label, phone_number) ")
|
||||
.push_values(new_numbers, |mut b, (label, phone_number)| {
|
||||
b.push_bind(contact_id).push_bind(label).push_bind(phone_number);
|
||||
b.push_bind(contact_id)
|
||||
.push_bind(label)
|
||||
.push_bind(phone_number);
|
||||
})
|
||||
.build()
|
||||
.execute(pool)
|
||||
|
|
@ -743,7 +654,7 @@ mod delete {
|
|||
pub async fn contact(
|
||||
auth_session: AuthSession,
|
||||
State(state): State<AppState>,
|
||||
Path(contact_id): Path<u32>,
|
||||
Path(contact_id): Path<DbId>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let user = auth_session.user.unwrap();
|
||||
let pool = &state.db(&user).pool;
|
||||
Loading…
Add table
Add a link
Reference in a new issue