@agent-native/core 0.160.1 → 0.161.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/actions/bigquery.ts +57 -0
- package/corpus/templates/analytics/actions/restore-analysis-revision.ts +5 -2
- package/corpus/templates/analytics/actions/restore-dashboard-revision.ts +5 -2
- package/corpus/templates/analytics/app/components/analysis/AnalysisHistoryPanel.tsx +14 -3
- package/corpus/templates/analytics/app/components/dashboard/DashboardHistoryPanel.tsx +14 -3
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +3 -0
- package/corpus/templates/analytics/app/hooks/use-analysis-revisions.ts +1 -0
- package/corpus/templates/analytics/app/hooks/use-dashboard-revisions.ts +16 -1
- package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +19 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +0 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-sync.ts +14 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +19 -0
- package/corpus/templates/analytics/changelog/2026-08-17-fixed-dashboard-editing-focus-history-recovery-panel-setup-f.md +6 -0
- package/corpus/templates/calendar/actions/create-event.ts +7 -3
- package/corpus/templates/calendar/actions/manage-event-draft.ts +8 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +64 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +5 -1
- package/corpus/templates/calendar/app/hooks/use-events.ts +1 -0
- package/corpus/templates/calendar/changelog/2026-08-17-booking-links-now-respect-each-required-host-s-saved-availab.md +6 -0
- package/corpus/templates/calendar/server/lib/booking-host-availability.ts +76 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +2 -0
- package/corpus/templates/calendar/shared/api.ts +1 -1
- package/corpus/templates/clips/app/components/player/signed-out-share-actions.tsx +47 -0
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +3 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +27 -19
- package/corpus/templates/clips/changelog/2026-08-17-shared-clips-sign-in-comment-no-audio.md +6 -0
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +6 -5
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +13 -0
- package/corpus/templates/content/AGENTS.md +2 -0
- package/corpus/templates/content/actions/_blocks-field-identity.ts +17 -0
- package/corpus/templates/content/actions/_database-block-actions.ts +958 -0
- package/corpus/templates/content/actions/_database-row-mutation.ts +7 -7
- package/corpus/templates/content/actions/list-content-database-blocks.ts +31 -0
- package/corpus/templates/content/actions/mutate-content-database-block.ts +49 -0
- package/corpus/templates/content/changelog/2026-08-10-agents-can-safely-insert-update-upsert-delete-and-reorder-on.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.block.md +6 -2
- package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +2 -1
- package/corpus/templates/content/parity/matrix.md +30 -30
- package/corpus/templates/content/parity/matrix.ts +4 -1
- package/corpus/templates/content/shared/blocks-field-identity.ts +114 -37
- package/corpus/templates/content/shared/database-block-actions.ts +82 -0
- package/corpus/templates/content/shared/database-block-mutations.ts +366 -0
- package/corpus/templates/content/shared/nfm.ts +3 -2
- package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +24 -0
- package/corpus/templates/mail/actions/get-mail-settings.ts +1 -1
- package/corpus/templates/mail/actions/update-mail-settings.ts +7 -3
- package/corpus/templates/mail/app/i18n/en-US.ts +3 -1
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +63 -0
- package/corpus/templates/mail/changelog/2026-08-17-gmail-signatures-now-preserve-images-and-support-paste-or-up.md +6 -0
- package/corpus/templates/mail/changelog/2026-08-17-mail-now-updates-durable-drafting-preferences-without-openin.md +6 -0
- package/corpus/templates/mail/server/plugins/agent-chat.ts +19 -0
- package/corpus/templates/mail/shared/gmail-signature.ts +14 -2
- package/corpus/templates/mail/shared/signature.ts +1 -10
- package/corpus/templates/slides/actions/generate-slides-ai.ts +2 -0
- package/corpus/templates/slides/actions/patch-deck.ts +1 -1
- package/corpus/templates/slides/app/components/ThemeToggle.tsx +1 -0
- package/corpus/templates/slides/app/components/editor/SlideThumbnailContextMenu.tsx +59 -0
- package/corpus/templates/slides/app/global.css +1 -0
- package/corpus/templates/slides/app/lib/normalize-slide-padding.ts +40 -11
- package/corpus/templates/slides/changelog/2026-08-17-stable-layout-and-slide-editing.md +6 -0
- package/corpus/templates/slides/shared/deck-title.ts +12 -4
- package/dist/agent/engine/builder-engine.js +6 -1
- package/dist/agent/engine/error-detail.d.ts +17 -0
- package/dist/agent/engine/error-detail.js +27 -0
- package/dist/agent/production-agent.js +23 -5
- package/dist/agent/thread-data-builder.js +5 -0
- package/dist/agent/tool-approval-migrations.d.ts +1 -0
- package/dist/agent/tool-approval-migrations.js +10 -0
- package/dist/agent/tool-approval-store.d.ts +12 -0
- package/dist/agent/tool-approval-store.js +46 -1
- package/dist/agent/types.d.ts +3 -1
- package/dist/client/AssistantChat.js +3 -1
- package/dist/client/error-format.js +15 -0
- package/dist/client/sse-event-processor.js +4 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/localization/core-messages/ar-SA.js +2 -0
- package/dist/localization/core-messages/de-DE.js +2 -0
- package/dist/localization/core-messages/en-US.d.ts +2 -0
- package/dist/localization/core-messages/en-US.js +2 -0
- package/dist/localization/core-messages/es-ES.js +2 -0
- package/dist/localization/core-messages/fr-FR.js +2 -0
- package/dist/localization/core-messages/hi-IN.js +2 -0
- package/dist/localization/core-messages/ja-JP.js +2 -0
- package/dist/localization/core-messages/ko-KR.js +2 -0
- package/dist/localization/core-messages/pt-BR.js +2 -0
- package/dist/localization/core-messages/zh-CN.js +2 -0
- package/dist/localization/core-messages/zh-TW.js +2 -0
- package/dist/localization/core-messages.d.ts +2 -0
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/resources/read.js +4 -4
- package/dist/scripts/resources/save-memory.js +15 -2
- package/dist/scripts/resources/write.js +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/script-entries.d.ts +1 -0
- package/dist/server/agent-chat/script-entries.js +8 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/onboarding.mdx +13 -2
- package/package.json +3 -3
package/corpus/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AgentActionStopError, defineAction } from "@agent-native/core";
|
|
2
2
|
import type { ActionRunContext } from "@agent-native/core/action";
|
|
3
|
+
import { getRequestRunContext } from "@agent-native/core/server";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
|
|
5
6
|
import { runQuery } from "../server/lib/bigquery";
|
|
@@ -67,6 +68,59 @@ function stopForBigQueryCancellation(): never {
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function normalizeSqlForRepeat(sql: string): string {
|
|
72
|
+
return sql.trim().replace(/\s+/g, " ");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hasPriorFailedBigQueryCall(sql: string): boolean {
|
|
76
|
+
const runContext = getRequestRunContext();
|
|
77
|
+
const priorCalls = runContext?.toolCalls ?? [];
|
|
78
|
+
const priorResults = runContext?.toolResults ?? [];
|
|
79
|
+
const normalizedSql = normalizeSqlForRepeat(sql);
|
|
80
|
+
|
|
81
|
+
return priorCalls.some((call, index) => {
|
|
82
|
+
if (call.name !== "bigquery") return false;
|
|
83
|
+
const input = call.input;
|
|
84
|
+
if (
|
|
85
|
+
!input ||
|
|
86
|
+
typeof input !== "object" ||
|
|
87
|
+
normalizeSqlForRepeat(String((input as { sql?: unknown }).sql ?? "")) !==
|
|
88
|
+
normalizedSql
|
|
89
|
+
) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const result = priorResults[index];
|
|
93
|
+
if (!result || result.name !== "bigquery") return false;
|
|
94
|
+
try {
|
|
95
|
+
const content = JSON.parse(result.content) as { error?: unknown };
|
|
96
|
+
return (
|
|
97
|
+
content.error === "bigquery_query_failed" ||
|
|
98
|
+
content.error === "bigquery_query_timeout"
|
|
99
|
+
);
|
|
100
|
+
} catch {
|
|
101
|
+
// coercion-ok: malformed prior tool output is not evidence of a failed query.
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stopForRepeatedBigQueryQuery(): never {
|
|
108
|
+
const message =
|
|
109
|
+
"I stopped the repeated BigQuery request because the same SQL already failed in this turn. Change the SQL using the provider error or schema result before trying again.";
|
|
110
|
+
throw new AgentActionStopError(message, {
|
|
111
|
+
errorCode: "bigquery_repeated_query",
|
|
112
|
+
toolResult: JSON.stringify(
|
|
113
|
+
{
|
|
114
|
+
error: "bigquery_repeated_query",
|
|
115
|
+
message,
|
|
116
|
+
recoverable: false,
|
|
117
|
+
},
|
|
118
|
+
null,
|
|
119
|
+
2,
|
|
120
|
+
),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
70
124
|
export default defineAction({
|
|
71
125
|
description:
|
|
72
126
|
"Query the user-configured BigQuery data warehouse. Use this when the user asks for warehouse SQL, BigQuery, or a data-dictionary metric/table that lives in BigQuery. If the user names a provider action such as Jira or Pylon, use that provider action first and do not use BigQuery unless the user explicitly asks for a warehouse copy. For a named customer or organization ID, resolve the canonical CRM/contract identity first and verify the returned rows carry the same customer and org/root-org identifiers. For account health, distinguish completed-month usage from current partial snapshots, contract metrics from similarly named platform metrics, total distinct contracted users from DAU/WAU, and actual usage from contracted capacity. Pass standard SQL via the `sql` arg. Do NOT use `db-query` for warehouse data (it only reaches the app's own SQL database). If a query fails with a schema or SQL error (unknown dataset/table/column, syntax), treat it as a normal debugging signal: inspect the real schema with `search-bigquery-schema` (or query INFORMATION_SCHEMA), correct the query based on the error, and run it again — a few corrective attempts are expected. Surface the error to the user only if it still fails after a few attempts or is non-recoverable (missing credentials, permission, quota). Never rerun identical failing SQL, and never substitute made-up numbers for data you could not query.",
|
|
@@ -77,6 +131,9 @@ export default defineAction({
|
|
|
77
131
|
toolCallable: true,
|
|
78
132
|
grounding: true,
|
|
79
133
|
run: async (args, context?: ActionRunContext) => {
|
|
134
|
+
if (hasPriorFailedBigQueryCall(args.sql)) {
|
|
135
|
+
stopForRepeatedBigQueryQuery();
|
|
136
|
+
}
|
|
80
137
|
try {
|
|
81
138
|
return await runQuery(args.sql, { signal: context?.signal });
|
|
82
139
|
} catch (err) {
|
|
@@ -29,8 +29,11 @@ export default defineAction({
|
|
|
29
29
|
resolveScope(),
|
|
30
30
|
);
|
|
31
31
|
if (!analysis) {
|
|
32
|
-
throw
|
|
33
|
-
|
|
32
|
+
throw Object.assign(
|
|
33
|
+
new Error(
|
|
34
|
+
`Analysis revision "${args.revisionId}" was not found for analysis "${args.analysisId}".`,
|
|
35
|
+
),
|
|
36
|
+
{ statusCode: 404 },
|
|
34
37
|
);
|
|
35
38
|
}
|
|
36
39
|
return {
|
|
@@ -35,8 +35,11 @@ export default defineAction({
|
|
|
35
35
|
args.expectedUpdatedAt,
|
|
36
36
|
);
|
|
37
37
|
if (!restored) {
|
|
38
|
-
throw
|
|
39
|
-
|
|
38
|
+
throw Object.assign(
|
|
39
|
+
new Error(
|
|
40
|
+
`Dashboard revision "${args.revisionId}" was not found for dashboard "${args.dashboardId}".`,
|
|
41
|
+
),
|
|
42
|
+
{ statusCode: 404 },
|
|
40
43
|
);
|
|
41
44
|
}
|
|
42
45
|
const { dashboard, snapshotRevisionId } = restored;
|
|
@@ -3,6 +3,7 @@ import { IconHistory, IconLoader2, IconRotate } from "@tabler/icons-react";
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { toast } from "sonner";
|
|
5
5
|
|
|
6
|
+
import { ResourceLoadError } from "@/components/ResourceLoadError";
|
|
6
7
|
import {
|
|
7
8
|
AlertDialog,
|
|
8
9
|
AlertDialogAction,
|
|
@@ -54,9 +55,12 @@ export function AnalysisHistoryPanel({
|
|
|
54
55
|
canRestore?: boolean;
|
|
55
56
|
}) {
|
|
56
57
|
const t = useT();
|
|
57
|
-
const {
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
const {
|
|
59
|
+
data: revisions,
|
|
60
|
+
isLoading,
|
|
61
|
+
isError,
|
|
62
|
+
refetch,
|
|
63
|
+
} = useAnalysisRevisions(open ? analysisId : null);
|
|
60
64
|
const restoreRevision = useRestoreAnalysisRevision(analysisId);
|
|
61
65
|
const [pendingRestore, setPendingRestore] = useState<AnalysisRevision | null>(
|
|
62
66
|
null,
|
|
@@ -98,6 +102,13 @@ export function AnalysisHistoryPanel({
|
|
|
98
102
|
className="animate-spin text-muted-foreground"
|
|
99
103
|
/>
|
|
100
104
|
</div>
|
|
105
|
+
) : isError ? (
|
|
106
|
+
<ResourceLoadError
|
|
107
|
+
inline
|
|
108
|
+
message={t("sidebar.analysesLoadFailed")}
|
|
109
|
+
retryLabel={t("sidebar.retry")}
|
|
110
|
+
onRetry={() => void refetch()}
|
|
111
|
+
/>
|
|
101
112
|
) : !revisions?.length ? (
|
|
102
113
|
<div className="px-6 py-12 text-center text-xs text-muted-foreground">
|
|
103
114
|
{t("analyses.historyEmpty")}
|
|
@@ -3,6 +3,7 @@ import { IconHistory, IconLoader2, IconRotate } from "@tabler/icons-react";
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { toast } from "sonner";
|
|
5
5
|
|
|
6
|
+
import { ResourceLoadError } from "@/components/ResourceLoadError";
|
|
6
7
|
import {
|
|
7
8
|
AlertDialog,
|
|
8
9
|
AlertDialogAction,
|
|
@@ -58,9 +59,12 @@ export function DashboardHistoryPanel({
|
|
|
58
59
|
onRestored,
|
|
59
60
|
}: DashboardHistoryPanelProps) {
|
|
60
61
|
const t = useT();
|
|
61
|
-
const {
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
const {
|
|
63
|
+
data: revisions,
|
|
64
|
+
isLoading,
|
|
65
|
+
isError,
|
|
66
|
+
refetch,
|
|
67
|
+
} = useDashboardRevisions(open ? dashboardId : null);
|
|
64
68
|
const restoreRevision = useRestoreDashboardRevision(dashboardId);
|
|
65
69
|
const [pendingRestore, setPendingRestore] =
|
|
66
70
|
useState<DashboardRevision | null>(null);
|
|
@@ -104,6 +108,13 @@ export function DashboardHistoryPanel({
|
|
|
104
108
|
className="animate-spin text-muted-foreground"
|
|
105
109
|
/>
|
|
106
110
|
</div>
|
|
111
|
+
) : isError ? (
|
|
112
|
+
<ResourceLoadError
|
|
113
|
+
inline
|
|
114
|
+
message={t("sidebar.dashboardsLoadFailed")}
|
|
115
|
+
retryLabel={t("sidebar.retry")}
|
|
116
|
+
onRetry={() => void refetch()}
|
|
117
|
+
/>
|
|
107
118
|
) : !revisions?.length ? (
|
|
108
119
|
<div className="px-6 py-12 text-center text-xs text-muted-foreground">
|
|
109
120
|
{t("dashboard.historyEmpty")}
|
|
@@ -150,6 +150,7 @@ import {
|
|
|
150
150
|
type PrefetchSnapshot,
|
|
151
151
|
} from "@/lib/prefetch-keys";
|
|
152
152
|
import type { ResourceAccess } from "@/lib/resource-access";
|
|
153
|
+
import { useAutoFocusSelect } from "@/lib/use-auto-focus-select";
|
|
153
154
|
|
|
154
155
|
import { resolveAskNavigationAction } from "./layout-route-policy";
|
|
155
156
|
import { NewDashboardDialog } from "./NewDashboardDialog";
|
|
@@ -492,6 +493,7 @@ function SortableRow({
|
|
|
492
493
|
useState(false);
|
|
493
494
|
const [isRenaming, setIsRenaming] = useState(false);
|
|
494
495
|
const [renameValue, setRenameValue] = useState(name);
|
|
496
|
+
const renameInputRef = useAutoFocusSelect<HTMLInputElement>(isRenaming);
|
|
495
497
|
|
|
496
498
|
useEffect(() => {
|
|
497
499
|
if (!isRenaming) setRenameValue(name);
|
|
@@ -650,6 +652,7 @@ function SortableRow({
|
|
|
650
652
|
>
|
|
651
653
|
{isRenaming ? (
|
|
652
654
|
<input
|
|
655
|
+
ref={renameInputRef}
|
|
653
656
|
autoFocus
|
|
654
657
|
value={renameValue}
|
|
655
658
|
onChange={(e) => setRenameValue(e.target.value)}
|
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
} from "@agent-native/core/client/hooks";
|
|
6
6
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
dashboardCacheScope,
|
|
10
|
+
sqlDashboardPrefetchKey,
|
|
11
|
+
} from "@/lib/prefetch-keys";
|
|
9
12
|
|
|
10
13
|
export interface DashboardRevision {
|
|
11
14
|
id: string;
|
|
@@ -39,10 +42,13 @@ export function useDashboardRevisions(dashboardId: string | null) {
|
|
|
39
42
|
const rows = revisions ?? data;
|
|
40
43
|
return (Array.isArray(rows) ? rows : []) as DashboardRevision[];
|
|
41
44
|
},
|
|
45
|
+
retry: false,
|
|
42
46
|
});
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
export function useRestoreDashboardRevision(dashboardId: string) {
|
|
50
|
+
const { session } = useSession();
|
|
51
|
+
const scope = dashboardCacheScope(session);
|
|
46
52
|
const queryClient = useQueryClient();
|
|
47
53
|
return useActionMutation<
|
|
48
54
|
{
|
|
@@ -67,6 +73,15 @@ export function useRestoreDashboardRevision(dashboardId: string) {
|
|
|
67
73
|
queryClient.invalidateQueries({
|
|
68
74
|
queryKey: ["data", "sql-dashboard", dashboardId],
|
|
69
75
|
});
|
|
76
|
+
queryClient.removeQueries({
|
|
77
|
+
queryKey: sqlDashboardPrefetchKey(dashboardId, scope),
|
|
78
|
+
});
|
|
79
|
+
queryClient.invalidateQueries({
|
|
80
|
+
queryKey: ["sql-dashboards-sidebar", scope],
|
|
81
|
+
});
|
|
82
|
+
queryClient.invalidateQueries({
|
|
83
|
+
queryKey: ["sql-dashboards-palette", scope],
|
|
84
|
+
});
|
|
70
85
|
},
|
|
71
86
|
});
|
|
72
87
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useEffect, useRef, type RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
/** Focus an input and select its existing value after the next paint. */
|
|
4
|
+
export function useAutoFocusSelect<T extends HTMLInputElement>(
|
|
5
|
+
enabled: boolean,
|
|
6
|
+
): RefObject<T | null> {
|
|
7
|
+
const inputRef = useRef<T | null>(null);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!enabled) return;
|
|
11
|
+
const frame = window.requestAnimationFrame(() => {
|
|
12
|
+
inputRef.current?.focus();
|
|
13
|
+
inputRef.current?.select();
|
|
14
|
+
});
|
|
15
|
+
return () => window.cancelAnimationFrame(frame);
|
|
16
|
+
}, [enabled]);
|
|
17
|
+
|
|
18
|
+
return inputRef;
|
|
19
|
+
}
|
|
@@ -756,7 +756,6 @@ function PanelEditorContent({
|
|
|
756
756
|
<div className="grid gap-3">
|
|
757
757
|
<Label>{t("panelEditor.whatToChart")}</Label>
|
|
758
758
|
<PromptComposer
|
|
759
|
-
autoFocus
|
|
760
759
|
disabled={isGenerating}
|
|
761
760
|
placeholder={t("panelEditor.promptPlaceholder")}
|
|
762
761
|
draftScope="analytics:add-panel"
|
|
@@ -23,6 +23,20 @@ export function dashboardUpdatedAtMs(value: string | null | undefined) {
|
|
|
23
23
|
return Number.isFinite(ms) ? ms : null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function shouldShowDashboardLoadError({
|
|
27
|
+
dashboardId,
|
|
28
|
+
isError,
|
|
29
|
+
loaded,
|
|
30
|
+
hasDashboard,
|
|
31
|
+
}: {
|
|
32
|
+
dashboardId: string | null;
|
|
33
|
+
isError: boolean;
|
|
34
|
+
loaded: boolean;
|
|
35
|
+
hasDashboard: boolean;
|
|
36
|
+
}): boolean {
|
|
37
|
+
return Boolean(dashboardId && isError && (!loaded || !hasDashboard));
|
|
38
|
+
}
|
|
39
|
+
|
|
26
40
|
export function createDashboardAdoptionHold({
|
|
27
41
|
dashboardId,
|
|
28
42
|
currentUpdatedAt,
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
useSetPageTitle,
|
|
72
72
|
useSetHeaderActions,
|
|
73
73
|
} from "@/components/layout/HeaderActions";
|
|
74
|
+
import { ResourceLoadError } from "@/components/ResourceLoadError";
|
|
74
75
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
75
76
|
import {
|
|
76
77
|
AlertDialog,
|
|
@@ -148,6 +149,7 @@ import { createDashboardSaveQueue } from "./dashboard-save-queue";
|
|
|
148
149
|
import {
|
|
149
150
|
createDashboardAdoptionHold,
|
|
150
151
|
dashboardPrefetchInitialData,
|
|
152
|
+
shouldShowDashboardLoadError,
|
|
151
153
|
shouldAdoptDashboardQueryResult,
|
|
152
154
|
type DashboardAdoptionHold,
|
|
153
155
|
} from "./dashboard-sync";
|
|
@@ -2070,6 +2072,23 @@ function SqlDashboardPageContent({
|
|
|
2070
2072
|
);
|
|
2071
2073
|
}
|
|
2072
2074
|
|
|
2075
|
+
if (
|
|
2076
|
+
shouldShowDashboardLoadError({
|
|
2077
|
+
dashboardId,
|
|
2078
|
+
isError: dashboardQuery.isError,
|
|
2079
|
+
loaded,
|
|
2080
|
+
hasDashboard: !!dashboard,
|
|
2081
|
+
})
|
|
2082
|
+
) {
|
|
2083
|
+
return (
|
|
2084
|
+
<ResourceLoadError
|
|
2085
|
+
message={t("sidebar.dashboardsLoadFailed")}
|
|
2086
|
+
retryLabel={t("sidebar.retry")}
|
|
2087
|
+
onRetry={() => void dashboardQuery.refetch()}
|
|
2088
|
+
/>
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2073
2092
|
if (!loaded) {
|
|
2074
2093
|
if (reportScreenshot) {
|
|
2075
2094
|
return (
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
ensureOrganizerInAttendees,
|
|
23
23
|
normalizeAttendees,
|
|
24
24
|
normalizeCreateEventInput,
|
|
25
|
+
normalizeRecurrence,
|
|
25
26
|
resolveOwnedAccountEmail,
|
|
26
27
|
reminderMethodInput,
|
|
27
28
|
reminderMinutesInput,
|
|
@@ -99,9 +100,11 @@ export default defineAction({
|
|
|
99
100
|
"Google Calendar event color id, 1 through 11.",
|
|
100
101
|
),
|
|
101
102
|
recurrence: z
|
|
102
|
-
.array(z.string())
|
|
103
|
+
.union([z.string(), z.array(z.string())])
|
|
103
104
|
.optional()
|
|
104
|
-
.describe(
|
|
105
|
+
.describe(
|
|
106
|
+
"Google recurrence rules, such as RRULE:FREQ=DAILY. Pass an empty string or [] for a non-recurring event.",
|
|
107
|
+
),
|
|
105
108
|
reminderMinutes: reminderMinutesInput.describe(
|
|
106
109
|
"Convenience field for a single reminder in minutes before the event.",
|
|
107
110
|
),
|
|
@@ -176,6 +179,7 @@ export default defineAction({
|
|
|
176
179
|
reminderMethod: args.reminderMethod,
|
|
177
180
|
useDefaultReminders: args.remindersUseDefault,
|
|
178
181
|
});
|
|
182
|
+
const recurrence = normalizeRecurrence(args.recurrence);
|
|
179
183
|
const statusEventFields = buildStatusEventFields({
|
|
180
184
|
eventType: args.eventType,
|
|
181
185
|
title: normalized.title,
|
|
@@ -204,7 +208,7 @@ export default defineAction({
|
|
|
204
208
|
attendees,
|
|
205
209
|
attachments: args.attachments,
|
|
206
210
|
colorId: args.colorId,
|
|
207
|
-
recurrence
|
|
211
|
+
...(recurrence && recurrence.length > 0 ? { recurrence } : {}),
|
|
208
212
|
...reminderFields,
|
|
209
213
|
...statusEventFields,
|
|
210
214
|
createdAt: new Date().toISOString(),
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
googleColorIdInput,
|
|
21
21
|
normalizeAttendees,
|
|
22
22
|
normalizeCreateEventInput,
|
|
23
|
+
normalizeRecurrence,
|
|
23
24
|
reminderMethodInput,
|
|
24
25
|
reminderMinutesInput,
|
|
25
26
|
remindersInput,
|
|
@@ -136,6 +137,12 @@ export default defineAction({
|
|
|
136
137
|
reminders: remindersInput.describe(
|
|
137
138
|
"Custom reminder overrides, max 5, such as [{method:'popup', minutes:10}].",
|
|
138
139
|
),
|
|
140
|
+
recurrence: z
|
|
141
|
+
.union([z.string(), z.array(z.string())])
|
|
142
|
+
.optional()
|
|
143
|
+
.describe(
|
|
144
|
+
"Google recurrence rules, such as RRULE:FREQ=DAILY. Pass an empty string or [] to clear recurrence.",
|
|
145
|
+
),
|
|
139
146
|
reminderMinutes: reminderMinutesInput.describe(
|
|
140
147
|
"Convenience field for a single reminder in minutes before the event.",
|
|
141
148
|
),
|
|
@@ -239,6 +246,7 @@ export default defineAction({
|
|
|
239
246
|
setIfPresent(draft, "transparency", args.transparency);
|
|
240
247
|
setIfPresent(draft, "visibility", args.visibility);
|
|
241
248
|
setIfPresent(draft, "colorId", args.colorId);
|
|
249
|
+
setIfPresent(draft, "recurrence", normalizeRecurrence(args.recurrence));
|
|
242
250
|
setIfPresent(draft, "attachments", args.attachments);
|
|
243
251
|
setIfPresent(draft, "workingLocationType", args.workingLocationType);
|
|
244
252
|
setIfPresent(draft, "workingLocationLabel", args.workingLocationLabel);
|
|
@@ -71,14 +71,17 @@ import { getGoogleEventColorHex } from "@/lib/event-colors";
|
|
|
71
71
|
import { buildEventFormInitializationKey } from "@/lib/event-form-initialization";
|
|
72
72
|
import {
|
|
73
73
|
attachmentsToDrafts,
|
|
74
|
+
buildRecurrenceRules,
|
|
74
75
|
buildReminderPayload,
|
|
75
76
|
createAttachmentDraft,
|
|
76
77
|
createReminderDraft,
|
|
77
78
|
dateTimeInTimezoneToIso,
|
|
78
79
|
getEventEndValidationMessage,
|
|
80
|
+
getRecurrencePreset,
|
|
79
81
|
remindersToDraftState,
|
|
80
82
|
resolveEventTimezone,
|
|
81
83
|
type AttachmentDraft,
|
|
84
|
+
type RecurrencePreset,
|
|
82
85
|
type ReminderDraft,
|
|
83
86
|
type ReminderMode,
|
|
84
87
|
validateAttachmentDrafts,
|
|
@@ -265,6 +268,8 @@ export function CreateEventPopover({
|
|
|
265
268
|
);
|
|
266
269
|
const [availability, setAvailability] = useState<Availability>("opaque");
|
|
267
270
|
const [visibility, setVisibility] = useState<Visibility>("default");
|
|
271
|
+
const [recurrencePreset, setRecurrencePreset] =
|
|
272
|
+
useState<RecurrencePreset>("none");
|
|
268
273
|
const [timezone, setTimezone] = useState(defaultTimezone);
|
|
269
274
|
const [colorId, setColorId] = useState<string | undefined>();
|
|
270
275
|
const [reminderMode, setReminderMode] = useState<ReminderMode>("default");
|
|
@@ -371,6 +376,7 @@ export function CreateEventPopover({
|
|
|
371
376
|
);
|
|
372
377
|
setAvailability(draft.transparency ?? "opaque");
|
|
373
378
|
setVisibility(draft.visibility ?? "default");
|
|
379
|
+
setRecurrencePreset(getRecurrencePreset(draft.recurrence));
|
|
374
380
|
setTimezone(draftTimezone);
|
|
375
381
|
setColorId(draft.colorId);
|
|
376
382
|
setReminderMode(reminderState.mode);
|
|
@@ -409,6 +415,7 @@ export function CreateEventPopover({
|
|
|
409
415
|
setDeclineMessage("Declined because I am out of office");
|
|
410
416
|
setAvailability("opaque");
|
|
411
417
|
setVisibility("default");
|
|
418
|
+
setRecurrencePreset("none");
|
|
412
419
|
setTimezone(defaultTimezone);
|
|
413
420
|
setColorId(undefined);
|
|
414
421
|
setReminderMode("default");
|
|
@@ -471,6 +478,11 @@ export function CreateEventPopover({
|
|
|
471
478
|
: dateTimeInTimezoneToIso(endDate, endTime, eventTimezone);
|
|
472
479
|
const attachmentResult = validateAttachmentDrafts(attachments);
|
|
473
480
|
const reminderPatch = buildReminderPayload(reminderMode, reminders);
|
|
481
|
+
const recurrence = buildRecurrenceRules(
|
|
482
|
+
recurrencePreset,
|
|
483
|
+
effectiveAllDay ? date : startValue,
|
|
484
|
+
eventTimezone,
|
|
485
|
+
);
|
|
474
486
|
const nextDraft: CalendarEventDraft = {
|
|
475
487
|
id: draftId,
|
|
476
488
|
createdAt: draft?.createdAt,
|
|
@@ -499,6 +511,7 @@ export function CreateEventPopover({
|
|
|
499
511
|
: "opaque",
|
|
500
512
|
visibility: eventType === "workingLocation" ? "public" : visibility,
|
|
501
513
|
...reminderPatch,
|
|
514
|
+
recurrence: recurrence ?? undefined,
|
|
502
515
|
colorId,
|
|
503
516
|
attachments:
|
|
504
517
|
attachmentResult.error ||
|
|
@@ -555,6 +568,7 @@ export function CreateEventPopover({
|
|
|
555
568
|
declineMessage,
|
|
556
569
|
availability,
|
|
557
570
|
visibility,
|
|
571
|
+
recurrencePreset,
|
|
558
572
|
eventTimezone,
|
|
559
573
|
colorId,
|
|
560
574
|
reminderMode,
|
|
@@ -753,6 +767,11 @@ export function CreateEventPopover({
|
|
|
753
767
|
...trailingAttendees,
|
|
754
768
|
]);
|
|
755
769
|
const reminderPatch = buildReminderPayload(reminderMode, reminders);
|
|
770
|
+
const recurrence = buildRecurrenceRules(
|
|
771
|
+
recurrencePreset,
|
|
772
|
+
effectiveAllDay ? date : startValue,
|
|
773
|
+
eventTimezone,
|
|
774
|
+
);
|
|
756
775
|
const statusPatch =
|
|
757
776
|
eventType === "default"
|
|
758
777
|
? {}
|
|
@@ -788,6 +807,7 @@ export function CreateEventPopover({
|
|
|
788
807
|
: "opaque",
|
|
789
808
|
visibility: eventType === "workingLocation" ? "public" : visibility,
|
|
790
809
|
...reminderPatch,
|
|
810
|
+
recurrence: recurrence ?? undefined,
|
|
791
811
|
...statusPatch,
|
|
792
812
|
color: colorId ? getGoogleEventColorHex(colorId) : undefined,
|
|
793
813
|
colorId,
|
|
@@ -1411,6 +1431,50 @@ export function CreateEventPopover({
|
|
|
1411
1431
|
</div>
|
|
1412
1432
|
)}
|
|
1413
1433
|
|
|
1434
|
+
<div className="space-y-1.5">
|
|
1435
|
+
<Label htmlFor="event-recurrence" className="text-xs">
|
|
1436
|
+
{t("eventForm.repeats")}
|
|
1437
|
+
</Label>
|
|
1438
|
+
<Select
|
|
1439
|
+
value={recurrencePreset}
|
|
1440
|
+
onValueChange={(value) =>
|
|
1441
|
+
setRecurrencePreset(value as RecurrencePreset)
|
|
1442
|
+
}
|
|
1443
|
+
>
|
|
1444
|
+
<SelectTrigger
|
|
1445
|
+
id="event-recurrence"
|
|
1446
|
+
className="h-8 text-sm"
|
|
1447
|
+
>
|
|
1448
|
+
<SelectValue />
|
|
1449
|
+
</SelectTrigger>
|
|
1450
|
+
<SelectContent>
|
|
1451
|
+
<SelectItem value="none">
|
|
1452
|
+
{t("eventForm.doesNotRepeat")}
|
|
1453
|
+
</SelectItem>
|
|
1454
|
+
<SelectItem value="daily">
|
|
1455
|
+
{t("eventForm.daily")}
|
|
1456
|
+
</SelectItem>
|
|
1457
|
+
<SelectItem value="weekdays">
|
|
1458
|
+
{t("eventForm.everyWeekday")}
|
|
1459
|
+
</SelectItem>
|
|
1460
|
+
<SelectItem value="weekly">
|
|
1461
|
+
{t("eventForm.weekly")}
|
|
1462
|
+
</SelectItem>
|
|
1463
|
+
<SelectItem value="monthly">
|
|
1464
|
+
{t("eventForm.monthly")}
|
|
1465
|
+
</SelectItem>
|
|
1466
|
+
<SelectItem value="yearly">
|
|
1467
|
+
{t("eventForm.yearly")}
|
|
1468
|
+
</SelectItem>
|
|
1469
|
+
{recurrencePreset === "custom" && (
|
|
1470
|
+
<SelectItem value="custom" disabled>
|
|
1471
|
+
{t("eventForm.customSchedule")}
|
|
1472
|
+
</SelectItem>
|
|
1473
|
+
)}
|
|
1474
|
+
</SelectContent>
|
|
1475
|
+
</Select>
|
|
1476
|
+
</div>
|
|
1477
|
+
|
|
1414
1478
|
{(!allDay || isOutOfOffice) && (
|
|
1415
1479
|
<div className="space-y-1.5">
|
|
1416
1480
|
<Label htmlFor="event-timezone" className="text-xs">
|
|
@@ -1290,7 +1290,10 @@ export function EventDetailPopover({
|
|
|
1290
1290
|
toast.error(t("eventForm.customRepeatGoogleCalendar"));
|
|
1291
1291
|
return;
|
|
1292
1292
|
}
|
|
1293
|
-
saveField({
|
|
1293
|
+
saveField({
|
|
1294
|
+
recurrence,
|
|
1295
|
+
scope: isRecurringEvent ? "all" : "single",
|
|
1296
|
+
});
|
|
1294
1297
|
},
|
|
1295
1298
|
[
|
|
1296
1299
|
editTimezone,
|
|
@@ -1298,6 +1301,7 @@ export function EventDetailPopover({
|
|
|
1298
1301
|
event.startTimeZone,
|
|
1299
1302
|
masterEvent.data?.start,
|
|
1300
1303
|
masterEvent.data?.startTimeZone,
|
|
1304
|
+
isRecurringEvent,
|
|
1301
1305
|
saveField,
|
|
1302
1306
|
t,
|
|
1303
1307
|
],
|
|
@@ -165,6 +165,7 @@ function buildOptimisticCalendarEvent(
|
|
|
165
165
|
: undefined,
|
|
166
166
|
reminders: newData.reminders,
|
|
167
167
|
remindersUseDefault: newData.remindersUseDefault,
|
|
168
|
+
recurrence: newData.recurrence,
|
|
168
169
|
attendees: newData.attendees,
|
|
169
170
|
accountEmail: newData.accountEmail,
|
|
170
171
|
source: "local",
|