mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
release: v1.32.0 — hardening and scalability (#580)
* release: prepare v1.32.0 * test: stabilize water capture re-entry
This commit is contained in:
1 parent
b01822be48
commit
62eb431d66
372 files changed
+25178
-8561
No files matched your search
@@ -145,7 +145,10 @@ describe("MoodList — rated factor editing", () => {
|
||||
expect(listSrc).toContain(
|
||||
"const [editRatedFactors, setEditRatedFactors] = useState<",
|
||||
);
|
||||
expect(listSrc).toContain("setEditRatedFactors(entry.ratedFactors ?? [])");
|
||||
expect(listSrc).toContain(
|
||||
"ratedFactors: [...(entry.ratedFactors ?? [])].sort",
|
||||
);
|
||||
expect(listSrc).toContain("setEditRatedFactors(seed.ratedFactors)");
|
||||
expect(listSrc).toMatch(
|
||||
/<MoodTagPicker[\s\S]*ratedFactors=\{editRatedFactors\}[\s\S]*onRateFactor=\{rateEditFactor\}/,
|
||||
);
|
||||
|
||||
@@ -42,7 +42,8 @@ import {
|
||||
moodDependentKeys,
|
||||
refetchInactiveDailyReads,
|
||||
} from "@/lib/query-keys";
|
||||
import { ApiError, apiPost } from "@/lib/api/api-fetch";
|
||||
import { apiPost } from "@/lib/api/api-fetch";
|
||||
import { localizedApiError } from "@/lib/api/localized-error";
|
||||
import { SheetSection, SheetSectionCount } from "@/components/ui/sheet-section";
|
||||
import { MoodTagPicker, type RatedFactor } from "./mood-tag-picker";
|
||||
import { MoodQuickTags } from "./mood-quick-tags";
|
||||
@@ -224,11 +225,7 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
|
||||
toast.success(t("common.saved"));
|
||||
onSuccess?.();
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof ApiError && err.message
|
||||
? err.message
|
||||
: t("mood.saveError"),
|
||||
);
|
||||
setError(localizedApiError(err, t, "mood.saveError"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -287,8 +284,7 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
|
||||
|
||||
// v1.17.0 — the "Note & details" badge counts the free-text tags entered
|
||||
// plus the note (when present), so a collapsed section communicates what
|
||||
// it holds. The auto-populated timestamp is excluded (it always carries a
|
||||
// default, so it would never read as "empty").
|
||||
// it holds.
|
||||
const freeTextTagCount = tagsInput
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
@@ -365,6 +361,20 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
|
||||
onExpand={() => setMoreTagsOpen(true)}
|
||||
/>
|
||||
|
||||
<FieldGroup htmlFor="mood-logged-at" label={t("mood.timestamp")}>
|
||||
<DateTimeField
|
||||
id="mood-logged-at"
|
||||
value={moodLoggedAt}
|
||||
onChange={setMoodLoggedAt}
|
||||
// v1.17 W1b — match the server bound: no future instant.
|
||||
max={getDefaultMoodLoggedAtValue()}
|
||||
required
|
||||
aria-required="true"
|
||||
aria-invalid={!!error || undefined}
|
||||
aria-describedby={errorDescriptor}
|
||||
/>
|
||||
</FieldGroup>
|
||||
|
||||
{/* v1.17.0 — "More tags": the full structured-tag taxonomy
|
||||
(binary tiles + custom). Controlled so the Quick row's "+" can
|
||||
open it; the badge reports how many tags are selected. */}
|
||||
@@ -399,8 +409,8 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
|
||||
</SheetSection>
|
||||
|
||||
{/* v1.17.0 — "Note & details": free-text tags, GLP-1 quick-tags,
|
||||
the note field, and the timestamp. The badge counts whichever
|
||||
of these carry input. */}
|
||||
and the note field. The badge counts whichever of these carry
|
||||
input. */}
|
||||
<SheetSection
|
||||
title={t("mood.sectionNoteDetails")}
|
||||
icon={<StickyNote />}
|
||||
@@ -518,20 +528,6 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
|
||||
rows={3}
|
||||
/>
|
||||
</FieldGroup>
|
||||
|
||||
<FieldGroup htmlFor="mood-logged-at" label={t("mood.timestamp")}>
|
||||
<DateTimeField
|
||||
id="mood-logged-at"
|
||||
value={moodLoggedAt}
|
||||
onChange={setMoodLoggedAt}
|
||||
// v1.17 W1b — match the server bound: no future instant.
|
||||
max={getDefaultMoodLoggedAtValue()}
|
||||
required
|
||||
aria-required="true"
|
||||
aria-invalid={!!error || undefined}
|
||||
aria-describedby={errorDescriptor}
|
||||
/>
|
||||
</FieldGroup>
|
||||
</SheetSection>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -23,6 +23,10 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import {
|
||||
SeededFormDiscardDialog,
|
||||
useSeededFormDismissal,
|
||||
} from "@/components/forms/use-seeded-form-dismissal";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -83,6 +87,7 @@ import {
|
||||
import { useRovingRadioGroup } from "@/hooks/use-roving-radio-group";
|
||||
import { MoodTagPicker, type RatedFactor } from "./mood-tag-picker";
|
||||
import { apiDelete, apiGet, apiPost, apiPut } from "@/lib/api/api-fetch";
|
||||
import { localizedApiError } from "@/lib/api/localized-error";
|
||||
|
||||
// Re-export the score map under the legacy local name to keep the
|
||||
// rest of this file unchanged. v1.4.27 B6 / BL-P6-11 — the single
|
||||
@@ -178,6 +183,21 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
const [editRatedFactors, setEditRatedFactors] = useState<RatedFactor[]>([]);
|
||||
const [editNote, setEditNote] = useState("");
|
||||
const [editMoodLoggedAt, setEditMoodLoggedAt] = useState("");
|
||||
const [editSeed, setEditSeed] = useState<{
|
||||
mood: string;
|
||||
tagsInput: string;
|
||||
tagKeys: string[];
|
||||
ratedFactors: RatedFactor[];
|
||||
note: string;
|
||||
moodLoggedAt: string;
|
||||
}>({
|
||||
mood: "",
|
||||
tagsInput: "",
|
||||
tagKeys: [],
|
||||
ratedFactors: [],
|
||||
note: "",
|
||||
moodLoggedAt: "",
|
||||
});
|
||||
const [editError, setEditError] = useState<string | null>(null);
|
||||
|
||||
function toggleEditTagKey(key: string) {
|
||||
@@ -390,41 +410,78 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
onSuccess: async () => {
|
||||
await invalidateKeys(queryClient, moodDependentKeys);
|
||||
await refetchInactiveDailyReads(queryClient);
|
||||
setEditing(null);
|
||||
setEditError(null);
|
||||
setEditSeed({
|
||||
mood: editMood,
|
||||
tagsInput: editTagsInput,
|
||||
tagKeys: [...editTagKeys].sort(),
|
||||
ratedFactors: [...editRatedFactors].sort((a, b) =>
|
||||
a.key.localeCompare(b.key),
|
||||
),
|
||||
note: editNote,
|
||||
moodLoggedAt: editMoodLoggedAt,
|
||||
});
|
||||
dismissEdit();
|
||||
},
|
||||
onError: (err) => {
|
||||
setEditError(err instanceof Error ? err.message : t("mood.saveError"));
|
||||
setEditError(localizedApiError(err, t, "mood.saveError"));
|
||||
},
|
||||
});
|
||||
const editDismissal = useSeededFormDismissal({
|
||||
seed: editSeed,
|
||||
value: {
|
||||
mood: editMood,
|
||||
tagsInput: editTagsInput,
|
||||
tagKeys: [...editTagKeys].sort(),
|
||||
ratedFactors: [...editRatedFactors].sort((a, b) =>
|
||||
a.key.localeCompare(b.key),
|
||||
),
|
||||
note: editNote,
|
||||
moodLoggedAt: editMoodLoggedAt,
|
||||
},
|
||||
blocked: updateMutation.isPending || deleteMutation.isPending,
|
||||
});
|
||||
|
||||
const totalPages = data ? Math.ceil(data.meta.total / PAGE_SIZE) : 0;
|
||||
|
||||
function startEdit(entry: MoodEntry) {
|
||||
const seed = {
|
||||
mood: entry.mood,
|
||||
tagsInput: entry.tags.join(", "),
|
||||
tagKeys: [...(entry.tagKeys ?? [])].sort(),
|
||||
ratedFactors: [...(entry.ratedFactors ?? [])].sort((a, b) =>
|
||||
a.key.localeCompare(b.key),
|
||||
),
|
||||
note: entry.note ?? "",
|
||||
moodLoggedAt: toDateTimeLocalValue(entry.moodLoggedAt),
|
||||
};
|
||||
setEditing(entry);
|
||||
setEditMood(entry.mood);
|
||||
setEditTagsInput(entry.tags.join(", "));
|
||||
setEditTagKeys(entry.tagKeys ?? []);
|
||||
setEditRatedFactors(entry.ratedFactors ?? []);
|
||||
setEditNote(entry.note ?? "");
|
||||
setEditMoodLoggedAt(toDateTimeLocalValue(entry.moodLoggedAt));
|
||||
setEditMood(seed.mood);
|
||||
setEditTagsInput(seed.tagsInput);
|
||||
setEditTagKeys(seed.tagKeys);
|
||||
setEditRatedFactors(seed.ratedFactors);
|
||||
setEditNote(seed.note);
|
||||
setEditMoodLoggedAt(seed.moodLoggedAt);
|
||||
setEditSeed(seed);
|
||||
setEditError(null);
|
||||
}
|
||||
|
||||
function closeEdit() {
|
||||
if (updateMutation.isPending || deleteMutation.isPending) return;
|
||||
function dismissEdit() {
|
||||
setEditing(null);
|
||||
setEditError(null);
|
||||
setEditDeleteDialogOpen(false);
|
||||
}
|
||||
|
||||
function closeEdit() {
|
||||
editDismissal.requestClose(dismissEdit);
|
||||
}
|
||||
|
||||
async function deleteEditingEntry() {
|
||||
if (!editing) return;
|
||||
|
||||
try {
|
||||
setEditError(null);
|
||||
await deleteMutation.mutateAsync(editing.id);
|
||||
closeEdit();
|
||||
dismissEdit();
|
||||
} catch {
|
||||
setEditError(t("mood.deleteError"));
|
||||
}
|
||||
@@ -1044,6 +1101,11 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
</form>
|
||||
)}
|
||||
</ResponsiveSheet>
|
||||
<SeededFormDiscardDialog
|
||||
open={editDismissal.discardDialogOpen}
|
||||
onConfirm={editDismissal.confirmDiscard}
|
||||
onCancel={editDismissal.cancelDiscard}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user