fix(review): carry Measure species into Create title/short-desc
Live overnight slabs after removing the Review species picker had species_id null, so Create title stayed gated off. Require species on Measure Calculate, stop the select sync from wiping choices before options load, and have Review gate/PUT from slab.species_id with a Capture handoff repair so deterministic title/short-desc always see it.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
type Slab, type Taxonomy, type TaxCategory, type TaxTag, type SettingsView, type PhotoRow,
|
||||
} from '../../api/slabs';
|
||||
import type { CalibratedCapture } from '../flow';
|
||||
import { canCreateTitleFromSpecies, effectiveSpeciesId } from './speciesFromCapture';
|
||||
import MultiSelect from './MultiSelect.svelte';
|
||||
import { deriveThicknessTermId } from './thicknessBand';
|
||||
import {
|
||||
@@ -92,7 +93,7 @@
|
||||
|
||||
// --- Editable model (mirrors the live slab) ---
|
||||
// Species is set on Measure / createSlab — Review only reads it (no picker).
|
||||
// Kept as a string so canCreateTitle can gate on a non-empty value from hydrate.
|
||||
// speciesSel mirrors the resolved id for hydrate/debug; gates use effectiveSpeciesId.
|
||||
let speciesSel: string = '';
|
||||
// #90: single-selects bind a STRING var to the native <select>, because
|
||||
// <option value={id}> coerces the option value to a string. Binding a
|
||||
@@ -131,8 +132,9 @@
|
||||
// being enabled.
|
||||
let createBusy = false;
|
||||
let createError = '';
|
||||
$: canCreateTitle =
|
||||
speciesSel !== '' && woodCatIds.length > 0;
|
||||
// Prefer slab.species_id; fall back to Capture handoff if createSlab omitted it.
|
||||
$: resolvedSpeciesId = effectiveSpeciesId(slab?.species_id, capture?.speciesId ?? null);
|
||||
$: canCreateTitle = canCreateTitleFromSpecies(resolvedSpeciesId, woodCatIds.length);
|
||||
$: canCreateDesc =
|
||||
canGenerateContent && title.trim() !== '' && shortDescription.trim() !== '';
|
||||
// Monotonic token: each commit captures the current value and ignores its
|
||||
@@ -318,7 +320,8 @@
|
||||
}
|
||||
|
||||
function hydrate(s: Slab, t: Taxonomy | null) {
|
||||
speciesSel = s.species_id != null ? String(s.species_id) : '';
|
||||
const sid = effectiveSpeciesId(s.species_id, capture?.speciesId ?? null);
|
||||
speciesSel = sid != null ? String(sid) : '';
|
||||
woodCatIds = [...(s.wood_category_ids ?? [])];
|
||||
edgeSel = s.edge_type_term_id != null ? String(s.edge_type_term_id) : '';
|
||||
figureIds = [...(s.figure_term_ids ?? [])];
|
||||
@@ -376,6 +379,14 @@
|
||||
reviewBusy = true;
|
||||
reviewError = '';
|
||||
try {
|
||||
// Repair Measure species onto the row before probing title (review reads the DB).
|
||||
if (resolvedSpeciesId != null && slab.species_id == null) {
|
||||
slab = await updateSlab(slabId, {
|
||||
species_id: resolvedSpeciesId,
|
||||
client_rev: slab.server_rev,
|
||||
});
|
||||
if (myToken !== commitToken) return;
|
||||
}
|
||||
// Run the deterministic review FIRST (reads the current metadata) to get the
|
||||
// title/short_description build before we persist.
|
||||
// Do NOT send price/sku here: review POST persists those overrides and bumps
|
||||
@@ -389,9 +400,9 @@
|
||||
if (probe.title && title === '') title = probe.title;
|
||||
if (probe.short_description && shortDescription === '') shortDescription = probe.short_description;
|
||||
|
||||
// species_id intentionally omitted — set at createSlab from Measure; Review
|
||||
// must not clear or re-edit it.
|
||||
const patch = {
|
||||
// Always echo known species_id (slab or Capture repair) so Review PUTs never
|
||||
// leave a Measure-chosen species off the row; never send null (would wipe).
|
||||
const patch: Record<string, unknown> = {
|
||||
wood_category_ids: woodCatIds,
|
||||
edge_type_term_id: edgeSel === '' ? null : Number(edgeSel),
|
||||
figure_term_ids: figureIds,
|
||||
@@ -407,7 +418,8 @@
|
||||
sku: sku || null,
|
||||
client_rev: slab.server_rev,
|
||||
};
|
||||
const fresh = await updateSlab(slabId, patch);
|
||||
if (resolvedSpeciesId != null) patch.species_id = resolvedSpeciesId;
|
||||
const fresh = await updateSlab(slabId, patch as Parameters<typeof updateSlab>[1]);
|
||||
if (myToken !== commitToken) return; // superseded; don't clobber the latest
|
||||
slab = fresh;
|
||||
// Re-run the review against the just-persisted row (authoritative ready/missing).
|
||||
@@ -418,6 +430,9 @@
|
||||
});
|
||||
if (myToken !== commitToken) return;
|
||||
if (review.primary_wood_category_id != null) primaryCatId = review.primary_wood_category_id;
|
||||
// Post-PUT review may newly have species+wood cats — fill empty Create fields.
|
||||
if (review.title && title === '') title = review.title;
|
||||
if (review.short_description && shortDescription === '') shortDescription = review.short_description;
|
||||
} catch (e) {
|
||||
if (myToken !== commitToken) return;
|
||||
reviewError = e instanceof ApiError ? e.message : 'Could not review. Try again.';
|
||||
@@ -437,6 +452,14 @@
|
||||
createBusy = true;
|
||||
createError = '';
|
||||
try {
|
||||
// 0. Repair: if Measure chose a species but createSlab omitted it, write it
|
||||
// before the deterministic build (review reads species_id from the row).
|
||||
if (resolvedSpeciesId != null && slab.species_id == null) {
|
||||
slab = await updateSlab(slabId, {
|
||||
species_id: resolvedSpeciesId,
|
||||
client_rev: slab.server_rev,
|
||||
});
|
||||
}
|
||||
// 1. Run the deterministic review to get title / short_description.
|
||||
const res = await reviewSlab(slabId, {
|
||||
primary_wood_category_id: primaryCatId === '' ? undefined : Number(primaryCatId),
|
||||
@@ -447,10 +470,11 @@
|
||||
if (res.short_description) shortDescription = res.short_description;
|
||||
// 3. Persist the generated prose (single PUT, fresh client_rev).
|
||||
if ((title || shortDescription) && (title !== (slab as Slab).title || shortDescription !== (slab as Slab).short_description)) {
|
||||
const prosePatch = {
|
||||
const prosePatch: Parameters<typeof updateSlab>[1] = {
|
||||
title: title || null,
|
||||
short_description: shortDescription || null,
|
||||
client_rev: (slab as Slab).server_rev,
|
||||
...(resolvedSpeciesId != null ? { species_id: resolvedSpeciesId } : {}),
|
||||
};
|
||||
const fresh = await updateSlab(slabId, prosePatch);
|
||||
slab = fresh;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { canCreateTitleFromSpecies, effectiveSpeciesId } from './speciesFromCapture';
|
||||
|
||||
test('effectiveSpeciesId prefers slab over capture', () => {
|
||||
assert.equal(effectiveSpeciesId(281, 11), 281);
|
||||
assert.equal(effectiveSpeciesId(null, 11), 11);
|
||||
assert.equal(effectiveSpeciesId(undefined, 11), 11);
|
||||
assert.equal(effectiveSpeciesId(null, null), null);
|
||||
assert.equal(effectiveSpeciesId(undefined, undefined), null);
|
||||
});
|
||||
|
||||
test('effectiveSpeciesId ignores non-finite values', () => {
|
||||
assert.equal(effectiveSpeciesId(Number.NaN, 11), 11);
|
||||
assert.equal(effectiveSpeciesId(null, Number.NaN), null);
|
||||
});
|
||||
|
||||
test('canCreateTitleFromSpecies requires species + ≥1 wood category', () => {
|
||||
assert.equal(canCreateTitleFromSpecies(281, 1), true);
|
||||
assert.equal(canCreateTitleFromSpecies(281, 0), false);
|
||||
assert.equal(canCreateTitleFromSpecies(null, 2), false);
|
||||
assert.equal(canCreateTitleFromSpecies(null, 0), false);
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/** Resolve Woo species leaf for Review Create-title gate + PUTs.
|
||||
*
|
||||
* Species is chosen on Measure and stored on the slab at createSlab. Review has
|
||||
* no picker; Create title/short-desc read species from the slab (Capture
|
||||
* handoff is a repair fallback when create omitted it).
|
||||
*/
|
||||
|
||||
export function effectiveSpeciesId(
|
||||
slabSpeciesId: number | null | undefined,
|
||||
captureSpeciesId: number | null | undefined,
|
||||
): number | null {
|
||||
if (slabSpeciesId != null && Number.isFinite(Number(slabSpeciesId))) {
|
||||
return Math.round(Number(slabSpeciesId));
|
||||
}
|
||||
if (captureSpeciesId != null && Number.isFinite(Number(captureSpeciesId))) {
|
||||
return Math.round(Number(captureSpeciesId));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function canCreateTitleFromSpecies(
|
||||
speciesId: number | null,
|
||||
woodCategoryCount: number,
|
||||
): boolean {
|
||||
return speciesId != null && woodCategoryCount > 0;
|
||||
}
|
||||
Reference in New Issue
Block a user