@agent-native/core 0.0.0-beta-20260819161541 → 0.0.0-beta-20260819172516
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/app/components/agents/FeatureFlagsFleetPanel.tsx +78 -19
- package/corpus/templates/analytics/changelog/2026-08-18-verify-cross-app-feature-flag-changes.md +6 -0
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +196 -48
- package/corpus/templates/analytics/server/plugins/feature-flags.ts +5 -0
- package/corpus/templates/analytics/shared/feature-flags.ts +10 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +36 -47
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +18 -1
- package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +256 -38
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +43 -3
- package/corpus/templates/calendar/app/i18n-data.ts +120 -0
- package/corpus/templates/calendar/app/lib/event-form-utils.ts +113 -0
- package/corpus/templates/calendar/server/handlers/google-auth.ts +119 -3
- package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/calendar/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/calendar/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/calendar/server/routes/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +1 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +3 -0
- package/corpus/templates/clips/changelog/2026-08-19-social-preview-video-frames.md +5 -0
- package/corpus/templates/clips/desktop/src/app.tsx +78 -16
- package/corpus/templates/clips/server/lib/slack-unfurls.ts +7 -13
- package/corpus/templates/clips/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.get.ts +41 -3
- package/corpus/templates/clips/server/routes/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +22 -1
- package/corpus/templates/clips/shared/share-meta.ts +67 -3
- package/corpus/templates/content/actions/list-content-databases.ts +95 -1
- package/corpus/templates/content/changelog/2026-08-11-content-mutations-delegated-from-verified-slack-dms-now-use-.md +6 -0
- package/corpus/templates/content/shared/api.ts +11 -0
- package/corpus/templates/factory/AGENTS.md +1 -1
- package/corpus/templates/factory/actions/list-factory-automations.ts +4 -0
- package/corpus/templates/factory/actions/start-builder-for-item.ts +19 -17
- package/corpus/templates/factory/app/components/factory/FactoryAuditView.tsx +100 -36
- package/corpus/templates/factory/app/i18n/en-US.ts +4 -0
- package/corpus/templates/factory/app/routes/factory.tsx +209 -46
- package/corpus/templates/factory/server/connectors/slack.ts +44 -2
- package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -7
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +13 -0
- package/corpus/templates/factory/server/triage/slack-client.ts +5 -0
- package/corpus/templates/factory/server/triage/slack-poller.ts +72 -4
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +100 -18
- package/corpus/templates/mail/server/handlers/google-auth.ts +105 -2
- package/corpus/templates/mail/server/routes/[base]/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/mail/server/routes/[base]/_agent-native/google/auth-url.post.ts +1 -0
- package/corpus/templates/mail/server/routes/_agent-native/google/add-account/auth-url.post.ts +1 -0
- package/corpus/templates/mail/server/routes/_agent-native/google/auth-url.post.ts +1 -0
- package/dist/a2a/artifact-response.d.ts +50 -0
- package/dist/a2a/artifact-response.js +266 -39
- package/dist/a2a/index.d.ts +2 -0
- package/dist/a2a/index.js +1 -0
- package/dist/cli/code-agent-schedules.d.ts +11 -0
- package/dist/cli/code-agent-schedules.js +44 -5
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/integrations/adapters/slack.js +58 -7
- package/dist/jobs/background-automation-runner.js +111 -9
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/action-filters-a2a.d.ts +2 -1
- package/dist/server/agent-chat/action-filters-a2a.js +10 -2
- package/dist/server/agent-chat-plugin.js +33 -11
- package/dist/server/auth.d.ts +17 -1
- package/dist/server/auth.js +369 -153
- package/dist/server/google-auth-plugin.js +3 -1
- package/dist/server/google-oauth.d.ts +10 -0
- package/dist/server/google-oauth.js +25 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/onboarding-html.js +79 -30
- package/dist/server/realtime-token.d.ts +1 -1
- package/docs/content/drop-in-agent.mdx +4 -4
- package/docs/content/faq.mdx +15 -15
- package/docs/content/getting-started.mdx +173 -41
- package/docs/content/key-concepts.mdx +10 -10
- package/docs/content/using-your-agent.mdx +2 -2
- package/docs/content/what-is-agent-native.mdx +79 -228
- package/package.json +2 -2
package/corpus/README.md
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "@agent-native/core/client/hooks";
|
|
10
10
|
import { useT } from "@agent-native/core/client/i18n";
|
|
11
11
|
import { IconRefresh } from "@tabler/icons-react";
|
|
12
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
12
|
+
import { type QueryClient, useQueryClient } from "@tanstack/react-query";
|
|
13
13
|
|
|
14
14
|
import { Badge } from "@/components/ui/badge";
|
|
15
15
|
import { Button } from "@/components/ui/button";
|
|
@@ -40,6 +40,51 @@ export interface FlagDirectory {
|
|
|
40
40
|
apps?: FlagApp[];
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
interface VerifiedFlagMutation {
|
|
44
|
+
contractVersion: 3;
|
|
45
|
+
status: "verified";
|
|
46
|
+
key: string;
|
|
47
|
+
rules: FeatureFlagMetadata["rules"];
|
|
48
|
+
enabledForCurrentUser: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function reconcileVerifiedFleetMutation(
|
|
52
|
+
value: unknown,
|
|
53
|
+
targetAppId: string,
|
|
54
|
+
verified: VerifiedFlagMutation,
|
|
55
|
+
): FlagDirectory {
|
|
56
|
+
const data = asDirectory(value);
|
|
57
|
+
return {
|
|
58
|
+
...data,
|
|
59
|
+
apps: data.apps?.map((app) =>
|
|
60
|
+
appId(app) !== targetAppId
|
|
61
|
+
? app
|
|
62
|
+
: {
|
|
63
|
+
...app,
|
|
64
|
+
flags: app.flags?.map((flag) =>
|
|
65
|
+
flag.key === verified.key
|
|
66
|
+
? {
|
|
67
|
+
...flag,
|
|
68
|
+
rules: verified.rules,
|
|
69
|
+
enabledForCurrentUser: verified.enabledForCurrentUser,
|
|
70
|
+
}
|
|
71
|
+
: flag,
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function recoverFleetMutationFailure(
|
|
79
|
+
client: QueryClient,
|
|
80
|
+
context?: { previous: unknown; key: readonly unknown[] },
|
|
81
|
+
) {
|
|
82
|
+
if (context) client.setQueryData(context.key, context.previous);
|
|
83
|
+
void client.invalidateQueries({
|
|
84
|
+
queryKey: ["action", "list-workspace-feature-flags"],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
43
88
|
function asDirectory(value: unknown): FlagDirectory {
|
|
44
89
|
return value && typeof value === "object" ? (value as FlagDirectory) : {};
|
|
45
90
|
}
|
|
@@ -76,9 +121,10 @@ export function FeatureFlagsFleetPanel() {
|
|
|
76
121
|
{ retry: false },
|
|
77
122
|
);
|
|
78
123
|
const mutation = useActionMutation<
|
|
79
|
-
unknown,
|
|
124
|
+
VerifiedFlagMutation | unknown,
|
|
80
125
|
{ appId: string } & SetFeatureFlagInput
|
|
81
126
|
>("set-workspace-feature-flag", {
|
|
127
|
+
skipActionQueryInvalidation: true,
|
|
82
128
|
onMutate: async (input) => {
|
|
83
129
|
await client.cancelQueries({
|
|
84
130
|
queryKey: ["action", "list-workspace-feature-flags"],
|
|
@@ -119,13 +165,25 @@ export function FeatureFlagsFleetPanel() {
|
|
|
119
165
|
});
|
|
120
166
|
return { previous, key };
|
|
121
167
|
},
|
|
122
|
-
onError: (_error, _input, context: any) =>
|
|
123
|
-
|
|
168
|
+
onError: (_error, _input, context: any) =>
|
|
169
|
+
recoverFleetMutationFailure(client, context),
|
|
170
|
+
onSuccess: (result, input, context: any) => {
|
|
171
|
+
if (
|
|
172
|
+
!result ||
|
|
173
|
+
typeof result !== "object" ||
|
|
174
|
+
(result as Partial<VerifiedFlagMutation>).contractVersion !== 3 ||
|
|
175
|
+
(result as Partial<VerifiedFlagMutation>).status !== "verified"
|
|
176
|
+
) {
|
|
177
|
+
void client.invalidateQueries({
|
|
178
|
+
queryKey: ["action", "list-workspace-feature-flags"],
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const verified = result as VerifiedFlagMutation;
|
|
183
|
+
client.setQueryData(context.key, (old: unknown) =>
|
|
184
|
+
reconcileVerifiedFleetMutation(old, input.appId, verified),
|
|
185
|
+
);
|
|
124
186
|
},
|
|
125
|
-
onSettled: () =>
|
|
126
|
-
void client.invalidateQueries({
|
|
127
|
-
queryKey: ["action", "list-workspace-feature-flags"],
|
|
128
|
-
}),
|
|
129
187
|
});
|
|
130
188
|
const directory = asDirectory(flags.data);
|
|
131
189
|
const apps = directory.apps ?? [];
|
|
@@ -134,7 +192,11 @@ export function FeatureFlagsFleetPanel() {
|
|
|
134
192
|
return (
|
|
135
193
|
<StatusState
|
|
136
194
|
title={t("agents.flagsUnavailable")}
|
|
137
|
-
detail={
|
|
195
|
+
detail={
|
|
196
|
+
mutation.error?.message ||
|
|
197
|
+
flags.error?.message ||
|
|
198
|
+
t("agents.flagsUnreachable")
|
|
199
|
+
}
|
|
138
200
|
onRetry={() => void flags.refetch()}
|
|
139
201
|
/>
|
|
140
202
|
);
|
|
@@ -179,22 +241,19 @@ export function FeatureFlagsFleetPanel() {
|
|
|
179
241
|
</div>
|
|
180
242
|
{isReady(app) ? (
|
|
181
243
|
<div className="p-4">
|
|
244
|
+
{mutation.variables?.appId === appId(app) && mutation.error ? (
|
|
245
|
+
<p className="mb-3 text-sm text-destructive">
|
|
246
|
+
{mutation.error.message}
|
|
247
|
+
</p>
|
|
248
|
+
) : null}
|
|
182
249
|
<FeatureFlagsEditor
|
|
183
250
|
flags={(app.flags ?? []).filter(
|
|
184
251
|
(flag): flag is FeatureFlagMetadata =>
|
|
185
252
|
!!flag.rules && typeof flag.defaultValue === "boolean",
|
|
186
253
|
)}
|
|
187
254
|
isPending={mutation.isPending}
|
|
188
|
-
error={
|
|
189
|
-
|
|
190
|
-
? mutation.error
|
|
191
|
-
: null
|
|
192
|
-
}
|
|
193
|
-
errorFlagKey={
|
|
194
|
-
mutation.variables?.appId === appId(app)
|
|
195
|
-
? mutation.variables.key
|
|
196
|
-
: null
|
|
197
|
-
}
|
|
255
|
+
error={null}
|
|
256
|
+
errorFlagKey={null}
|
|
198
257
|
showHeader={false}
|
|
199
258
|
onMutate={(input) =>
|
|
200
259
|
mutation.mutate(qualifyFleetMutation(appId(app), input))
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { signA2AToken } from "@agent-native/core/a2a";
|
|
4
|
+
import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
|
|
4
5
|
import { fetchOrgApps, type OrgApp } from "@agent-native/core/mcp";
|
|
5
6
|
import { getOrgDomain } from "@agent-native/core/org";
|
|
6
7
|
|
|
8
|
+
import { VERIFIED_FLEET_FLAG_MUTATIONS } from "../../shared/feature-flags.js";
|
|
7
9
|
import type { AnalyticsAdminContext } from "./db-admin-connections.js";
|
|
8
10
|
|
|
9
11
|
const TARGET_TIMEOUT_MS = 3_000;
|
|
@@ -31,7 +33,16 @@ export interface WorkspaceFeatureFlagsResult {
|
|
|
31
33
|
apps: FleetFlagApp[];
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
export interface
|
|
36
|
+
export interface VerifiedWorkspaceFeatureFlagMutationResult {
|
|
37
|
+
contractVersion: 3;
|
|
38
|
+
status: "verified";
|
|
39
|
+
key: string;
|
|
40
|
+
rules: Record<string, unknown>;
|
|
41
|
+
scope: { orgId: string | null; orgDomain: string | null };
|
|
42
|
+
enabledForCurrentUser: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface TargetFeatureFlagMutationResult {
|
|
35
46
|
contractVersion: 2;
|
|
36
47
|
status: "ready";
|
|
37
48
|
key: string;
|
|
@@ -39,6 +50,10 @@ export interface WorkspaceFeatureFlagMutationResult {
|
|
|
39
50
|
scope: { orgId: string | null; orgDomain: string | null };
|
|
40
51
|
}
|
|
41
52
|
|
|
53
|
+
export type WorkspaceFeatureFlagMutationResult =
|
|
54
|
+
| TargetFeatureFlagMutationResult
|
|
55
|
+
| VerifiedWorkspaceFeatureFlagMutationResult;
|
|
56
|
+
|
|
42
57
|
export interface WorkspaceFeatureFlagMutationInput {
|
|
43
58
|
appId: string;
|
|
44
59
|
key: string;
|
|
@@ -72,8 +87,8 @@ export function validateWorkspaceFeatureFlagMutation(
|
|
|
72
87
|
rules?: Record<string, unknown>;
|
|
73
88
|
enabledForEmail?: string;
|
|
74
89
|
},
|
|
75
|
-
):
|
|
76
|
-
const payload = body as Partial<
|
|
90
|
+
): TargetFeatureFlagMutationResult {
|
|
91
|
+
const payload = body as Partial<TargetFeatureFlagMutationResult> | null;
|
|
77
92
|
const valid =
|
|
78
93
|
payload?.contractVersion === 2 &&
|
|
79
94
|
payload.status === "ready" &&
|
|
@@ -144,7 +159,7 @@ export function validateWorkspaceFeatureFlagMutation(
|
|
|
144
159
|
"The target app did not enable the feature flag for the delegated operator.",
|
|
145
160
|
);
|
|
146
161
|
}
|
|
147
|
-
return payload as
|
|
162
|
+
return payload as TargetFeatureFlagMutationResult;
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
function targetOrigin(app: OrgApp): string {
|
|
@@ -173,6 +188,39 @@ async function delegatedToken(
|
|
|
173
188
|
}
|
|
174
189
|
}
|
|
175
190
|
|
|
191
|
+
export type WorkspaceFeatureFlagFailurePhase =
|
|
192
|
+
| "directory"
|
|
193
|
+
| "token-generation"
|
|
194
|
+
| "timeout"
|
|
195
|
+
| "network"
|
|
196
|
+
| "authorization"
|
|
197
|
+
| "unsupported-target"
|
|
198
|
+
| "target-action"
|
|
199
|
+
| "persistence"
|
|
200
|
+
| "verification";
|
|
201
|
+
|
|
202
|
+
const FAILURE_MESSAGES: Record<WorkspaceFeatureFlagFailurePhase, string> = {
|
|
203
|
+
directory: "The organization app directory could not resolve this target.",
|
|
204
|
+
"token-generation":
|
|
205
|
+
"Analytics could not create delegated feature flag authority.",
|
|
206
|
+
timeout: "The target app timed out before the feature flag change completed.",
|
|
207
|
+
network: "Analytics could not reach the target app.",
|
|
208
|
+
authorization: "The target app denied this delegated flag operation.",
|
|
209
|
+
"unsupported-target":
|
|
210
|
+
"The target app does not support feature flag management.",
|
|
211
|
+
"target-action": "The target app could not complete the feature flag action.",
|
|
212
|
+
persistence:
|
|
213
|
+
"The target app did not persist the requested feature flag rules.",
|
|
214
|
+
verification: "Analytics could not verify the persisted feature flag change.",
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export class WorkspaceFeatureFlagFailure extends Error {
|
|
218
|
+
constructor(readonly phase: WorkspaceFeatureFlagFailurePhase) {
|
|
219
|
+
super(`[${phase}] ${FAILURE_MESSAGES[phase]}`);
|
|
220
|
+
this.name = "WorkspaceFeatureFlagFailure";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
176
224
|
type TargetFailureReason = "token-generation" | "timeout" | "network";
|
|
177
225
|
|
|
178
226
|
class TargetCallFailure extends Error {
|
|
@@ -194,6 +242,30 @@ export function classifyWorkspaceFeatureFlagTargetFailure(
|
|
|
194
242
|
return "network";
|
|
195
243
|
}
|
|
196
244
|
|
|
245
|
+
function targetFailure(error: unknown): WorkspaceFeatureFlagFailure {
|
|
246
|
+
const reason = classifyWorkspaceFeatureFlagTargetFailure(error);
|
|
247
|
+
return new WorkspaceFeatureFlagFailure(reason);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function resolveTargetApp(
|
|
251
|
+
admin: AnalyticsAdminContext,
|
|
252
|
+
appId: string,
|
|
253
|
+
): Promise<OrgApp> {
|
|
254
|
+
let apps: OrgApp[];
|
|
255
|
+
try {
|
|
256
|
+
apps = await fetchOrgApps({
|
|
257
|
+
selfId: "analytics",
|
|
258
|
+
includeDirectoryApp: true,
|
|
259
|
+
serviceOrgId: admin.orgId,
|
|
260
|
+
});
|
|
261
|
+
} catch {
|
|
262
|
+
throw new WorkspaceFeatureFlagFailure("directory");
|
|
263
|
+
}
|
|
264
|
+
const app = apps.find((candidate) => candidate.id === appId);
|
|
265
|
+
if (!app) throw new WorkspaceFeatureFlagFailure("directory");
|
|
266
|
+
return app;
|
|
267
|
+
}
|
|
268
|
+
|
|
197
269
|
async function callTarget(
|
|
198
270
|
app: OrgApp,
|
|
199
271
|
admin: AnalyticsAdminContext,
|
|
@@ -326,63 +398,139 @@ export async function setWorkspaceFeatureFlag(
|
|
|
326
398
|
admin: AnalyticsAdminContext,
|
|
327
399
|
input: WorkspaceFeatureFlagMutationInput,
|
|
328
400
|
): Promise<WorkspaceFeatureFlagMutationResult> {
|
|
329
|
-
const
|
|
330
|
-
selfId: "analytics",
|
|
331
|
-
includeDirectoryApp: true,
|
|
332
|
-
serviceOrgId: admin.orgId,
|
|
333
|
-
});
|
|
334
|
-
const app = apps.find((candidate) => candidate.id === input.appId);
|
|
335
|
-
if (!app)
|
|
336
|
-
throw new Error(
|
|
337
|
-
"The requested app is not available in this organization directory.",
|
|
338
|
-
);
|
|
401
|
+
const app = await resolveTargetApp(admin, input.appId);
|
|
339
402
|
const targetInput = workspaceFeatureFlagTargetInput(input);
|
|
340
403
|
let orgDomain: string | undefined;
|
|
341
404
|
try {
|
|
342
405
|
orgDomain = (await getOrgDomain(admin.orgId))?.trim().toLowerCase();
|
|
343
406
|
} catch {
|
|
344
|
-
throw new
|
|
407
|
+
throw new WorkspaceFeatureFlagFailure("token-generation");
|
|
408
|
+
}
|
|
409
|
+
if (!orgDomain) throw new WorkspaceFeatureFlagFailure("token-generation");
|
|
410
|
+
let result: Awaited<ReturnType<typeof callTarget>>;
|
|
411
|
+
try {
|
|
412
|
+
result = await callTarget(
|
|
413
|
+
app,
|
|
414
|
+
admin,
|
|
415
|
+
"set-feature-flag",
|
|
416
|
+
targetInput,
|
|
417
|
+
orgDomain,
|
|
418
|
+
);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
throw targetFailure(error);
|
|
345
421
|
}
|
|
346
|
-
if (!orgDomain) throw new TargetCallFailure("token-generation");
|
|
347
|
-
const result = await callTarget(
|
|
348
|
-
app,
|
|
349
|
-
admin,
|
|
350
|
-
"set-feature-flag",
|
|
351
|
-
targetInput,
|
|
352
|
-
orgDomain,
|
|
353
|
-
);
|
|
354
422
|
if (result.status === 401 || result.status === 403)
|
|
355
|
-
throw new
|
|
423
|
+
throw new WorkspaceFeatureFlagFailure("authorization");
|
|
356
424
|
if (result.status === 404 || result.status === 405)
|
|
357
|
-
throw new
|
|
425
|
+
throw new WorkspaceFeatureFlagFailure("unsupported-target");
|
|
358
426
|
if (result.status < 200 || result.status >= 300)
|
|
359
|
-
throw new
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
? {
|
|
368
|
-
rules: {
|
|
369
|
-
mode: input.rules.mode,
|
|
370
|
-
emails: input.rules.emails ?? [],
|
|
371
|
-
orgIds: input.rules.orgIds ?? [],
|
|
372
|
-
percentage: input.rules.percentage ?? 0,
|
|
373
|
-
},
|
|
374
|
-
}
|
|
375
|
-
: input.operation === "off"
|
|
427
|
+
throw new WorkspaceFeatureFlagFailure("target-action");
|
|
428
|
+
let mutation: TargetFeatureFlagMutationResult;
|
|
429
|
+
try {
|
|
430
|
+
mutation = validateWorkspaceFeatureFlagMutation(result.body, {
|
|
431
|
+
key: input.key,
|
|
432
|
+
orgDomain,
|
|
433
|
+
allowExplicitNoOrgTarget: true,
|
|
434
|
+
...(input.operation === "replace-rules" && input.rules
|
|
376
435
|
? {
|
|
377
436
|
rules: {
|
|
378
|
-
mode:
|
|
379
|
-
emails: [],
|
|
380
|
-
orgIds: [],
|
|
381
|
-
percentage: 0,
|
|
437
|
+
mode: input.rules.mode,
|
|
438
|
+
emails: input.rules.emails ?? [],
|
|
439
|
+
orgIds: input.rules.orgIds ?? [],
|
|
440
|
+
percentage: input.rules.percentage ?? 0,
|
|
382
441
|
},
|
|
383
442
|
}
|
|
384
|
-
:
|
|
385
|
-
|
|
443
|
+
: input.operation === "off"
|
|
444
|
+
? {
|
|
445
|
+
rules: {
|
|
446
|
+
mode: "off",
|
|
447
|
+
emails: [],
|
|
448
|
+
orgIds: [],
|
|
449
|
+
percentage: 0,
|
|
450
|
+
},
|
|
451
|
+
}
|
|
452
|
+
: { enabledForEmail: admin.userEmail }),
|
|
453
|
+
});
|
|
454
|
+
} catch {
|
|
455
|
+
throw new WorkspaceFeatureFlagFailure("persistence");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (
|
|
459
|
+
!(await isFeatureFlagEnabled(VERIFIED_FLEET_FLAG_MUTATIONS, {
|
|
460
|
+
userEmail: admin.userEmail,
|
|
461
|
+
userKey: admin.userEmail,
|
|
462
|
+
orgId: admin.orgId,
|
|
463
|
+
}))
|
|
464
|
+
) {
|
|
465
|
+
return mutation;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let readBack: Awaited<ReturnType<typeof callTarget>>;
|
|
469
|
+
try {
|
|
470
|
+
readBack = await callTarget(
|
|
471
|
+
app,
|
|
472
|
+
admin,
|
|
473
|
+
"list-feature-flags",
|
|
474
|
+
{},
|
|
475
|
+
orgDomain,
|
|
476
|
+
);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
throw targetFailure(error);
|
|
479
|
+
}
|
|
480
|
+
if (readBack.status === 401 || readBack.status === 403)
|
|
481
|
+
throw new WorkspaceFeatureFlagFailure("authorization");
|
|
482
|
+
if (readBack.status === 404 || readBack.status === 405)
|
|
483
|
+
throw new WorkspaceFeatureFlagFailure("unsupported-target");
|
|
484
|
+
if (readBack.status < 200 || readBack.status >= 300)
|
|
485
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
486
|
+
const verifiedApp = classifyWorkspaceFeatureFlagList(app, readBack);
|
|
487
|
+
if (verifiedApp.state === "forbidden")
|
|
488
|
+
throw new WorkspaceFeatureFlagFailure("authorization");
|
|
489
|
+
if (verifiedApp.state === "unsupported")
|
|
490
|
+
throw new WorkspaceFeatureFlagFailure("unsupported-target");
|
|
491
|
+
const verifiedFlag = verifiedApp.flags.find((flag) => flag.key === input.key);
|
|
492
|
+
const expectedEnabled =
|
|
493
|
+
input.operation === "enable-for-current-user"
|
|
494
|
+
? true
|
|
495
|
+
: input.operation === "off"
|
|
496
|
+
? false
|
|
497
|
+
: null;
|
|
498
|
+
if (
|
|
499
|
+
verifiedApp.state !== "ready" ||
|
|
500
|
+
!verifiedFlag ||
|
|
501
|
+
typeof verifiedFlag.enabledForCurrentUser !== "boolean" ||
|
|
502
|
+
(expectedEnabled !== null &&
|
|
503
|
+
verifiedFlag.enabledForCurrentUser !== expectedEnabled)
|
|
504
|
+
) {
|
|
505
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
validateWorkspaceFeatureFlagMutation(
|
|
509
|
+
{
|
|
510
|
+
contractVersion: 2,
|
|
511
|
+
status: "ready",
|
|
512
|
+
key: input.key,
|
|
513
|
+
rules: verifiedFlag.rules,
|
|
514
|
+
scope: mutation.scope,
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
key: input.key,
|
|
518
|
+
orgDomain,
|
|
519
|
+
allowExplicitNoOrgTarget: true,
|
|
520
|
+
rules: mutation.rules,
|
|
521
|
+
},
|
|
522
|
+
);
|
|
523
|
+
} catch {
|
|
524
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
contractVersion: 3,
|
|
528
|
+
status: "verified",
|
|
529
|
+
key: mutation.key,
|
|
530
|
+
rules: verifiedFlag.rules as Record<string, unknown>,
|
|
531
|
+
scope: mutation.scope,
|
|
532
|
+
enabledForCurrentUser: verifiedFlag.enabledForCurrentUser,
|
|
533
|
+
};
|
|
386
534
|
}
|
|
387
535
|
|
|
388
536
|
export async function getWorkspaceFlagTarget(
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineFeatureFlag } from "@agent-native/core/feature-flags/registry";
|
|
2
|
+
|
|
3
|
+
export const VERIFIED_FLEET_FLAG_MUTATIONS = defineFeatureFlag({
|
|
4
|
+
key: "analytics.verified-fleet-flag-mutations",
|
|
5
|
+
displayName: "Verified fleet flag mutations",
|
|
6
|
+
description:
|
|
7
|
+
"Verify cross-app feature flag changes with an independent target read-back.",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const ANALYTICS_FEATURE_FLAGS = [VERIFIED_FLEET_FLAG_MUTATIONS];
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
ReminderControls,
|
|
28
28
|
} from "@/components/calendar/EventOptionControls";
|
|
29
29
|
import { FindTimeTakeover } from "@/components/calendar/FindTimePanel";
|
|
30
|
+
import { RepeatPicker } from "@/components/calendar/InlineEventPickers";
|
|
30
31
|
import { TimezoneCombobox } from "@/components/TimezoneCombobox";
|
|
31
32
|
import { Button } from "@/components/ui/button";
|
|
32
33
|
import {
|
|
@@ -71,6 +72,7 @@ import { getGoogleEventColorHex } from "@/lib/event-colors";
|
|
|
71
72
|
import { buildEventFormInitializationKey } from "@/lib/event-form-initialization";
|
|
72
73
|
import {
|
|
73
74
|
attachmentsToDrafts,
|
|
75
|
+
buildCustomRecurrenceRules,
|
|
74
76
|
buildRecurrenceRules,
|
|
75
77
|
buildReminderPayload,
|
|
76
78
|
createAttachmentDraft,
|
|
@@ -78,9 +80,11 @@ import {
|
|
|
78
80
|
dateTimeInTimezoneToIso,
|
|
79
81
|
getEventEndValidationMessage,
|
|
80
82
|
getRecurrencePreset,
|
|
83
|
+
parseCustomRecurrence,
|
|
81
84
|
remindersToDraftState,
|
|
82
85
|
resolveEventTimezone,
|
|
83
86
|
type AttachmentDraft,
|
|
87
|
+
type CustomRecurrenceDraft,
|
|
84
88
|
type RecurrencePreset,
|
|
85
89
|
type ReminderDraft,
|
|
86
90
|
type ReminderMode,
|
|
@@ -270,6 +274,10 @@ export function CreateEventPopover({
|
|
|
270
274
|
const [visibility, setVisibility] = useState<Visibility>("default");
|
|
271
275
|
const [recurrencePreset, setRecurrencePreset] =
|
|
272
276
|
useState<RecurrencePreset>("none");
|
|
277
|
+
const [customRecurrence, setCustomRecurrence] =
|
|
278
|
+
useState<CustomRecurrenceDraft>(() =>
|
|
279
|
+
parseCustomRecurrence(undefined, defaultDateStr),
|
|
280
|
+
);
|
|
273
281
|
const [timezone, setTimezone] = useState(defaultTimezone);
|
|
274
282
|
const [colorId, setColorId] = useState<string | undefined>();
|
|
275
283
|
const [reminderMode, setReminderMode] = useState<ReminderMode>("default");
|
|
@@ -377,6 +385,9 @@ export function CreateEventPopover({
|
|
|
377
385
|
setAvailability(draft.transparency ?? "opaque");
|
|
378
386
|
setVisibility(draft.visibility ?? "default");
|
|
379
387
|
setRecurrencePreset(getRecurrencePreset(draft.recurrence));
|
|
388
|
+
setCustomRecurrence(
|
|
389
|
+
parseCustomRecurrence(draft.recurrence, draft.start || nextDate),
|
|
390
|
+
);
|
|
380
391
|
setTimezone(draftTimezone);
|
|
381
392
|
setColorId(draft.colorId);
|
|
382
393
|
setReminderMode(reminderState.mode);
|
|
@@ -416,6 +427,7 @@ export function CreateEventPopover({
|
|
|
416
427
|
setAvailability("opaque");
|
|
417
428
|
setVisibility("default");
|
|
418
429
|
setRecurrencePreset("none");
|
|
430
|
+
setCustomRecurrence(parseCustomRecurrence(undefined, nextDate));
|
|
419
431
|
setTimezone(defaultTimezone);
|
|
420
432
|
setColorId(undefined);
|
|
421
433
|
setReminderMode("default");
|
|
@@ -478,11 +490,14 @@ export function CreateEventPopover({
|
|
|
478
490
|
: dateTimeInTimezoneToIso(endDate, endTime, eventTimezone);
|
|
479
491
|
const attachmentResult = validateAttachmentDrafts(attachments);
|
|
480
492
|
const reminderPatch = buildReminderPayload(reminderMode, reminders);
|
|
481
|
-
const recurrence =
|
|
482
|
-
recurrencePreset
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
493
|
+
const recurrence =
|
|
494
|
+
recurrencePreset === "custom"
|
|
495
|
+
? buildCustomRecurrenceRules(customRecurrence)
|
|
496
|
+
: buildRecurrenceRules(
|
|
497
|
+
recurrencePreset,
|
|
498
|
+
effectiveAllDay ? date : startValue,
|
|
499
|
+
eventTimezone,
|
|
500
|
+
);
|
|
486
501
|
const nextDraft: CalendarEventDraft = {
|
|
487
502
|
id: draftId,
|
|
488
503
|
createdAt: draft?.createdAt,
|
|
@@ -569,6 +584,7 @@ export function CreateEventPopover({
|
|
|
569
584
|
availability,
|
|
570
585
|
visibility,
|
|
571
586
|
recurrencePreset,
|
|
587
|
+
customRecurrence,
|
|
572
588
|
eventTimezone,
|
|
573
589
|
colorId,
|
|
574
590
|
reminderMode,
|
|
@@ -767,11 +783,14 @@ export function CreateEventPopover({
|
|
|
767
783
|
...trailingAttendees,
|
|
768
784
|
]);
|
|
769
785
|
const reminderPatch = buildReminderPayload(reminderMode, reminders);
|
|
770
|
-
const recurrence =
|
|
771
|
-
recurrencePreset
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
786
|
+
const recurrence =
|
|
787
|
+
recurrencePreset === "custom"
|
|
788
|
+
? buildCustomRecurrenceRules(customRecurrence)
|
|
789
|
+
: buildRecurrenceRules(
|
|
790
|
+
recurrencePreset,
|
|
791
|
+
effectiveAllDay ? date : startValue,
|
|
792
|
+
eventTimezone,
|
|
793
|
+
);
|
|
775
794
|
const statusPatch =
|
|
776
795
|
eventType === "default"
|
|
777
796
|
? {}
|
|
@@ -1435,44 +1454,14 @@ export function CreateEventPopover({
|
|
|
1435
1454
|
<Label htmlFor="event-recurrence" className="text-xs">
|
|
1436
1455
|
{t("eventForm.repeats")}
|
|
1437
1456
|
</Label>
|
|
1438
|
-
<
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1457
|
+
<RepeatPicker
|
|
1458
|
+
preset={recurrencePreset}
|
|
1459
|
+
referenceDate={
|
|
1460
|
+
effectiveAllDay ? date : currentStartISO || date
|
|
1442
1461
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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>
|
|
1462
|
+
onChange={setRecurrencePreset}
|
|
1463
|
+
onCustomChange={setCustomRecurrence}
|
|
1464
|
+
/>
|
|
1476
1465
|
</div>
|
|
1477
1466
|
|
|
1478
1467
|
{(!allDay || isOutOfOffice) && (
|
|
@@ -441,6 +441,7 @@ const DayEventCard = memo(function DayEventCard({
|
|
|
441
441
|
onDelete={onDeleteEvent}
|
|
442
442
|
isDraft={isDraft}
|
|
443
443
|
defaultOpen={defaultOpen}
|
|
444
|
+
popoverSide="bottom"
|
|
444
445
|
onTitleSave={onQuickEditSave}
|
|
445
446
|
onDismissNew={onQuickEditCancel}
|
|
446
447
|
onDraftUpdate={onDraftUpdate}
|
|
@@ -810,6 +811,7 @@ export const DayView = memo(function DayView({
|
|
|
810
811
|
onDelete={onDeleteEvent}
|
|
811
812
|
isDraft={draftEventIds.includes(event.id)}
|
|
812
813
|
defaultOpen={quickEditEventId === event.id}
|
|
814
|
+
popoverSide="bottom"
|
|
813
815
|
onTitleSave={onQuickEditSave}
|
|
814
816
|
onDismissNew={onQuickEditCancel}
|
|
815
817
|
onDraftUpdate={onDraftUpdate}
|
|
@@ -887,6 +889,7 @@ export const DayView = memo(function DayView({
|
|
|
887
889
|
onDelete={onDeleteEvent}
|
|
888
890
|
isDraft={draftEventIds.includes(event.id)}
|
|
889
891
|
defaultOpen={quickEditEventId === event.id}
|
|
892
|
+
popoverSide="bottom"
|
|
890
893
|
onTitleSave={onQuickEditSave}
|
|
891
894
|
onDismissNew={onQuickEditCancel}
|
|
892
895
|
onDraftUpdate={onDraftUpdate}
|