Merge pull request 'Drop stale label drafts after inactivity and bump version to 0.2.3' (#8) from codex/reset-stale-app-date into main
ci/woodpecker/push/woodpecker Pipeline is pending
ci/woodpecker/push/woodpecker Pipeline is pending
Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lonc-web",
|
"name": "lonc-web",
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "lonc-web",
|
"name": "lonc-web",
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zxing/browser": "^0.1.5",
|
"@zxing/browser": "^0.1.5",
|
||||||
"alpinejs": "^3.14.9",
|
"alpinejs": "^3.14.9",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lonc-web",
|
"name": "lonc-web",
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
export const APP_NAME = 'Lonc';
|
export const APP_NAME = 'Lonc';
|
||||||
export const APP_VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.2.2';
|
export const APP_VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.2.3';
|
||||||
export const TRYTON_APPLICATION = 'kitchen';
|
export const TRYTON_APPLICATION = 'kitchen';
|
||||||
|
|
||||||
export const CONNECTION_STATES = {
|
export const CONNECTION_STATES = {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const STOCK_LEVEL_OPTIONS = [
|
|||||||
|
|
||||||
const QUANTITY_UNIT_OPTIONS = ['g', 'ml', 'pc'];
|
const QUANTITY_UNIT_OPTIONS = ['g', 'ml', 'pc'];
|
||||||
const EXPIRATION_DAY_OPTIONS = ['3', '5', '8', '10', '15', '20', '25', '30', '45', '60', '90', '120', '150', '180'];
|
const EXPIRATION_DAY_OPTIONS = ['3', '5', '8', '10', '15', '20', '25', '30', '45', '60', '90', '120', '150', '180'];
|
||||||
|
const LABEL_DRAFT_STALE_MS = 30 * 60 * 1000;
|
||||||
|
|
||||||
export function renderLabelCreatePage() {
|
export function renderLabelCreatePage() {
|
||||||
return `
|
return `
|
||||||
@@ -607,9 +608,7 @@ function createDefaultForm() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadLabelDraft() {
|
function normalizeLabelDraft(draft) {
|
||||||
const draft = loadStoredValue(STORAGE_KEYS.labelDraft, createDefaultForm());
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...createDefaultForm(),
|
...createDefaultForm(),
|
||||||
...draft,
|
...draft,
|
||||||
@@ -638,6 +637,41 @@ function buildDraftPayload(form) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildLabelDraftEnvelope(form) {
|
||||||
|
return {
|
||||||
|
form: buildDraftPayload(form),
|
||||||
|
savedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLabelDraft(form) {
|
||||||
|
saveStoredValue(STORAGE_KEYS.labelDraft, buildLabelDraftEnvelope(form));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLabelDraft() {
|
||||||
|
const storedDraft = loadStoredValue(STORAGE_KEYS.labelDraft, null);
|
||||||
|
|
||||||
|
if (!storedDraft || typeof storedDraft !== 'object' || Array.isArray(storedDraft)) {
|
||||||
|
return createDefaultForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasEnvelope =
|
||||||
|
storedDraft.form
|
||||||
|
&& typeof storedDraft.form === 'object'
|
||||||
|
&& !Array.isArray(storedDraft.form);
|
||||||
|
|
||||||
|
if (!hasEnvelope) {
|
||||||
|
return normalizeLabelDraft(storedDraft);
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedAt = Number(storedDraft.savedAt || 0);
|
||||||
|
if (!savedAt || Date.now() - savedAt >= LABEL_DRAFT_STALE_MS) {
|
||||||
|
return createDefaultForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeLabelDraft(storedDraft.form);
|
||||||
|
}
|
||||||
|
|
||||||
export function labelCreatePageData(store) {
|
export function labelCreatePageData(store) {
|
||||||
return {
|
return {
|
||||||
previewState: createAsyncState(),
|
previewState: createAsyncState(),
|
||||||
@@ -1084,7 +1118,7 @@ export function labelCreatePageData(store) {
|
|||||||
this.persistDraft();
|
this.persistDraft();
|
||||||
},
|
},
|
||||||
persistDraft() {
|
persistDraft() {
|
||||||
saveStoredValue(STORAGE_KEYS.labelDraft, buildDraftPayload(this.form));
|
saveLabelDraft(this.form);
|
||||||
},
|
},
|
||||||
get filteredLocations() {
|
get filteredLocations() {
|
||||||
const query = this.locationSearch.trim().toLowerCase();
|
const query = this.locationSearch.trim().toLowerCase();
|
||||||
@@ -1487,7 +1521,7 @@ export function labelCreatePageData(store) {
|
|||||||
message: `${entryName} was ${operationVerb} successfully.`,
|
message: `${entryName} was ${operationVerb} successfully.`,
|
||||||
});
|
});
|
||||||
this.upsertPreview = entry;
|
this.upsertPreview = entry;
|
||||||
saveStoredValue(STORAGE_KEYS.labelDraft, buildDraftPayload(this.form));
|
saveLabelDraft(this.form);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.fieldErrors = normalizeValidationError(error);
|
this.fieldErrors = normalizeValidationError(error);
|
||||||
this.submitError = error.message;
|
this.submitError = error.message;
|
||||||
@@ -1508,7 +1542,7 @@ export function labelCreatePageData(store) {
|
|||||||
this.fieldErrors = {};
|
this.fieldErrors = {};
|
||||||
this.upsertPreview = null;
|
this.upsertPreview = null;
|
||||||
this.printIssue = '';
|
this.printIssue = '';
|
||||||
saveStoredValue(STORAGE_KEYS.labelDraft, this.form);
|
saveLabelDraft(this.form);
|
||||||
if (revokePreview && this.previewUrl.startsWith('blob:')) {
|
if (revokePreview && this.previewUrl.startsWith('blob:')) {
|
||||||
URL.revokeObjectURL(this.previewUrl);
|
URL.revokeObjectURL(this.previewUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,33 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
const applyItemUpsertMock = vi.fn();
|
const applyItemUpsertMock = vi.fn();
|
||||||
const previewItemUpsertMock = vi.fn();
|
const previewItemUpsertMock = vi.fn();
|
||||||
const printItemLabelMock = vi.fn();
|
const printItemLabelMock = vi.fn();
|
||||||
|
const LABEL_DRAFT_STORAGE_KEY = 'lonc.labels.draft';
|
||||||
|
|
||||||
|
let localStorageState;
|
||||||
|
let localStorageMock;
|
||||||
|
|
||||||
|
function createWindowStorageMock(initialState = {}) {
|
||||||
|
const state = new Map(Object.entries(initialState));
|
||||||
|
const localStorage = {
|
||||||
|
getItem: vi.fn((key) => (state.has(key) ? state.get(key) : null)),
|
||||||
|
setItem: vi.fn((key, value) => {
|
||||||
|
state.set(key, String(value));
|
||||||
|
}),
|
||||||
|
removeItem: vi.fn((key) => {
|
||||||
|
state.delete(key);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.stubGlobal('window', { localStorage });
|
||||||
|
return { state, localStorage };
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStoredLabelDraft() {
|
||||||
|
const raw = localStorageState.get(LABEL_DRAFT_STORAGE_KEY);
|
||||||
|
return raw ? JSON.parse(raw) : null;
|
||||||
|
}
|
||||||
|
|
||||||
vi.mock('../../../src/api/stock.js', () => ({
|
vi.mock('../../../src/api/stock.js', () => ({
|
||||||
applyItemUpsert: (...args) => applyItemUpsertMock(...args),
|
applyItemUpsert: (...args) => applyItemUpsertMock(...args),
|
||||||
@@ -23,6 +48,23 @@ vi.mock('../../../src/api/locations.js', () => ({
|
|||||||
const { labelCreatePageData } = await import('../../../src/features/labels/label-create-page.js');
|
const { labelCreatePageData } = await import('../../../src/features/labels/label-create-page.js');
|
||||||
|
|
||||||
describe('label create upsert-first submit', () => {
|
describe('label create upsert-first submit', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2026-04-12T12:00:00Z'));
|
||||||
|
applyItemUpsertMock.mockReset();
|
||||||
|
previewItemUpsertMock.mockReset();
|
||||||
|
printItemLabelMock.mockReset();
|
||||||
|
|
||||||
|
const storageMock = createWindowStorageMock();
|
||||||
|
localStorageState = storageMock.state;
|
||||||
|
localStorageMock = storageMock.localStorage;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
it('defaults print checkbox to enabled', () => {
|
it('defaults print checkbox to enabled', () => {
|
||||||
const data = labelCreatePageData({
|
const data = labelCreatePageData({
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
@@ -32,6 +74,132 @@ describe('label create upsert-first submit', () => {
|
|||||||
expect(data.printLabelOnSave).toBe(true);
|
expect(data.printLabelOnSave).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('restores a fresh enveloped draft when inactivity is below 30 minutes', () => {
|
||||||
|
localStorageState.set(
|
||||||
|
LABEL_DRAFT_STORAGE_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
form: {
|
||||||
|
name: 'Draft yogurt',
|
||||||
|
productionDate: '2026-04-11',
|
||||||
|
stockType: 'descriptive',
|
||||||
|
},
|
||||||
|
savedAt: Date.now() - (29 * 60 * 1000),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = labelCreatePageData({
|
||||||
|
isConnected: false,
|
||||||
|
activeKitchen: { id: 1 },
|
||||||
|
addAlert: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(data.form.name).toBe('Draft yogurt');
|
||||||
|
expect(data.form.productionDate).toBe('2026-04-11');
|
||||||
|
expect(data.form.stockType).toBe('descriptive');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops stale enveloped drafts at 30 minutes inactivity and loads a clean form', () => {
|
||||||
|
localStorageState.set(
|
||||||
|
LABEL_DRAFT_STORAGE_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
form: {
|
||||||
|
name: 'Old draft',
|
||||||
|
description: 'Should be removed',
|
||||||
|
productionDate: '2026-04-10',
|
||||||
|
stockType: 'measured',
|
||||||
|
quantity: '4',
|
||||||
|
uom: 'kg',
|
||||||
|
},
|
||||||
|
savedAt: Date.now() - (30 * 60 * 1000),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = labelCreatePageData({
|
||||||
|
isConnected: false,
|
||||||
|
activeKitchen: { id: 1 },
|
||||||
|
addAlert: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(data.form.name).toBe('');
|
||||||
|
expect(data.form.description).toBe('');
|
||||||
|
expect(data.form.stockType).toBe('binary');
|
||||||
|
expect(data.form.quantity).toBe('');
|
||||||
|
expect(data.form.uom).toBe('g');
|
||||||
|
expect(data.form.productionDate).toBe('2026-04-12');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps draft when day changes but inactivity stays below 30 minutes', () => {
|
||||||
|
vi.setSystemTime(new Date('2026-04-12T00:10:00Z'));
|
||||||
|
localStorageState.set(
|
||||||
|
LABEL_DRAFT_STORAGE_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
form: {
|
||||||
|
name: 'Day-changed draft',
|
||||||
|
productionDate: '2026-04-11',
|
||||||
|
},
|
||||||
|
savedAt: Date.now() - (10 * 60 * 1000),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = labelCreatePageData({
|
||||||
|
isConnected: false,
|
||||||
|
activeKitchen: { id: 1 },
|
||||||
|
addAlert: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(data.form.name).toBe('Day-changed draft');
|
||||||
|
expect(data.form.productionDate).toBe('2026-04-11');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads legacy plain-object drafts without forcing discard', () => {
|
||||||
|
localStorageState.set(
|
||||||
|
LABEL_DRAFT_STORAGE_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
name: 'Legacy draft',
|
||||||
|
productionDate: '2026-04-05',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = labelCreatePageData({
|
||||||
|
isConnected: false,
|
||||||
|
activeKitchen: { id: 1 },
|
||||||
|
addAlert: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(data.form.name).toBe('Legacy draft');
|
||||||
|
expect(data.form.productionDate).toBe('2026-04-05');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes enveloped draft payload from persist and reset save paths', () => {
|
||||||
|
const data = labelCreatePageData({
|
||||||
|
isConnected: false,
|
||||||
|
activeKitchen: { id: 1 },
|
||||||
|
addAlert: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
data.form = {
|
||||||
|
...data.form,
|
||||||
|
name: 'Persisted entry',
|
||||||
|
search: 'temp search value',
|
||||||
|
};
|
||||||
|
data.persistDraft();
|
||||||
|
|
||||||
|
const persistedDraft = readStoredLabelDraft();
|
||||||
|
expect(persistedDraft.savedAt).toBe(Date.now());
|
||||||
|
expect(persistedDraft.form.name).toBe('Persisted entry');
|
||||||
|
expect(persistedDraft.form.search).toBe('');
|
||||||
|
|
||||||
|
vi.setSystemTime(new Date('2026-04-12T12:05:00Z'));
|
||||||
|
data.form.name = 'Reset me';
|
||||||
|
data.$refs = {};
|
||||||
|
data.reset(false);
|
||||||
|
|
||||||
|
const resetDraft = readStoredLabelDraft();
|
||||||
|
expect(resetDraft.savedAt).toBe(Date.now());
|
||||||
|
expect(resetDraft.form.name).toBe('');
|
||||||
|
expect(resetDraft.form.productionDate).toBe('2026-04-12');
|
||||||
|
});
|
||||||
|
|
||||||
it('builds upsert payload with selected template uuid', () => {
|
it('builds upsert payload with selected template uuid', () => {
|
||||||
const store = {
|
const store = {
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
@@ -96,6 +264,13 @@ describe('label create upsert-first submit', () => {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
message: 'Rice was updated successfully.',
|
message: 'Rice was updated successfully.',
|
||||||
});
|
});
|
||||||
|
const savedDraft = readStoredLabelDraft();
|
||||||
|
expect(savedDraft).toMatchObject({
|
||||||
|
form: {
|
||||||
|
name: 'Rice',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(savedDraft.savedAt).toBeTypeOf('number');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('create shows parsed print issue warning when printing fails', async () => {
|
it('create shows parsed print issue warning when printing fails', async () => {
|
||||||
@@ -129,5 +304,6 @@ describe('label create upsert-first submit', () => {
|
|||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: 'Beans was created, but printing has an issue: Printer is unavailable.',
|
message: 'Beans was created, but printing has an issue: Printer is unavailable.',
|
||||||
});
|
});
|
||||||
|
expect(localStorageMock.setItem).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user