mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 22:59:04 +02:00
* fix(issues): store start_date/due_date as DATE, not timestamp (MUL-2925) These fields are calendar days (the pickers offer no time-of-day), but were stored as TIMESTAMPTZ. A client serializing local midnight via toISOString() folded its timezone into the instant, so the day shifted by the local offset (GH #3618). Migrate the columns to DATE and parse/serialize date-only "YYYY-MM-DD". ParseCalendarDate still accepts legacy RFC3339 (truncated to the UTC day) so older clients keep working. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): render start_date/due_date as timezone-stable calendar days (MUL-2925) Pickers now emit date-only "YYYY-MM-DD" (local calendar day) instead of toISOString(), and every read formats via the shared @multica/core/issues/date helpers with timeZone:"UTC" so the day never shifts with the viewer's offset. The Gantt's existing UTC bucketing is now correct. Covers web/desktop pickers, quick-set menu, list/board/detail/activity, and the mobile due-date picker. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): address date-only review — loud-fail ambiguous dates, finish display sweep (MUL-2925) Review follow-ups on #3692: - ParseCalendarDate no longer silently truncates a legacy non-midnight RFC3339 to the wrong UTC day; it accepts only YYYY-MM-DD or an exact UTC-midnight instant and rejects ambiguous ones loudly. Adds util unit tests. - migration 112 pins the TIMESTAMPTZ->DATE conversion to UTC explicitly via AT TIME ZONE 'UTC' (was session-timezone dependent); down migration too. - Convert remaining date-change display sites to formatDateOnly: inbox detail label (web) and mobile activity + inbox labels (were new Date()+local format). - CLI --start-date/--due-date help now says YYYY-MM-DD, not RFC3339. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
139 lines
4.8 KiB
TypeScript
139 lines
4.8 KiB
TypeScript
/**
|
|
* Bottom chip row for the new-issue form. Mirrors `attribute-row.tsx`'s
|
|
* visual pattern but operates on the `useNewIssueDraftStore` instead of an
|
|
* `issue` object + mutation. Tapping a chip pushes a formSheet picker
|
|
* route under `new-issue-picker/<field>` — the route reads/writes the same
|
|
* draft store, so the chip rehydrates automatically when the sheet
|
|
* dismisses.
|
|
*
|
|
* Why a draft store: the picker routes are siblings of new-issue.tsx in
|
|
* the Stack — they can't reach into the new-issue screen's local state.
|
|
* The draft store is the cross-screen channel.
|
|
*/
|
|
import { View } from "react-native";
|
|
import { router } from "expo-router";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { AttributeChip } from "@/components/issue/attribute-chip";
|
|
import { ActorAvatar } from "@/components/ui/actor-avatar";
|
|
import { PriorityIcon } from "@/components/ui/priority-icon";
|
|
import { ProjectIcon } from "@/components/ui/project-icon";
|
|
import { StatusIcon } from "@/components/ui/status-icon";
|
|
import { formatDateOnly } from "@multica/core/issues/date";
|
|
import { useActorLookup } from "@/data/use-actor-name";
|
|
import { useNewIssueDraftStore } from "@/data/stores/new-issue-draft-store";
|
|
import { useWorkspaceStore } from "@/data/workspace-store";
|
|
import { PRIORITY_LABEL, STATUS_LABEL } from "@/lib/issue-status";
|
|
|
|
/**
|
|
* Picker fields the new-issue draft form can open. Bound to a typed map
|
|
* of Expo Router pathnames so typos become compile errors (previously
|
|
* the call site used `as never` on a template string).
|
|
*/
|
|
type NewIssuePickerField =
|
|
| "status"
|
|
| "priority"
|
|
| "assignee"
|
|
| "project"
|
|
| "due-date";
|
|
|
|
const NEW_ISSUE_PICKER_PATHNAMES = {
|
|
status: "/[workspace]/new-issue-picker/status",
|
|
priority: "/[workspace]/new-issue-picker/priority",
|
|
assignee: "/[workspace]/new-issue-picker/assignee",
|
|
project: "/[workspace]/new-issue-picker/project",
|
|
"due-date": "/[workspace]/new-issue-picker/due-date",
|
|
} as const satisfies Record<NewIssuePickerField, string>;
|
|
|
|
export function CreateFormAttributeRow() {
|
|
const wsSlug = useWorkspaceStore((s) => s.currentWorkspaceSlug);
|
|
const status = useNewIssueDraftStore((s) => s.status);
|
|
const priority = useNewIssueDraftStore((s) => s.priority);
|
|
const assignee = useNewIssueDraftStore((s) => s.assignee);
|
|
const dueDate = useNewIssueDraftStore((s) => s.dueDate);
|
|
const project = useNewIssueDraftStore((s) => s.project);
|
|
|
|
const { getName } = useActorLookup();
|
|
const assigneeLabel = assignee
|
|
? getName(assignee.type, assignee.id)
|
|
: "Assignee";
|
|
const priorityLabel =
|
|
priority === "none" ? "Priority" : PRIORITY_LABEL[priority];
|
|
|
|
const open = (field: NewIssuePickerField) => {
|
|
if (!wsSlug) return;
|
|
router.push({
|
|
pathname: NEW_ISSUE_PICKER_PATHNAMES[field],
|
|
params: { workspace: wsSlug },
|
|
});
|
|
};
|
|
|
|
return (
|
|
<View>
|
|
<View className="flex-row flex-wrap gap-2">
|
|
<AttributeChip
|
|
icon={<StatusIcon status={status} size={12} />}
|
|
label={STATUS_LABEL[status]}
|
|
variant="filled"
|
|
onPress={() => open("status")}
|
|
/>
|
|
<AttributeChip
|
|
icon={<PriorityIcon priority={priority} />}
|
|
label={priorityLabel}
|
|
variant={priority === "none" ? "dimmed" : "filled"}
|
|
onPress={() => open("priority")}
|
|
/>
|
|
<AttributeChip
|
|
icon={
|
|
assignee ? (
|
|
<ActorAvatar
|
|
type={assignee.type}
|
|
id={assignee.id}
|
|
size={16}
|
|
showPresence
|
|
/>
|
|
) : (
|
|
<Ionicons
|
|
name="person-circle-outline"
|
|
size={16}
|
|
color="#a1a1aa"
|
|
/>
|
|
)
|
|
}
|
|
label={assigneeLabel}
|
|
variant={assignee ? "filled" : "dimmed"}
|
|
onPress={() => open("assignee")}
|
|
/>
|
|
<AttributeChip
|
|
icon={
|
|
<Ionicons
|
|
name="calendar-outline"
|
|
size={14}
|
|
color={dueDate ? undefined : "#a1a1aa"}
|
|
/>
|
|
}
|
|
label={dueDate ? formatDueDate(dueDate) : "Due date"}
|
|
variant={dueDate ? "filled" : "dimmed"}
|
|
onPress={() => open("due-date")}
|
|
/>
|
|
<AttributeChip
|
|
icon={
|
|
project ? (
|
|
<ProjectIcon icon={project.icon} size="sm" />
|
|
) : (
|
|
<Ionicons name="folder-outline" size={14} color="#a1a1aa" />
|
|
)
|
|
}
|
|
label={project?.title ?? "Project"}
|
|
variant={project ? "filled" : "dimmed"}
|
|
onPress={() => open("project")}
|
|
/>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
// due_date is a calendar day — format timezone-safely (no offset day shift).
|
|
function formatDueDate(iso: string): string {
|
|
return formatDateOnly(iso, { month: "short", day: "numeric" }) || "Due date";
|
|
}
|