feat: inactive contacts hidden in sidebar
This commit is contained in:
parent
f75260c079
commit
b079001cc5
7 changed files with 123 additions and 58 deletions
|
|
@ -9,7 +9,7 @@ test.beforeEach(async ({ page }) => {
|
||||||
|
|
||||||
test('manual-freshen date is editable', async ({ page }) => {
|
test('manual-freshen date is editable', async ({ page }) => {
|
||||||
await page.getByRole('link', { name: /edit/i }).click();
|
await page.getByRole('link', { name: /edit/i }).click();
|
||||||
await expect(page.getByRole('textbox', { name: /freshened/i })).toBeVisible();
|
await expect(page.locator('input[name="manually_freshened_on"]')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('last-contact date on display resolves journal mentions and manual-freshen', async ({ page }) => {
|
test('last-contact date on display resolves journal mentions and manual-freshen', async ({ page }) => {
|
||||||
|
|
@ -38,7 +38,15 @@ test.skip("groups wrap nicely", async ({ page }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allow marking as inactive', async ({ page }) => {
|
test('allow marking as inactive', async ({ page }) => {
|
||||||
|
await page.getByRole('link', { name: /edit/i }).click();
|
||||||
|
await expect(page.locator('#alpine-loaded')).not.toHaveAttribute('x-cloak');
|
||||||
|
|
||||||
|
// TODO: this only works if there's no other comboboxes on the page :/
|
||||||
|
await page.getByRole('combobox').selectOption('Inactive')
|
||||||
|
await page.getByRole('button', { name: /save/i }).click();
|
||||||
|
await expect(page.locator('#alpine-loaded')).not.toHaveAttribute('x-cloak');
|
||||||
|
|
||||||
|
await expect(page.locator('#contacts-sidebar').getByText("Test Testerson")).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allow exempting from stale', async ({ page }) => {
|
test('allow exempting from stale', async ({ page }) => {
|
||||||
|
|
@ -50,7 +58,7 @@ test('stale list considers periodicity', async ({ page }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('page title has contact primary name', async ({ page }) => {
|
test('page title has contact primary name', async ({ page }) => {
|
||||||
await expect(page.title()).toContain("Test Testerson");
|
expect(await page.title()).toContain("Test Testerson");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ test("changing a contact's names updates journal entries", async ({ page }) => {
|
||||||
await page.getByRole('button', { name: 'Add' }).nth(1).click();
|
await page.getByRole('button', { name: 'Add' }).nth(1).click();
|
||||||
await page.getByRole('button', { name: /save/i }).click();
|
await page.getByRole('button', { name: /save/i }).click();
|
||||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||||
console.log(await journal.innerHTML());
|
|
||||||
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(1);
|
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(1);
|
||||||
|
|
||||||
// delete an existing name
|
// delete an existing name
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,72 @@
|
||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
import 'custom-expects';
|
import './custom-expects';
|
||||||
|
|
||||||
// purposefully not using ??: we want to replace empty empty string with default
|
// purposefully not using ??: we want to replace empty empty string with default
|
||||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
|
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
let addlConfig = {
|
let addlConfig = {
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let projects = [
|
let projects = [
|
||||||
{
|
{
|
||||||
name: 'chromium',
|
name: 'chromium',
|
||||||
use: { ...devices['Desktop Chrome'] },
|
use: { ...devices['Desktop Chrome'] },
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'firefox',
|
name: 'firefox',
|
||||||
use: { ...devices['Desktop Firefox'] },
|
use: { ...devices['Desktop Firefox'] },
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'webkit',
|
name: 'webkit',
|
||||||
use: { ...devices['Desktop Safari'] },
|
use: { ...devices['Desktop Safari'] },
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Test against mobile viewports. */
|
/* Test against mobile viewports. */
|
||||||
{
|
{
|
||||||
name: 'Mobile Chrome',
|
name: 'Mobile Chrome',
|
||||||
use: { ...devices['Pixel 5'] },
|
use: { ...devices['Pixel 5'] },
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'Mobile Safari',
|
name: 'Mobile Safari',
|
||||||
use: { ...devices['iPhone 12'] },
|
use: { ...devices['iPhone 12'] },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const pfil = process.env.PROJECT_FILTER;
|
const pfil = process.env.PROJECT_FILTER;
|
||||||
if (pfil) {
|
if (pfil) {
|
||||||
if (pfil.startsWith('!')) {
|
if (pfil.startsWith('!')) {
|
||||||
projects = projects.filter(p => p.name !== pfil.slice(1));
|
projects = projects.filter(p => p.name !== pfil.slice(1));
|
||||||
} else {
|
} else {
|
||||||
projects = projects.filter(p => p.name === pfil);
|
projects = projects.filter(p => p.name === pfil);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See https://playwright.dev/docs/test-configuration.
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
*/
|
*/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './pages',
|
testDir: './pages',
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
workers: 1,
|
workers: 1,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: Boolean(process.env.CI),
|
forbidOnly: Boolean(process.env.CI),
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
baseURL: BASE_URL,
|
baseURL: BASE_URL,
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
projects,
|
projects,
|
||||||
...addlConfig,
|
...addlConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ insert into names(contact_id, sort, name) values
|
||||||
insert into groups(contact_id, name, slug) values
|
insert into groups(contact_id, name, slug) values
|
||||||
(0, 'ABC', 'abc');
|
(0, 'ABC', 'abc');
|
||||||
|
|
||||||
insert into contacts(id, birthday) values
|
insert into contacts(id, birthday, active) values
|
||||||
(1, 'April?');
|
(1, 'April?', false);
|
||||||
insert into names(contact_id, sort, name) values
|
insert into names(contact_id, sort, name) values
|
||||||
(1, 0, 'Bazel Bagend'),
|
(1, 0, 'Bazel Bagend'),
|
||||||
(1, 1, 'Bazel');
|
(1, 1, 'Bazel');
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,9 @@ mod get {
|
||||||
let mfresh_str = contact
|
let mfresh_str = contact
|
||||||
.manually_freshened_at
|
.manually_freshened_at
|
||||||
.clone()
|
.clone()
|
||||||
.map_or("".to_string(), |m| m.to_string());
|
.map_or("".to_string(), |m| {
|
||||||
|
m.to_zoned(TimeZone::UTC).date().to_string()
|
||||||
|
});
|
||||||
|
|
||||||
let text_body: String =
|
let text_body: String =
|
||||||
sqlx::query!("select text_body from contacts where id = $1", contact_id)
|
sqlx::query!("select text_body from contacts where id = $1", contact_id)
|
||||||
|
|
@ -278,10 +280,20 @@ mod get {
|
||||||
span .hint { code { "(yyyy|--)mmdd" } " or free text" }
|
span .hint { code { "(yyyy|--)mmdd" } " or free text" }
|
||||||
}
|
}
|
||||||
label { "freshened" }
|
label { "freshened" }
|
||||||
div x-data=(json!({ "date": mfresh_str })) {
|
div x-data=(json!({ "date": mfresh_str, "stamp": "" })) x-init="today = () => (new Date().toISOString().split('T')[0])" {
|
||||||
input type="hidden" name="manually_freshened_at" x-model="date";
|
input
|
||||||
span x-text="date.length ? date.split('T')[0] : '(never)'" {}
|
type="hidden"
|
||||||
input type="button" value="Mark fresh now" x-on:click="date = new Date().toISOString()";
|
name="manually_freshened_at"
|
||||||
|
x-model="stamp";
|
||||||
|
input
|
||||||
|
type="date"
|
||||||
|
name="manually_freshened_on"
|
||||||
|
x-model="date"
|
||||||
|
x-bind:max="today()"
|
||||||
|
x-on:input="stamp = new Date(date).toISOString()";
|
||||||
|
|
||||||
|
input type="button" value="Mark fresh now" x-on:click="date = today(); stamp = new Date().toISOString()";
|
||||||
|
span .hint x-text="`max ${today()}`";
|
||||||
}
|
}
|
||||||
label { "phone" }
|
label { "phone" }
|
||||||
#phone_numbers x-data=(json!({ "phones": phone_numbers, "new_label": "", "new_number": "" })) {
|
#phone_numbers x-data=(json!({ "phones": phone_numbers, "new_label": "", "new_number": "" })) {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ struct ContactLink {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Layout {
|
pub struct Layout {
|
||||||
contact_links: Vec<ContactLink>,
|
contact_links: Vec<ContactLink>,
|
||||||
|
inactive_contact_links: Vec<ContactLink>,
|
||||||
user: User,
|
user: User,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,6 +49,20 @@ impl FromRequestParts<AppState> for Layout {
|
||||||
from contacts c
|
from contacts c
|
||||||
left join names n on c.id = n.contact_id
|
left join names n on c.id = n.contact_id
|
||||||
where n.sort is null or n.sort = 0
|
where n.sort is null or n.sort = 0
|
||||||
|
and c.active = true
|
||||||
|
order by name asc",
|
||||||
|
)
|
||||||
|
.fetch_all(&state.db(&user).pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let inactive_contact_links = sqlx::query_as!(
|
||||||
|
ContactLink,
|
||||||
|
"select c.id as contact_id,
|
||||||
|
coalesce(n.name, '(unnamed)') as name
|
||||||
|
from contacts c
|
||||||
|
left join names n on c.id = n.contact_id
|
||||||
|
where n.sort is null or n.sort = 0
|
||||||
|
and c.active = false
|
||||||
order by name asc",
|
order by name asc",
|
||||||
)
|
)
|
||||||
.fetch_all(&state.db(&user).pool)
|
.fetch_all(&state.db(&user).pool)
|
||||||
|
|
@ -55,13 +70,19 @@ impl FromRequestParts<AppState> for Layout {
|
||||||
|
|
||||||
Ok(Layout {
|
Ok(Layout {
|
||||||
contact_links,
|
contact_links,
|
||||||
|
inactive_contact_links,
|
||||||
user,
|
user,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout {
|
impl Layout {
|
||||||
pub fn render(&self, title: impl AsRef<str>, css: Option<Vec<&str>>, content: Markup) -> Markup {
|
pub fn render(
|
||||||
|
&self,
|
||||||
|
title: impl AsRef<str>,
|
||||||
|
css: Option<Vec<&str>>,
|
||||||
|
content: Markup,
|
||||||
|
) -> Markup {
|
||||||
html! {
|
html! {
|
||||||
(DOCTYPE)
|
(DOCTYPE)
|
||||||
html {
|
html {
|
||||||
|
|
@ -101,6 +122,23 @@ impl Layout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if !self.inactive_contact_links.is_empty() {
|
||||||
|
li .inactive {
|
||||||
|
details {
|
||||||
|
summary { "Inactive contacts" }
|
||||||
|
ul {
|
||||||
|
@for link in &self.inactive_contact_links {
|
||||||
|
li {
|
||||||
|
a href=(format!("/contact/{}", link.contact_id)) {
|
||||||
|
(link.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
main {
|
main {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abb
|
||||||
|
|
||||||
body {
|
body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -35,6 +35,7 @@ section#content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
@media only screen and (max-width: 650px) {
|
@media only screen and (max-width: 650px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +45,8 @@ section#content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-right: 1em;
|
padding-right: 1em;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
@media only screen and (max-width: 650px) {
|
@media only screen and (max-width: 650px) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
float: left;
|
float: left;
|
||||||
|
|
@ -58,7 +61,7 @@ section#content {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
& > ul {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
background-color: var(--main-bg-color);
|
background-color: var(--main-bg-color);
|
||||||
|
|
@ -79,12 +82,17 @@ section#content {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li.inactive {
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue