Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ El formato está basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1

> Las entradas entre la 0.6.0 y la 1.4.0 se reconstruyeron a posteriori a partir del historial de git, agrupadas por los saltos de versión que realmente ocurrieron en `package.json`. La 1.1.0 nunca existió: se pasó directamente de la 1.0.0 a la 1.2.0.

## [1.27.1] - 2026-09-08

### Fixed

- **La fecha de entrega cambiaba de formato al editarla.** La tarjeta la pinta el servidor con el idioma de la aplicación —«9 sept 2026»— y al cambiarla desde el modal se reescribía **dentro de la propia tarjeta** con un `toLocaleDateString()` sin argumentos, que usa el idioma del navegador: «9/09/26». Es el mismo fallo que se corrigió en la 1.25.0 para la tarjeta, del que quedó una copia suelta en el modal. Ahora las dos llaman a `fecha()`.
- **«Cancelar» al crear una base de datos hacía lo mismo que la equis.** El alta tiene dos pasos —elegir plantilla y rellenar el formulario— y cancelar en el segundo cerraba el diálogo entero, así que «me he equivocado de plantilla» se convertía en «empieza de cero». Ahora vuelve a la galería, y solo cierra si ya estabas en ella.

## [1.27.0] - 2026-08-23

### Security
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "forge-js",
"type": "module",
"version": "1.27.0",
"version": "1.27.1",
"engines": {
"node": ">=22.12.0"
},
Expand Down
14 changes: 13 additions & 1 deletion src/components/jira/IssueDetailsModal.astro
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ const t = useTranslations(lang);

<script>
import { escaparHtml } from '../../lib/texto';
import { fecha } from '../../lib/fechas';
const closeBtn = document.getElementById('close-modal-btn');
const modal = document.getElementById('issue-details-modal');
const backdrop = document.getElementById('issue-details-backdrop');
Expand Down Expand Up @@ -656,7 +657,18 @@ const t = useTranslations(lang);
// hasta cuatrocientas veces en el HTML de un tablero lleno, para leerla
// una vez al abrir uno.
const etiqueta = raizModal?.dataset.tDue || 'Due:';
fechaEl.textContent = `${etiqueta} ${new Date(iso + 'T00:00:00').toLocaleDateString()}`;
/*
* El mismo formato que la tarjeta, no el del navegador.
*
* Esto era un `toLocaleDateString()` **sin argumentos**, así que usaba el
* idioma del navegador y no el de la aplicación: la misma entrega salía
* «9 sept 2026» en la tarjeta y «9/09/26» al abrirla. Es el fallo que ya
* se corrigió una vez en `IssueCard.astro`; aquí quedó una copia suelta.
*
* Se llama a `fecha()` en vez de repetir las opciones a mano, que es
* exactamente cómo se llega a tener dos formatos.
*/
fechaEl.textContent = `${etiqueta} ${fecha(iso, raizModal?.dataset.lang || 'en')}`;
// El rojo de vencida se recalcula aquí: si no, una fecha movida al futuro
// seguiría pintada en rojo hasta recargar.
const vencida = card.dataset.status !== 'done' && new Date(iso + 'T00:00:00') < new Date();
Expand Down
22 changes: 21 additions & 1 deletion src/pages/w/[sys_tag]/db/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,13 @@ const TXT = {
</div>

<div class="pt-6 border-t border-forge-border/40 flex justify-end gap-3 mt-8 p-8">
<button type="button" class="px-6 py-2.5 text-sm font-bold text-forge-muted hover:text-forge-text transition-colors" onclick="document.getElementById('new-db-modal').close()">
{/* «Cancelar» vuelve al paso anterior; la equis cierra.

Hacía `close()`, o sea exactamente lo mismo que la equis, y en un
alta de dos pasos eso convierte «me he equivocado de plantilla» en
«empieza de cero». Dos controles distintos que hacen lo mismo son
uno de más. */}
<button type="button" id="btn-cancel-db" class="px-6 py-2.5 text-sm font-bold text-forge-muted hover:text-forge-text transition-colors">
{t('ws.settings.modal_cancel')}
</button>
<button type="button" id="btn-save-db" class="bg-forge-primary text-forge-on-accent px-8 py-2.5 rounded-xl font-bold hover:opacity-90 transition-all shadow-sm shadow-forge-primary/20">
Expand All @@ -270,6 +276,7 @@ const TXT = {
stepTpl: document.getElementById('step-tpl'),
stepForm: document.getElementById('step-form'),
backBtn: document.getElementById('btn-tpl-back'),
cancelBtn: document.getElementById('btn-cancel-db'),
subtitulo: document.getElementById('modal-sub'),
addColBtn: document.getElementById('btn-add-col'),
colsContainer: document.getElementById('columns-container'),
Expand All @@ -288,6 +295,19 @@ const TXT = {

this.elements.newDbBtn?.addEventListener('click', () => this.abrir());
this.elements.backBtn?.addEventListener('click', () => this.mostrarPaso('tpl'));

/*
* «Cancelar» deshace un paso; no cierra el diálogo entero.
*
* Estando en el formulario, cancelar quiere decir «esta plantilla no
* era»: se vuelve a la galería. Solo cierra si ya estás en la galería,
* que es donde no hay nada que deshacer.
*/
this.elements.cancelBtn?.addEventListener('click', () => {
const enFormulario = !this.elements.stepForm?.classList.contains('hidden');
if (enFormulario) this.mostrarPaso('tpl');
else this.elements.modal?.close();
});
this.elements.addColBtn?.addEventListener('click', () => this.addColumnRow());
this.elements.saveBtn?.addEventListener('click', () => this.handleSave());

Expand Down
98 changes: 98 additions & 0 deletions tests/e2e/fecha-y-cancelar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { test, expect } from '@playwright/test';
import { getTestDb } from './test-utils';

const ESPACIO = 'ws-fecha-cancelar';

async function entrar(page: any) {
await page.goto('/login');
await page.fill('input[name="username"]', 'jose');
await page.fill('input[name="password"]', process.env.TEST_PASSWORD || 'LocalDevPass123!');
await page.click('button[type="submit"]');
await page.waitForURL('**/');
}

function conEspacio() {
const db = getTestDb();
const yo = db.prepare("SELECT id FROM users WHERE username = 'jose'").get() as any;
db.prepare('INSERT OR IGNORE INTO workspaces (id, name, sys_tag, created_by) VALUES (?,?,?,?)')
.run(crypto.randomUUID(), 'Fecha y cancelar', ESPACIO, yo.id);
const ws = db.prepare('SELECT id FROM workspaces WHERE sys_tag = ?').get(ESPACIO) as any;
db.prepare("INSERT OR IGNORE INTO workspace_members (workspace_id, user_id, ws_role) VALUES (?,?,'owner')")
.run(ws.id, yo.id);
return { db, yo, ws };
}

/**
* La misma entrega se lee igual en la tarjeta y al abrirla.
*
* El modal usaba `toLocaleDateString()` **sin argumentos**, así que tomaba el
* idioma del navegador y no el de la aplicación: «9 sept 2026» en la tarjeta,
* «9/09/26» al abrir el ticket. Es el mismo fallo que ya se corrigió una vez en
* la tarjeta y del que quedó una copia suelta aquí.
*/
test('la fecha de entrega se ve igual en la tarjeta y en el modal', async ({ page }) => {
await entrar(page);
const { db, yo, ws } = conEspacio();
db.prepare("DELETE FROM issues WHERE title = 'Con fecha'").run();
db.prepare(`INSERT INTO issues (id, workspace_id, type, title, status, reporter_id, position, due_date)
VALUES (?, ?, 'task', 'Con fecha', 'todo', ?, 100000, '2026-09-09')`)
.run(crypto.randomUUID(), ws.id, yo.id);

await page.goto(`/w/${ESPACIO}/board?sprint=backlog`);
const tarjeta = page.locator('.issue-card', { hasText: 'Con fecha' }).first();
await expect(tarjeta).toBeVisible();

// Como la pinta el servidor, con el idioma de la aplicación.
const antes = (await tarjeta.locator('.card-due').textContent())?.trim() ?? '';

// Y ahora se edita desde el modal, que es cuando se reescribía con el
// formato del navegador: el modal escribe de vuelta en la propia tarjeta.
await tarjeta.locator('h4').click();
await expect(page.locator('#issue-details-modal')).not.toHaveClass(/translate-x-full/);

// El modal se rellena desde la tarjeta al abrirse. Sin esperar a que la
// fecha esté puesta, el `fill` corría antes que el manejador y el cambio se
// perdía: la prueba fallaba una de cada tres.
await expect(page.locator('#modal-issue-due-date')).toHaveValue('2026-09-09');

await page.fill('#modal-issue-due-date', '2026-09-10');
await page.locator('#modal-issue-due-date').dispatchEvent('change');

await expect
.poll(async () => (await tarjeta.locator('.card-due').textContent())?.trim() ?? '')
.not.toBe(antes);

const despues = (await tarjeta.locator('.card-due').textContent())?.trim() ?? '';

/*
* No se fija el formato concreto, sino que los dos se escriban igual: el
* servidor usa `fecha()` y el cliente usaba `toLocaleDateString()` a secas,
* así que salía «9 sept 2026» antes de tocarla y «9/09/26» después.
*/
const forma = (s: string) => s.replace(/\d+/g, '#');
expect(forma(despues), `antes «${antes}» y después «${despues}»`).toBe(forma(antes));
});

/**
* «Cancelar» deshace un paso; la equis cierra.
*
* Hacía `close()`, exactamente lo mismo que la equis, y en un alta de dos pasos
* eso convierte «me he equivocado de plantilla» en «empieza de cero».
*/
test('cancelar en el formulario vuelve a las plantillas, no cierra', async ({ page }) => {
await entrar(page);
conEspacio();
await page.goto(`/w/${ESPACIO}/db`);

await page.locator('#btn-new-db').click();
await expect(page.locator('#step-tpl')).toBeVisible();

// Se entra al formulario eligiendo una plantilla.
await page.locator('[data-tpl]').first().click();
await expect(page.locator('#step-form')).toBeVisible();

await page.locator('#btn-cancel-db').click();

await expect(page.locator('#new-db-modal'), 'el diálogo se cerró entero').toBeVisible();
await expect(page.locator('#step-tpl'), 'no volvió a las plantillas').toBeVisible();
});