Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const periodicReportEditor = {
{ key: "profile_preset" },
{ key: "route_ref" },
{ key: "timezone" },
{ key: "schedule", nullable: true },
],
};

Expand Down Expand Up @@ -65,3 +66,10 @@ for (const invalid of ['{', 'null', '[]', 'true', '{"schema_version":"injected"}
}

console.log("capability configuration projection and JSON boundary smoke: ok");

const schedule = { schema_version: "periodic_report_schedule_v0", schedule_id: "weekly",
rrule: "FREQ=WEEKLY;BYDAY=FR;BYHOUR=18;BYMINUTE=0", timezone: "Asia/Shanghai" };
assert.deepEqual(projectEditableCapabilityConfiguration(periodicReportEditor, { schedule }), { schedule });
assert.deepEqual(parseEditableCapabilityJson(periodicReportEditor, JSON.stringify({ schedule })), { schedule });
assert.deepEqual(projectEditableCapabilityConfiguration(periodicReportEditor, { schedule: null }, { schedule }), { schedule: null },
"explicit nullable clear must not restore the inherited schedule on editor mode changes");
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type CapabilityConfigurationFieldDescriptor = {
key: string;
nullable?: boolean;
};

export type CapabilityConfigurationEditorDescriptor = {
Expand Down Expand Up @@ -27,10 +28,12 @@ export function projectEditableCapabilityConfiguration(
): Record<string, unknown> {
const primary = configurationObject(value);
const defaults = configurationObject(fallback);
return Object.fromEntries(editor.fields.flatMap(({ key }) => {
return Object.fromEntries(editor.fields.flatMap(({ key, nullable }) => {
const primaryValue = primary[key];
const defaultValue = defaults[key];
if (Object.hasOwn(primary, key) && primaryValue != null) return [[key, primaryValue]];
// Explicit null clears an optional value; it must not resurrect a default
// when switching between guided and JSON editors.
if (Object.hasOwn(primary, key) && (primaryValue != null || (nullable && primaryValue === null))) return [[key, primaryValue]];
if (Object.hasOwn(defaults, key) && defaultValue != null) return [[key, defaultValue]];
return [];
}));
Expand Down
11 changes: 10 additions & 1 deletion apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,13 +1057,21 @@ export async function configureGoalChannelAutoNotify(options: { autoNotify: bool
);
}

export const periodicReportScheduleSchema = z.object({
schema_version: z.literal("periodic_report_schedule_v0"),
schedule_id: z.string(),
rrule: z.string(),
timezone: z.string(),
});

export const periodicReportMachineConfigurationSchema = z.object({
schema_version: z.literal("periodic_report_machine_defaults_v0"),
enabled: z.boolean(),
inheritance: z.literal("live_machine_default"),
profile_preset: z.string().optional(),
route_ref: z.string().optional(),
timezone: z.string(),
schedule: periodicReportScheduleSchema.nullable().optional(),
});

export const machineConfigurationSchema = z.object({
Expand All @@ -1089,7 +1097,8 @@ export const capabilityConfigurationFieldSchema = z.object({
key: z.string(),
label: z.string(),
description: z.string(),
input_kind: z.enum(["boolean", "number", "select", "string_list", "text"]),
input_kind: z.enum(["boolean", "number", "select", "string_list", "text", "periodic_report_schedule"]),
nullable: z.boolean().optional(),
required: z.boolean(),
minimum: z.number().int().optional(),
maximum: z.number().int().optional(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useId, type ReactNode } from "react";

import type { CapabilityConfigurationEditor } from "../../data/chat";
import { PeriodicReportScheduleField } from "./periodic-report-schedule-field";

type FieldCopy = Record<string, { description?: string; label?: string }>;
type ConfigurationField = CapabilityConfigurationEditor["fields"][number];
type FieldValue = boolean | number | string | string[];
type FieldValue = boolean | number | string | string[] | Record<string, unknown> | null;
type FieldChange = (key: string, value: FieldValue) => void;

type ConfigurationFieldProps = Readonly<{
Expand All @@ -13,12 +14,18 @@
id: string;
onChange?: FieldChange;
value: unknown;
timezone: string;
}>;

function ConfigurationFieldControl({ copy, field, id, onChange, value }: ConfigurationFieldProps) {
function ConfigurationFieldControl({ copy, field, id, onChange, value, timezone }: ConfigurationFieldProps) {

Check failure on line 20 in apps/presentation/dashboard/src/features/personal-workspace/capability-configuration-fields.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 52 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCTgjMrAB_sm_IMQayO&open=AaCTgjMrAB_sm_IMQayO&pullRequest=4263
const label = copy[field.key]?.label ?? field.label;
const readOnly = !onChange;

if (field.input_kind === "periodic_report_schedule") {
return <PeriodicReportScheduleField id={id} value={value} timezone={timezone}
onChange={onChange ? (schedule) => onChange(field.key, schedule) : undefined} />;
}

if (field.input_kind === "boolean") {
return (
<label className="is-boolean" htmlFor={id}>
Expand Down Expand Up @@ -96,6 +103,7 @@
key={field.key}
onChange={onChange}
value={value[field.key]}
timezone={String(value.timezone ?? "UTC")}

Check warning on line 106 in apps/presentation/dashboard/src/features/personal-workspace/capability-configuration-fields.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'value.timezone ?? "UTC"' will use Object's default stringification format ('[object Object]') when stringified.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCTgjMrAB_sm_IMQayP&open=AaCTgjMrAB_sm_IMQayP&pullRequest=4263
/>;
return field.key === "enabled" && field.input_kind === "boolean"
? <div className="personal-capability-enabled-row" key={field.key}>{control}{enabledAction}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ const fieldCopy: Record<WorkspaceLocale, FieldCopy> = {
safe_fix: { label: "Allow one bounded safe-fix pass" },
strict_receipt: { label: "Require an exact-diff receipt" },
timezone: { label: "Timezone", description: "Use an IANA timezone, for example Asia/Shanghai." },
schedule: { label: "Calendar reports", description: "Optional daily or weekly reports; no schedule preserves stage-only delivery." },
},
"zh-CN": {
completed_todos: { label: "两次 Goal 复核间的已完成 Todo 数", description: "可设置 1–5;机器默认值可被 Goal 显式覆盖。" },
Expand All @@ -133,6 +134,7 @@ const fieldCopy: Record<WorkspaceLocale, FieldCopy> = {
safe_fix: { label: "允许一次有界安全修复" },
strict_receipt: { label: "要求精确 diff 回执" },
timezone: { label: "时区", description: "使用 IANA 时区,例如 Asia/Shanghai。" },
schedule: { label: "日历汇报", description: "可选每日或每周计划;未设置时保持阶段结束汇报。" },
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { parseEditableCapabilityJson, projectEditableCapabilityConfiguration } from "../../data/capability-configuration";
import { useWorkspaceI18n } from "./i18n";
import { CapabilityConfigurationFields } from "./capability-configuration-fields";
import { withReportScheduleTimezone } from "./periodic-report-schedule-field";
import { localizeCapability, localizedCapabilityFieldCopy } from "./capability-localization";
import { orderCapabilitiesForPresentation, canEditCapability, CapabilityCatalogNavigation, CapabilityConfigurationSummary, CapabilityDetailHeader, CapabilityEditorStatus } from "./capability-workbench";

Expand Down Expand Up @@ -110,10 +111,12 @@ function useCapabilityMutation({ goalId, onApplied, selected, t }: Readonly<{
}
}

function changeDraft(key: string, value: boolean | number | string | string[]) {
function changeDraft(key: string, value: unknown) {
setMutation((current) => ({
...current,
draft: { ...current.draft, [key]: value },
draft: selected?.capability_id === "periodic_report"
? withReportScheduleTimezone(current.draft, key, value)
: { ...current.draft, [key]: value },
preview: null,
}));
setError(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
} from "../../data/chat";
import { projectEditableCapabilityConfiguration } from "../../data/capability-configuration";
import { CapabilityConfigurationFields } from "./capability-configuration-fields";
import { withReportScheduleTimezone } from "./periodic-report-schedule-field";
import { localizeCapability, localizedCapabilityFieldCopy } from "./capability-localization";
import { canEditCapability, CapabilityCatalogNavigation, CapabilityConfigurationSummary, CapabilityDetailHeader, CapabilityEditorStatus, orderCapabilitiesForPresentation } from "./capability-workbench";
import { useWorkspaceI18n } from "./i18n";
Expand Down Expand Up @@ -154,8 +155,9 @@
setRollbackPlan(null);
}, [inspection, selectedCapabilityId, locale]);

function changeDraft(key: string, value: boolean | number | string | string[]) {
setDraft((current) => ({ ...current, [key]: value }));
function changeDraft(key: string, value: unknown) {
setDraft((current) => selected?.capability_id === "periodic_report"
? withReportScheduleTimezone(current, key, value) : { ...current, [key]: value });
setPreview(null);
setPreviewOperation("upsert");
setError(null);
Expand Down Expand Up @@ -300,7 +302,13 @@
{selected.capability_id === "periodic_report" ? (
<section className="personal-capability-behavior-note">
<ShieldCheck aria-hidden size={18} />
<div><strong>{t("machine.periodicReportActivation")}</strong><p>{t("machine.periodicReportActivationDescription")}</p></div>
<div><strong>{selectedCurrent?.schedule
? (locale === "zh-CN" ? "日历与阶段汇报" : "Calendar and stage reports")

Check warning on line 306 in apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCTgjMJAB_sm_IMQayK&open=AaCTgjMJAB_sm_IMQayK&pullRequest=4263
: t("machine.periodicReportActivation")}</strong><p>{selectedCurrent?.schedule
? selectedCurrent.enabled === true
? (locale === "zh-CN" ? "已配置日历计划,由现有唤醒检查;是否送达请核对报告回执。" : "A calendar schedule is configured and checked by existing wakes. Verify delivery in the report receipt.")

Check warning on line 309 in apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCTgjMJAB_sm_IMQayM&open=AaCTgjMJAB_sm_IMQayM&pullRequest=4263
: (locale === "zh-CN" ? "日历计划已保存;启用此能力后才会检查和投递。" : "The schedule is saved; enable this capability to check and deliver reports.")

Check warning on line 310 in apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCTgjMJAB_sm_IMQayL&open=AaCTgjMJAB_sm_IMQayL&pullRequest=4263

Check warning on line 310 in apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCTgjMJAB_sm_IMQayN&open=AaCTgjMJAB_sm_IMQayN&pullRequest=4263
: t("machine.periodicReportActivationDescription")}</p></div>
</section>
) : null}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useWorkspaceI18n } from "./i18n";

type Schedule = Record<string, unknown>;

export function withReportScheduleTimezone(
configuration: Record<string, unknown>, key: string, value: unknown,
): Record<string, unknown> {
const result = { ...configuration, [key]: value };
const schedule = configuration.schedule;
if (key === "timezone" && schedule && typeof schedule === "object"
&& "schema_version" in schedule && schedule.schema_version === "periodic_report_schedule_v0") {
result.schedule = { ...schedule, timezone: value };
}
return result;
}

export function PeriodicReportScheduleField({ id, value, timezone, onChange }: Readonly<{

Check failure on line 17 in apps/presentation/dashboard/src/features/personal-workspace/periodic-report-schedule-field.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 31 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCTgjJrAB_sm_IMQayI&open=AaCTgjJrAB_sm_IMQayI&pullRequest=4263
id: string;
value: unknown;
timezone: string;
onChange?: (value: Schedule | null) => void;
}>) {
const { locale } = useWorkspaceI18n();
const zh = locale === "zh-CN";
const schedule = value && typeof value === "object" && !Array.isArray(value) ? value as Schedule : null;
const fields = String(schedule?.rrule ?? "").split(";").map((part) => part.split("="));

Check warning on line 26 in apps/presentation/dashboard/src/features/personal-workspace/periodic-report-schedule-field.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'schedule?.rrule ?? ""' will use Object's default stringification format ('[object Object]') when stringified.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCTgjJrAB_sm_IMQayJ&open=AaCTgjJrAB_sm_IMQayJ&pullRequest=4263
const rule = Object.fromEntries(fields.filter((part) => part.length === 2));
const days = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
const inRange = (value: string | undefined, maximum: number) => value !== undefined
&& /^\d+$/.test(value) && Number(value) <= maximum;
const supported = !schedule || (schedule.schema_version === "periodic_report_schedule_v0"
&& ["DAILY", "WEEKLY"].includes(rule.FREQ)
&& schedule.timezone === timezone
&& fields.every((part) => part.length === 2 && ["FREQ", "BYDAY", "BYHOUR", "BYMINUTE", "INTERVAL"].includes(part[0]))
&& new Set(fields.map(([key]) => key)).size === fields.length
&& inRange(rule.BYHOUR, 23) && inRange(rule.BYMINUTE ?? "0", 59)
&& (rule.FREQ === "WEEKLY" ? days.includes(rule.BYDAY) : !rule.BYDAY)
&& (!rule.INTERVAL || rule.INTERVAL === "1"));
const labels = zh ? ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
function change(patch: Record<string, string>) {
const next = { FREQ: "WEEKLY", BYDAY: "MO", BYHOUR: "9", BYMINUTE: "0", ...rule, ...patch };
onChange?.({
schema_version: "periodic_report_schedule_v0",
schedule_id: schedule?.schedule_id ?? "report-schedule",
timezone,
rrule: [`FREQ=${next.FREQ}`, ...(next.FREQ === "WEEKLY" ? [`BYDAY=${next.BYDAY}`] : []),
`BYHOUR=${next.BYHOUR}`, `BYMINUTE=${next.BYMINUTE}`].join(";"),
});
}
return <div className="personal-report-schedule">
<label className="is-boolean" htmlFor={id}>
<span>{zh ? "按日历汇报" : "Calendar reports"}</span>
<input id={id} type="checkbox" role="switch" checked={Boolean(schedule)} disabled={!onChange}
onChange={(event) => event.target.checked ? change({}) : onChange?.(null)} />
</label>
{!schedule ? <p>{zh ? "未设置日历计划;保持阶段结束时汇报。" : "No calendar schedule; report at validated stage boundaries."}</p> : <>
{!supported ? <p role="alert">{zh ? "此计划需在 JSON 模式中编辑;当前内容已保留。" : "Edit this schedule in JSON mode; its current value is preserved."}</p> : <>
<label htmlFor={`${id}-frequency`}><span>{zh ? "频率" : "Frequency"}</span>
<select id={`${id}-frequency`} value={rule.FREQ} disabled={!onChange} onChange={(event) => change({ FREQ: event.target.value })}>
<option value="DAILY">{zh ? "每天" : "Daily"}</option><option value="WEEKLY">{zh ? "每周" : "Weekly"}</option>
</select>
</label>
{rule.FREQ === "WEEKLY" && <label htmlFor={`${id}-day`}><span>{zh ? "星期" : "Weekday"}</span>
<select id={`${id}-day`} value={rule.BYDAY} disabled={!onChange} onChange={(event) => change({ BYDAY: event.target.value })}>
{days.map((day, index) => <option key={day} value={day}>{labels[index]}</option>)}
</select>
</label>}
<label htmlFor={`${id}-time`}><span>{zh ? "当地时间" : "Local time"} ({timezone})</span>
<input id={`${id}-time`} type="time" required disabled={!onChange}
value={`${(rule.BYHOUR ?? "9").padStart(2, "0")}:${(rule.BYMINUTE ?? "0").padStart(2, "0")}`}
onChange={(event) => {
if (!/^\d{2}:\d{2}$/.test(event.target.value)) return;
const [hour, minute] = event.target.value.split(":");
change({ BYHOUR: hour, BYMINUTE: minute });
}} />
</label>
</>}
<p>{zh ? "由现有唤醒检查到期计划;实际送达以回执为准。" : "Existing wakes check the schedule; delivery is confirmed by its receipt."}</p>
</>}
</div>;
}

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

Loading