From a0d6c7ecf73d25bf8633ac81320432f7114baf10 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sat, 26 Sep 2026 14:56:40 +0200 Subject: [PATCH 01/18] test(e2e): press Postpone to move a task a day later --- test/e2e/tests/tasks.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/tests/tasks.spec.ts b/test/e2e/tests/tasks.spec.ts index da5bae99..d10384eb 100644 --- a/test/e2e/tests/tasks.spec.ts +++ b/test/e2e/tests/tasks.spec.ts @@ -46,7 +46,7 @@ test('pushes a task to the next day', async ({ page }) => { await page .getByRole('listitem', { name: title }) - .getByRole('button', { name: 'Push to tomorrow' }) + .getByRole('button', { name: 'Postpone' }) .click() await expect(page.getByRole('listitem', { name: title })).toBeHidden() @@ -70,7 +70,7 @@ test('carries work left over from an earlier day into today', async ({ page }) = await page .getByRole('listitem', { name: title }) - .getByRole('button', { name: 'Push to tomorrow' }) + .getByRole('button', { name: 'Postpone' }) .click() await expect(page.getByRole('listitem', { name: title })).toBeHidden() From 0448d4a54ded46f2b0872d753ae6db00fa6220a9 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sat, 26 Sep 2026 14:56:40 +0200 Subject: [PATCH 02/18] feat(frontend): call the reschedule button Postpone --- frontend/src/languages/es-ES.json | 2 +- frontend/src/tasks/TaskList.tsx | 2 +- frontend/src/test/contact-tasks.test.tsx | 4 ++-- frontend/src/test/tasks-route.test.tsx | 10 +++++----- languages/alphone.pot | 6 +++--- languages/es-ES.po | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/src/languages/es-ES.json b/frontend/src/languages/es-ES.json index 37faba7f..a20ab510 100644 --- a/frontend/src/languages/es-ES.json +++ b/frontend/src/languages/es-ES.json @@ -1 +1 @@ -{"":{"lang":"es","plural-forms":""},"30 days":["30 días"],"90 days":["90 días"],"A contact needs a name.":["Un contacto necesita un nombre."],"A lifetime is zero days or more.":["Una duración es de cero días o más."],"A lifetime runs to %(maxDays)d days at most.":["Una duración llega como mucho a %(maxDays)d días."],"A name runs to %(max)d characters at most.":["Un nombre tiene como mucho %(max)d caracteres."],"A password runs to %(max)d characters at most.":["Una contraseña tiene como mucho %(max)d caracteres."],"A password runs to %(min)d characters at least.":["Una contraseña tiene al menos %(min)d caracteres."],"A task needs a title.":["Una tarea necesita un título."],"A token needs a name.":["Un token necesita un nombre."],"A year":["Un año"],"API tokens":["Tokens de API"],"Actions":["Acciones"],"Activation link":["Enlace de activación"],"Add a task":["Añadir una tarea"],"Add a task above, or open a contact and create one from their page.":["Añade una tarea arriba, o abre un contacto y crea una desde su página."],"Add a task for this contact":["Añadir una tarea para este contacto"],"Add identity":["Añadir identidad"],"Add one with New token.":["Añade uno con Token nuevo."],"Add one with New user.":["Añade uno con Usuario nuevo."],"Add task":["Añadir tarea"],"AlphOne cannot write to that channel.":["AlphOne no puede escribir en ese canal."],"AlphOne does not speak that language yet.":["AlphOne todavía no habla ese idioma."],"Already on contact %(name)s.":["Ya está en el contacto %(name)s."],"An account needs a name.":["Una cuenta necesita un nombre."],"An event needs the source it came from.":["Un evento necesita la fuente de la que viene."],"Another account already holds that address.":["Ya hay otra cuenta que tiene esa dirección."],"Another contact already holds that address.":["Ya hay otro contacto que tiene esa dirección."],"Areas":["Áreas"],"Ask for between %(min)d and %(max)d at a time.":["Pide entre %(min)d y %(max)d a la vez."],"Ask for tasks by one filter at a time.":["Pide las tareas con un solo filtro a la vez."],"Channel":["Canal"],"Choose at least one area the token reaches.":["Elige al menos un área a la que llegue el token."],"Choose at least one event to send.":["Elige al menos un evento que enviar."],"Choose the channel this address belongs to.":["Elige el canal al que pertenece esta dirección."],"Complete":["Completar"],"Contact":["Contacto"],"Contact tasks":["Tareas del contacto"],"Contacts":["Contactos"],"Contacts could not be loaded.":["No se han podido cargar los contactos."],"Copied.":["Copiado."],"Copy it by hand.":["Cópialo a mano."],"Copy secret":["Copiar el secreto"],"Copy the secret now, it is never shown again.":["Copia el secreto ahora, no se vuelve a mostrar nunca."],"Create contact":["Crear contacto"],"Create task":["Crear tarea"],"Create token":["Crear token"],"Created":["Creado"],"Created %(date)s":["Creado el %(date)s"],"Disable":["Desactivar"],"Disable %(name)s":["Desactivar a %(name)s"],"Done":["Hecho"],"Done (%(count)s)":["Hechas (%(count)s)"],"Done tasks":["Tareas hechas"],"Due %(date)s":["Vence el %(date)s"],"Due date":["Fecha de vencimiento"],"Email":["Correo electrónico"],"Enable":["Activar"],"Enable %(name)s":["Activar a %(name)s"],"Expires":["Caduca"],"Identities":["Identidades"],"Invitation sent.":["Invitación enviada."],"Label":["Etiqueta"],"Language":["Idioma"],"Last used":["Último uso"],"Link a contact":["Vincular un contacto"],"Load more":["Cargar más"],"Load more done":["Cargar más hechas"],"Load more overdue":["Cargar más vencidas"],"Load more tasks":["Cargar más tareas"],"Loading contacts…":["Cargando los contactos…"],"Loading contact…":["Cargando el contacto…"],"Loading tasks…":["Cargando las tareas…"],"Loading task…":["Cargando la tarea…"],"Loading tokens…":["Cargando los tokens…"],"Loading users…":["Cargando los usuarios…"],"Loading…":["Cargando…"],"Name":["Nombre"],"Navigation":["Navegación"],"Never expires":["No caduca nunca"],"Never used":["Sin usar"],"New API token":["Token de API nuevo"],"New contact":["Contacto nuevo"],"New task":["Tarea nueva"],"New task for this contact":["Tarea nueva para este contacto"],"New token":["Token nuevo"],"New user":["Usuario nuevo"],"Next day":["Día siguiente"],"No API tokens yet.":["Todavía no hay tokens de API."],"No contacts found.":["No se ha encontrado ningún contacto."],"No identities yet.":["Todavía no hay identidades."],"No mail server is configured. Deliver the activation link by hand.":["No hay servidor de correo configurado. Entrega el enlace de activación a mano."],"No open tasks.":["No hay tareas abiertas."],"No role":["Sin rol"],"No users yet.":["Todavía no hay usuarios."],"Nothing due today.":["No hay nada que venza hoy."],"Open tasks":["Tareas abiertas"],"Overdue":["Vencidas"],"Overdue tasks":["Tareas vencidas"],"Overdue tasks could not be loaded.":["No se han podido cargar las tareas vencidas."],"Previous day":["Día anterior"],"Priority":["Prioridad"],"Push to tomorrow":["Pasar a mañana"],"Read %(area)s":["Leer %(area)s"],"Reading the languages":["Leyendo los idiomas"],"Remove":["Quitar"],"Remove %(identifier)s":["Quitar %(identifier)s"],"Remove %(name)s":["Quitar a %(name)s"],"Reopen":["Reabrir"],"Resend invitation":["Reenviar invitación"],"Resend invitation to %(name)s":["Reenviar invitación a %(name)s"],"Revoke":["Revocar"],"Revoke %(name)s":["Revocar %(name)s"],"Revoke failed.":["No se ha podido revocar."],"Role":["Rol"],"Role of %(name)s":["Rol de %(name)s"],"Save":["Guardar"],"Scopes":["Ámbitos"],"Search contacts":["Buscar contactos"],"Send invitation":["Enviar invitación"],"Sign in to go on.":["Inicia sesión para continuar."],"Somebody must be able to manage users.":["Alguien tiene que poder gestionar los usuarios."],"Something went wrong.":["Algo ha ido mal."],"Something went wrong. Try again.":["Algo ha ido mal. Inténtalo de nuevo."],"Status":["Estado"],"Task":["Tarea"],"Tasks":["Tareas"],"Tasks could not be loaded.":["No se han podido cargar las tareas."],"That account no longer exists.":["Esa cuenta ya no existe."],"That address is no longer on this contact.":["Esa dirección ya no está en este contacto."],"That area is not one AlphOne offers.":["Esa área no es una de las que AlphOne ofrece."],"That contact no longer exists.":["Ese contacto ya no existe."],"That email and password do not match an account.":["Ese correo y esa contraseña no coinciden con ninguna cuenta."],"That event name is not one AlphOne knows.":["Ese nombre de evento no es uno de los que AlphOne conoce."],"That page marker is not one AlphOne issued.":["Ese marcador de página no es uno de los que AlphOne ha emitido."],"That priority is not one AlphOne offers.":["Esa prioridad no es una de las que AlphOne ofrece."],"That role holds more than your own.":["Ese rol abarca más que el tuyo."],"That role is not one this deployment names.":["Ese rol no es uno de los que este despliegue nombra."],"That status is not one AlphOne offers.":["Ese estado no es uno de los que AlphOne ofrece."],"That task no longer exists.":["Esa tarea ya no existe."],"That token no longer exists.":["Ese token ya no existe."],"That value is not in the form AlphOne expects.":["Ese valor no tiene la forma que AlphOne espera."],"That webhook no longer exists.":["Ese webhook ya no existe."],"The choice could not be saved.":["No se ha podido guardar la elección."],"The contact could not be created.":["No se ha podido crear el contacto."],"The contact could not be loaded.":["No se ha podido cargar el contacto."],"The contact could not be renamed.":["No se ha podido renombrar el contacto."],"The identity could not be added.":["No se ha podido añadir la identidad."],"The identity could not be removed.":["No se ha podido quitar la identidad."],"The invitation could not be sent.":["No se pudo enviar la invitación."],"The language changes when the page next loads.":["El idioma cambia la próxima vez que se cargue la página."],"The languages could not be read.":["No se han podido leer los idiomas."],"The task could not be added.":["No se ha podido añadir la tarea."],"The task could not be created.":["No se ha podido crear la tarea."],"The task could not be loaded.":["No se ha podido cargar la tarea."],"The task could not be saved.":["No se ha podido guardar la tarea."],"The task could not be updated.":["No se ha podido actualizar la tarea."],"The token could not be created.":["No se ha podido crear el token."],"This link is no longer valid. Ask for a new one.":["Este enlace ya no es válido. Pide uno nuevo."],"This token does not reach %(scope)s.":["Este token no alcanza %(scope)s."],"This workspace has been deactivated. Contact your operator.":["Este espacio de trabajo ha sido desactivado. Contacta con tu operador."],"Title":["Título"],"Today":["Hoy"],"Tokens could not be loaded.":["No se han podido cargar los tokens."],"Too many attempts. Wait %(retryAfter)d seconds and try again.":["Demasiados intentos. Espera %(retryAfter)d segundos e inténtalo de nuevo."],"Try a different search, or add one with New contact.":["Prueba otra búsqueda, o añade uno con Contacto nuevo."],"Users":["Usuarios"],"Users could not be loaded.":["No se han podido cargar los usuarios."],"Value":["Valor"],"Write %(area)s":["Escribir %(area)s"],"Write a scope as an area, a colon, then read or write.":["Escribe un ámbito como un área, dos puntos y luego read o write."],"Write the address as a full URL.":["Escribe la dirección como una URL completa."],"Write the address as a full email address.":["Escribe la dirección como una dirección de correo electrónico completa."],"Write the address itself.":["Escribe la dirección."],"You cannot change your own role.":["No puedes cambiar tu propio rol."],"You cannot disable your own account.":["No puedes desactivar tu propia cuenta."],"Your role does not allow %(capability)s.":["Tu rol no permite %(capability)s."],"account status\u0004Active":["Activa"],"account status\u0004Disabled":["Desactivada"],"account status\u0004Invited":["Invitada"],"admin section\u0004Contacts":["Contactos"],"admin section\u0004Language":["Idioma"],"admin section\u0004Tasks":["Tareas"],"contact channel\u0004Email":["Correo electrónico"],"contact channel\u0004Phone":["Teléfono"],"task priority\u0004High":["Alta"],"task priority\u0004Normal":["Normal"],"token expiry\u0004Expired":["Caducado"],"token lifetime\u0004Never":["Nunca"],"v%(version)s":["v%(version)s"]} +{"":{"lang":"es","plural-forms":""},"30 days":["30 días"],"90 days":["90 días"],"A contact needs a name.":["Un contacto necesita un nombre."],"A lifetime is zero days or more.":["Una duración es de cero días o más."],"A lifetime runs to %(maxDays)d days at most.":["Una duración llega como mucho a %(maxDays)d días."],"A name runs to %(max)d characters at most.":["Un nombre tiene como mucho %(max)d caracteres."],"A password runs to %(max)d characters at most.":["Una contraseña tiene como mucho %(max)d caracteres."],"A password runs to %(min)d characters at least.":["Una contraseña tiene al menos %(min)d caracteres."],"A task needs a title.":["Una tarea necesita un título."],"A token needs a name.":["Un token necesita un nombre."],"A year":["Un año"],"API tokens":["Tokens de API"],"Actions":["Acciones"],"Activation link":["Enlace de activación"],"Add a task":["Añadir una tarea"],"Add a task above, or open a contact and create one from their page.":["Añade una tarea arriba, o abre un contacto y crea una desde su página."],"Add a task for this contact":["Añadir una tarea para este contacto"],"Add identity":["Añadir identidad"],"Add one with New token.":["Añade uno con Token nuevo."],"Add one with New user.":["Añade uno con Usuario nuevo."],"Add task":["Añadir tarea"],"AlphOne cannot write to that channel.":["AlphOne no puede escribir en ese canal."],"AlphOne does not speak that language yet.":["AlphOne todavía no habla ese idioma."],"Already on contact %(name)s.":["Ya está en el contacto %(name)s."],"An account needs a name.":["Una cuenta necesita un nombre."],"An event needs the source it came from.":["Un evento necesita la fuente de la que viene."],"Another account already holds that address.":["Ya hay otra cuenta que tiene esa dirección."],"Another contact already holds that address.":["Ya hay otro contacto que tiene esa dirección."],"Areas":["Áreas"],"Ask for between %(min)d and %(max)d at a time.":["Pide entre %(min)d y %(max)d a la vez."],"Ask for tasks by one filter at a time.":["Pide las tareas con un solo filtro a la vez."],"Channel":["Canal"],"Choose at least one area the token reaches.":["Elige al menos un área a la que llegue el token."],"Choose at least one event to send.":["Elige al menos un evento que enviar."],"Choose the channel this address belongs to.":["Elige el canal al que pertenece esta dirección."],"Complete":["Completar"],"Contact":["Contacto"],"Contact tasks":["Tareas del contacto"],"Contacts":["Contactos"],"Contacts could not be loaded.":["No se han podido cargar los contactos."],"Copied.":["Copiado."],"Copy it by hand.":["Cópialo a mano."],"Copy secret":["Copiar el secreto"],"Copy the secret now, it is never shown again.":["Copia el secreto ahora, no se vuelve a mostrar nunca."],"Create contact":["Crear contacto"],"Create task":["Crear tarea"],"Create token":["Crear token"],"Created":["Creado"],"Created %(date)s":["Creado el %(date)s"],"Disable":["Desactivar"],"Disable %(name)s":["Desactivar a %(name)s"],"Done":["Hecho"],"Done (%(count)s)":["Hechas (%(count)s)"],"Done tasks":["Tareas hechas"],"Due %(date)s":["Vence el %(date)s"],"Due date":["Fecha de vencimiento"],"Email":["Correo electrónico"],"Enable":["Activar"],"Enable %(name)s":["Activar a %(name)s"],"Expires":["Caduca"],"Identities":["Identidades"],"Invitation sent.":["Invitación enviada."],"Label":["Etiqueta"],"Language":["Idioma"],"Last used":["Último uso"],"Link a contact":["Vincular un contacto"],"Load more":["Cargar más"],"Load more done":["Cargar más hechas"],"Load more overdue":["Cargar más vencidas"],"Load more tasks":["Cargar más tareas"],"Loading contacts…":["Cargando los contactos…"],"Loading contact…":["Cargando el contacto…"],"Loading tasks…":["Cargando las tareas…"],"Loading task…":["Cargando la tarea…"],"Loading tokens…":["Cargando los tokens…"],"Loading users…":["Cargando los usuarios…"],"Loading…":["Cargando…"],"Name":["Nombre"],"Navigation":["Navegación"],"Never expires":["No caduca nunca"],"Never used":["Sin usar"],"New API token":["Token de API nuevo"],"New contact":["Contacto nuevo"],"New task":["Tarea nueva"],"New task for this contact":["Tarea nueva para este contacto"],"New token":["Token nuevo"],"New user":["Usuario nuevo"],"Next day":["Día siguiente"],"No API tokens yet.":["Todavía no hay tokens de API."],"No contacts found.":["No se ha encontrado ningún contacto."],"No identities yet.":["Todavía no hay identidades."],"No mail server is configured. Deliver the activation link by hand.":["No hay servidor de correo configurado. Entrega el enlace de activación a mano."],"No open tasks.":["No hay tareas abiertas."],"No role":["Sin rol"],"No users yet.":["Todavía no hay usuarios."],"Nothing due today.":["No hay nada que venza hoy."],"Open tasks":["Tareas abiertas"],"Overdue":["Vencidas"],"Overdue tasks":["Tareas vencidas"],"Overdue tasks could not be loaded.":["No se han podido cargar las tareas vencidas."],"Postpone":["Posponer"],"Previous day":["Día anterior"],"Priority":["Prioridad"],"Read %(area)s":["Leer %(area)s"],"Reading the languages":["Leyendo los idiomas"],"Remove":["Quitar"],"Remove %(identifier)s":["Quitar %(identifier)s"],"Remove %(name)s":["Quitar a %(name)s"],"Reopen":["Reabrir"],"Resend invitation":["Reenviar invitación"],"Resend invitation to %(name)s":["Reenviar invitación a %(name)s"],"Revoke":["Revocar"],"Revoke %(name)s":["Revocar %(name)s"],"Revoke failed.":["No se ha podido revocar."],"Role":["Rol"],"Role of %(name)s":["Rol de %(name)s"],"Save":["Guardar"],"Scopes":["Ámbitos"],"Search contacts":["Buscar contactos"],"Send invitation":["Enviar invitación"],"Sign in to go on.":["Inicia sesión para continuar."],"Somebody must be able to manage users.":["Alguien tiene que poder gestionar los usuarios."],"Something went wrong.":["Algo ha ido mal."],"Something went wrong. Try again.":["Algo ha ido mal. Inténtalo de nuevo."],"Status":["Estado"],"Task":["Tarea"],"Tasks":["Tareas"],"Tasks could not be loaded.":["No se han podido cargar las tareas."],"That account no longer exists.":["Esa cuenta ya no existe."],"That address is no longer on this contact.":["Esa dirección ya no está en este contacto."],"That area is not one AlphOne offers.":["Esa área no es una de las que AlphOne ofrece."],"That contact no longer exists.":["Ese contacto ya no existe."],"That email and password do not match an account.":["Ese correo y esa contraseña no coinciden con ninguna cuenta."],"That event name is not one AlphOne knows.":["Ese nombre de evento no es uno de los que AlphOne conoce."],"That page marker is not one AlphOne issued.":["Ese marcador de página no es uno de los que AlphOne ha emitido."],"That priority is not one AlphOne offers.":["Esa prioridad no es una de las que AlphOne ofrece."],"That role holds more than your own.":["Ese rol abarca más que el tuyo."],"That role is not one this deployment names.":["Ese rol no es uno de los que este despliegue nombra."],"That status is not one AlphOne offers.":["Ese estado no es uno de los que AlphOne ofrece."],"That task no longer exists.":["Esa tarea ya no existe."],"That token no longer exists.":["Ese token ya no existe."],"That value is not in the form AlphOne expects.":["Ese valor no tiene la forma que AlphOne espera."],"That webhook no longer exists.":["Ese webhook ya no existe."],"The choice could not be saved.":["No se ha podido guardar la elección."],"The contact could not be created.":["No se ha podido crear el contacto."],"The contact could not be loaded.":["No se ha podido cargar el contacto."],"The contact could not be renamed.":["No se ha podido renombrar el contacto."],"The identity could not be added.":["No se ha podido añadir la identidad."],"The identity could not be removed.":["No se ha podido quitar la identidad."],"The invitation could not be sent.":["No se pudo enviar la invitación."],"The language changes when the page next loads.":["El idioma cambia la próxima vez que se cargue la página."],"The languages could not be read.":["No se han podido leer los idiomas."],"The task could not be added.":["No se ha podido añadir la tarea."],"The task could not be created.":["No se ha podido crear la tarea."],"The task could not be loaded.":["No se ha podido cargar la tarea."],"The task could not be saved.":["No se ha podido guardar la tarea."],"The task could not be updated.":["No se ha podido actualizar la tarea."],"The token could not be created.":["No se ha podido crear el token."],"This link is no longer valid. Ask for a new one.":["Este enlace ya no es válido. Pide uno nuevo."],"This token does not reach %(scope)s.":["Este token no alcanza %(scope)s."],"This workspace has been deactivated. Contact your operator.":["Este espacio de trabajo ha sido desactivado. Contacta con tu operador."],"Title":["Título"],"Today":["Hoy"],"Tokens could not be loaded.":["No se han podido cargar los tokens."],"Too many attempts. Wait %(retryAfter)d seconds and try again.":["Demasiados intentos. Espera %(retryAfter)d segundos e inténtalo de nuevo."],"Try a different search, or add one with New contact.":["Prueba otra búsqueda, o añade uno con Contacto nuevo."],"Users":["Usuarios"],"Users could not be loaded.":["No se han podido cargar los usuarios."],"Value":["Valor"],"Write %(area)s":["Escribir %(area)s"],"Write a scope as an area, a colon, then read or write.":["Escribe un ámbito como un área, dos puntos y luego read o write."],"Write the address as a full URL.":["Escribe la dirección como una URL completa."],"Write the address as a full email address.":["Escribe la dirección como una dirección de correo electrónico completa."],"Write the address itself.":["Escribe la dirección."],"You cannot change your own role.":["No puedes cambiar tu propio rol."],"You cannot disable your own account.":["No puedes desactivar tu propia cuenta."],"Your role does not allow %(capability)s.":["Tu rol no permite %(capability)s."],"account status\u0004Active":["Activa"],"account status\u0004Disabled":["Desactivada"],"account status\u0004Invited":["Invitada"],"admin section\u0004Contacts":["Contactos"],"admin section\u0004Language":["Idioma"],"admin section\u0004Tasks":["Tareas"],"contact channel\u0004Email":["Correo electrónico"],"contact channel\u0004Phone":["Teléfono"],"task priority\u0004High":["Alta"],"task priority\u0004Normal":["Normal"],"token expiry\u0004Expired":["Caducado"],"token lifetime\u0004Never":["Nunca"],"v%(version)s":["v%(version)s"]} diff --git a/frontend/src/tasks/TaskList.tsx b/frontend/src/tasks/TaskList.tsx index ffd6f867..b1636b15 100644 --- a/frontend/src/tasks/TaskList.tsx +++ b/frontend/src/tasks/TaskList.tsx @@ -109,7 +109,7 @@ function TaskRow({ className="alphone-tasks__push" onClick={() => controls.onPush(task)} > - {__('Push to tomorrow', 'alphone')} + {__('Postpone', 'alphone')} )} diff --git a/frontend/src/test/contact-tasks.test.tsx b/frontend/src/test/contact-tasks.test.tsx index 6574691b..b8087d29 100644 --- a/frontend/src/test/contact-tasks.test.tsx +++ b/frontend/src/test/contact-tasks.test.tsx @@ -178,7 +178,7 @@ test('pushes a task to tomorrow from the contact page', async () => { await screen.findByRole('list', { name: 'Contact tasks' }) const row = screen.getByRole('listitem', { name: 'Call her back' }) - await userEvent.click(within(row).getByRole('button', { name: 'Push to tomorrow' })) + await userEvent.click(within(row).getByRole('button', { name: 'Postpone' })) await waitFor(() => expect(patched).toHaveLength(1)) expect(patched[0]).toMatchObject({ id: callID, due_on: tomorrow }) @@ -316,7 +316,7 @@ test('reports when a contact task cannot be updated', async () => { await screen.findByRole('list', { name: 'Contact tasks' }) const row = screen.getByRole('listitem', { name: 'Call her back' }) - await userEvent.click(within(row).getByRole('button', { name: 'Push to tomorrow' })) + await userEvent.click(within(row).getByRole('button', { name: 'Postpone' })) expect(await screen.findByText('The task could not be updated.')).toBeInTheDocument() }) diff --git a/frontend/src/test/tasks-route.test.tsx b/frontend/src/test/tasks-route.test.tsx index 2b479390..ef75f268 100644 --- a/frontend/src/test/tasks-route.test.tsx +++ b/frontend/src/test/tasks-route.test.tsx @@ -579,7 +579,7 @@ test('pushes a task to tomorrow', async () => { await screen.findByText('Call the supplier') const row = screen.getByRole('listitem', { name: 'Call the supplier' }) - await userEvent.click(within(row).getByRole('button', { name: 'Push to tomorrow' })) + await userEvent.click(within(row).getByRole('button', { name: 'Postpone' })) await waitFor(() => expect(patched).toHaveLength(1)) expect(patched[0]).toEqual({ id: callID, body: { due_on: tomorrow } }) @@ -590,7 +590,7 @@ test('pushes an overdue task to tomorrow rather than to the day after it was due renderAt('/tasks') const row = await screen.findByRole('listitem', { name: 'Chase the invoice' }) - await userEvent.click(within(row).getByRole('button', { name: 'Push to tomorrow' })) + await userEvent.click(within(row).getByRole('button', { name: 'Postpone' })) await waitFor(() => expect(patched).toHaveLength(1)) expect(patched[0]).toEqual({ id: oldID, body: { due_on: tomorrow } }) @@ -602,7 +602,7 @@ test('pushes a task on a future day to the day after it', async () => { await screen.findByText('Call the supplier') const row = screen.getByRole('listitem', { name: 'Call the supplier' }) - await userEvent.click(within(row).getByRole('button', { name: 'Push to tomorrow' })) + await userEvent.click(within(row).getByRole('button', { name: 'Postpone' })) await waitFor(() => expect(patched).toHaveLength(1)) expect(patched[0]).toEqual({ id: callID, body: { due_on: localDate(2) } }) @@ -634,7 +634,7 @@ test('leaves done tasks without a push control', async () => { await userEvent.click(screen.getByRole('button', { name: 'Done (1)' })) const row = await screen.findByRole('listitem', { name: 'Book the courier' }) - expect(within(row).queryByRole('button', { name: 'Push to tomorrow' })).not.toBeInTheDocument() + expect(within(row).queryByRole('button', { name: 'Postpone' })).not.toBeInTheDocument() }) test('reports when overdue work cannot be loaded', async () => { @@ -727,7 +727,7 @@ test('leaves the day alone when pushing a task fails', async () => { await screen.findByText('Call the supplier') const row = screen.getByRole('listitem', { name: 'Call the supplier' }) - await userEvent.click(within(row).getByRole('button', { name: 'Push to tomorrow' })) + await userEvent.click(within(row).getByRole('button', { name: 'Postpone' })) expect(await screen.findByText('The task could not be updated.')).toBeInTheDocument() expect(screen.getByText('Call the supplier')).toBeInTheDocument() diff --git a/languages/alphone.pot b/languages/alphone.pot index f487bc8e..f283b9bd 100644 --- a/languages/alphone.pot +++ b/languages/alphone.pot @@ -307,13 +307,13 @@ msgstr "" msgid "Overdue tasks could not be loaded." msgstr "" -msgid "Previous day" +msgid "Postpone" msgstr "" -msgid "Priority" +msgid "Previous day" msgstr "" -msgid "Push to tomorrow" +msgid "Priority" msgstr "" msgid "Read %(area)s" diff --git a/languages/es-ES.po b/languages/es-ES.po index 8f6a0b6b..abe458ca 100644 --- a/languages/es-ES.po +++ b/languages/es-ES.po @@ -697,8 +697,8 @@ msgid "Loading…" msgstr "Cargando…" #, fuzzy -msgid "Push to tomorrow" -msgstr "Pasar a mañana" +msgid "Postpone" +msgstr "Posponer" #, fuzzy msgid "Activation link" From 98293f1be56f81290b70ac6847869a64fd39b01b Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sat, 26 Sep 2026 14:56:41 +0200 Subject: [PATCH 03/18] docs(tasks): describe Postpone for every due date --- docs/src/content/docs/guides/tasks.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/content/docs/guides/tasks.md b/docs/src/content/docs/guides/tasks.md index fe47b90a..354dcac5 100644 --- a/docs/src/content/docs/guides/tasks.md +++ b/docs/src/content/docs/guides/tasks.md @@ -5,7 +5,7 @@ description: How the daily task list works, from quick capture to rescheduling w Tasks are the work you owe someone. AlphOne opens on the day's list, and that list is the main screen of the product: work it top to bottom and -push forward whatever did not happen. +postpone whatever did not happen. ## The day @@ -42,10 +42,10 @@ something the app does behind your back. ## Rescheduling -**Push to tomorrow** moves a task to the day after today, whatever it was -due before. A task that has been overdue for a week lands on tomorrow, -not on a date that is still in the past. This is the no-answer pattern: -call, no reply, push, and it comes back tomorrow. +**Postpone** moves an open task one day later. A task due today, or overdue +for a week, lands on tomorrow, never on a date that is still in the past. A +task already due on a later day moves to the day after it. This is the +no-answer pattern: call, no reply, postpone, and it comes back tomorrow. For any other date, open the task and edit its due date directly. From cbf941eae66fcbba93515033ddceda1cd6e2ca15 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sat, 26 Sep 2026 16:11:05 +0200 Subject: [PATCH 04/18] test(e2e): add a contact task on a chosen day --- test/e2e/tests/tasks.spec.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/e2e/tests/tasks.spec.ts b/test/e2e/tests/tasks.spec.ts index d10384eb..77ea38cd 100644 --- a/test/e2e/tests/tasks.spec.ts +++ b/test/e2e/tests/tasks.spec.ts @@ -120,3 +120,34 @@ test('adds a task from a contact and links it back', async ({ page }) => { await expect(page.getByRole('heading', { name: title })).toBeVisible() await expect(page.getByRole('link', { name: contact })).toBeVisible() }) + +test('adds a task from a contact on a chosen day', async ({ page }) => { + const stamp = Date.now() + const contact = `Customer ${stamp}` + const title = `Follow up with ${contact}` + const chosen = new Date() + chosen.setDate(chosen.getDate() + 3) + const due = chosen.toLocaleDateString('en-CA') + const label = `Due ${chosen.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}` + + await page.goto('/') + await page.getByRole('link', { name: 'Contacts' }).click() + await page.getByRole('link', { name: 'New contact' }).click() + await page.getByLabel('Name').fill(contact) + await page.getByRole('button', { name: 'Create contact' }).click() + await expect(page.getByRole('heading', { name: contact })).toBeVisible() + + await page.getByRole('textbox', { name: 'New task for this contact' }).fill(title) + await page.getByLabel('Due date', { exact: true }).fill(due) + await page.getByRole('button', { name: 'Add task' }).click() + + const row = page.getByRole('list', { name: 'Contact tasks' }).getByRole('listitem', { name: title }) + await expect(row).toContainText(label) + const browserToday = await page.evaluate(() => new Date().toLocaleDateString('en-CA')) + await expect(page.getByLabel('Due date', { exact: true })).toHaveValue(browserToday) + + await row.getByRole('link', { name: title }).click() + + await expect(page.getByRole('heading', { name: title })).toBeVisible() + await expect(page.getByLabel('Due date', { exact: true })).toHaveValue(due) +}) From 92129f65bd652514e6e18cf1e6468abc010553d9 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sat, 26 Sep 2026 16:11:06 +0200 Subject: [PATCH 05/18] refactor(frontend): give the contact task form its own component --- frontend/src/tasks/ContactTasks.tsx | 58 ++++++++++++++++++----------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/frontend/src/tasks/ContactTasks.tsx b/frontend/src/tasks/ContactTasks.tsx index 30362f2a..939c18d4 100644 --- a/frontend/src/tasks/ContactTasks.tsx +++ b/frontend/src/tasks/ContactTasks.tsx @@ -21,7 +21,7 @@ import type { RowControls, ListedTask } from './TaskList' const contactDetailOperation = 'ContactDetail' /** - * Renders a contact's open tasks and the field that adds one. + * Renders a contact's open tasks and the form that adds one. * @returns The contact tasks section. */ export function ContactTasks({ @@ -33,19 +33,10 @@ export function ContactTasks({ }) { const today = isoDate(new Date()) const graph = useGraph() - const [title, setTitle] = useState('') const [pendingID, setPendingID] = useState('') - const [add, runAdd] = useGraphMutation(createTaskMutation) const [change, runChange] = useGraphMutation(updateTaskMutation) const [push, runPush] = useGraphMutation(updateTaskMutation) const settled = () => graph.refetch([contactDetailOperation]) - const submitAdd = async () => { - const result = await runAdd({ input: { title, dueOn: today, contactId } }) - if (result.data) { - setTitle('') - settled() - } - } const completeTask = async (task: ListedTask) => { setPendingID(task.id) const result = await runChange({ id: task.id, input: { status: 'done' } }) @@ -69,6 +60,40 @@ export function ContactTasks({ }> {__('Tasks', 'alphone')} + + {change.error || push.error ? ( + {__('The task could not be updated.', 'alphone')} + ) : null} + void completeTask(task), + onPush: (task) => void pushTask(task), + pendingID, + }} + /> + + ) +} + +/** + * Renders the form that adds a task to a contact. + * @param props - The contact the task belongs to and what to call once it is added. + * @returns The add task form and its error. + */ +function AddContactTaskForm({ contactId, onAdded }: { contactId: string; onAdded: () => void }) { + const [title, setTitle] = useState('') + const [add, runAdd] = useGraphMutation(createTaskMutation) + const submitAdd = async () => { + const result = await runAdd({ input: { title, dueOn: isoDate(new Date()), contactId } }) + if (result.data) { + setTitle('') + onAdded() + } + } + + return ( + <>
{ @@ -96,18 +121,7 @@ export function ContactTasks({ {validationMessage(graphError(add.error), __('The task could not be added.', 'alphone'))} ) : null} - {change.error || push.error ? ( - {__('The task could not be updated.', 'alphone')} - ) : null} - void completeTask(task), - onPush: (task) => void pushTask(task), - pendingID, - }} - /> - + ) } From 6d687d538f2911ce51e52e75c4caba14e53f4c34 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sat, 26 Sep 2026 16:11:06 +0200 Subject: [PATCH 06/18] feat(frontend): pick the due date of a task added on a contact --- frontend/src/index.css | 4 ++ frontend/src/tasks/ContactTasks.tsx | 19 ++++++--- frontend/src/test/contact-tasks.test.tsx | 49 +++++++++++++++++++++++- 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index 69e12e63..0d5b44a5 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -369,6 +369,10 @@ gap: var(--wpds-dimension-gap-sm); } +.alphone-tasks__add--contact { + flex-wrap: wrap; +} + .alphone-tasks__list { list-style: none; margin: 0; diff --git a/frontend/src/tasks/ContactTasks.tsx b/frontend/src/tasks/ContactTasks.tsx index 939c18d4..418abbcc 100644 --- a/frontend/src/tasks/ContactTasks.tsx +++ b/frontend/src/tasks/ContactTasks.tsx @@ -12,7 +12,7 @@ import { graphError, useGraph, useGraphMutation } from '@alphone/frontend-sdk' import type { ConnectionResult } from '@alphone/frontend-sdk' import { useState } from 'react' -import { isoDate, laterDate, shiftDate } from './format' +import { isValidDate, isoDate, laterDate, shiftDate } from './format' import { createTaskMutation, updateTaskMutation } from './operations' import { TaskList } from './TaskList' import type { RowControls, ListedTask } from './TaskList' @@ -77,17 +77,20 @@ export function ContactTasks({ } /** - * Renders the form that adds a task to a contact. + * Renders the form that adds a task to a contact on a chosen day. * @param props - The contact the task belongs to and what to call once it is added. * @returns The add task form and its error. */ function AddContactTaskForm({ contactId, onAdded }: { contactId: string; onAdded: () => void }) { const [title, setTitle] = useState('') + const [picked, setPicked] = useState(null) const [add, runAdd] = useGraphMutation(createTaskMutation) + const dueOn = picked ?? isoDate(new Date()) const submitAdd = async () => { - const result = await runAdd({ input: { title, dueOn: isoDate(new Date()), contactId } }) + const result = await runAdd({ input: { title, dueOn, contactId } }) if (result.data) { setTitle('') + setPicked(null) onAdded() } } @@ -95,7 +98,7 @@ function AddContactTaskForm({ contactId, onAdded }: { contactId: string; onAdded return ( <> { event.preventDefault() void submitAdd() @@ -108,9 +111,15 @@ function AddContactTaskForm({ contactId, onAdded }: { contactId: string; onAdded value={title} onChange={(event) => setTitle(event.target.value)} /> + setPicked(event.target.value)} + />