@agent-native/core 0.176.1 → 0.177.0-nightly-20260828155142
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/set-workspace-feature-flag.ts +2 -2
- package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +16 -1
- package/corpus/templates/analytics/changelog/2026-08-27-show-analytics-s-own-registered-flags-in-fleet-feature-flag-.md +6 -0
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +204 -32
- package/corpus/templates/assets/changelog/2026-08-26-templates-across-brand-kits.md +1 -0
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +61 -68
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +9 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +3 -3
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +8 -3
- package/corpus/templates/calendar/app/lib/calendar-timezone.ts +12 -3
- package/corpus/templates/calendar/changelog/2026-08-28-all-day-events-stay-in-the-compact-top-bar-in-day-and-week-v.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-28-event-creators-can-choose-whether-a-meeting-shows-as-free-or.md +6 -0
- package/corpus/templates/clips/app/global.css +0 -27
- package/corpus/templates/clips/app/i18n/en-US.ts +2 -0
- package/corpus/templates/clips/app/routes/download.tsx +131 -16
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +35 -20
- package/corpus/templates/clips/server/routes/api/clips-latest.json.get.ts +77 -22
- package/corpus/templates/forms/actions/response-insights.ts +4 -4
- package/corpus/templates/forms/actions/retry-response-deliveries.ts +26 -0
- package/corpus/templates/forms/server/db/schema.ts +64 -17
- package/corpus/templates/forms/server/handlers/submissions.ts +1022 -94
- package/corpus/templates/forms/server/lib/integrations.ts +100 -33
- package/corpus/templates/forms/server/middleware/00-public-cors.ts +1 -1
- package/corpus/templates/forms/server/plugins/db.ts +40 -0
- package/corpus/templates/plan/agent-native.json +1 -4
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +6 -1
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +2 -2
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -6
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +78 -12
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +4 -4
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +111 -27
- package/corpus/templates/slides/app/components/editor/SlidesLayersPanel.tsx +20 -34
- package/corpus/templates/slides/app/components/editor/slide-style.ts +2 -0
- package/corpus/templates/slides/app/global.css +35 -0
- package/corpus/templates/slides/app/lib/sanitize-slide-html.ts +133 -5
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +675 -0
- package/corpus/templates/slides/app/lib/validate-native-slide-html.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +44 -44
- package/corpus/templates/slides/app/root.tsx +1 -22
- package/corpus/templates/slides/changelog/2026-08-27-image-fit-controls-apply-cover-crops-and-position.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-28-shared-slides-with-a-style-block-render-again.md +6 -0
- package/dist/action.d.ts +3 -1
- package/dist/client/FeedbackButton.d.ts +12 -0
- package/dist/client/FeedbackButton.js +62 -28
- package/dist/client/app-providers.js +13 -1
- package/dist/client/feedback-context.d.ts +1 -0
- package/dist/client/feedback-context.js +3 -2
- package/dist/client/host-bridge.d.ts +1 -0
- package/dist/client/observability/ThumbsFeedback.js +73 -10
- package/dist/client/webmcp.d.ts +4 -0
- package/dist/client/webmcp.js +51 -4
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/observability/routes.js +8 -2
- package/dist/observability/store.d.ts +1 -1
- package/dist/observability/store.js +18 -3
- package/dist/observability/types.d.ts +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-routes.d.ts +1 -6
- package/dist/server/action-routes.js +58 -17
- package/dist/server/agent-chat-plugin.js +8 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/webmcp.mdx +31 -12
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -30,7 +30,7 @@ const schema = z.discriminatedUnion("operation", [
|
|
|
30
30
|
]);
|
|
31
31
|
export default defineAction({
|
|
32
32
|
description:
|
|
33
|
-
"Persist one feature-flag change on a trusted organization app.
|
|
33
|
+
"Persist one feature-flag change on Analytics or a trusted organization app. Analytics uses its local flag action; peer targets are resolved through the organization directory.",
|
|
34
34
|
schema,
|
|
35
35
|
agentInputSchema: z.object({
|
|
36
36
|
appId: z.string(),
|
|
@@ -40,6 +40,6 @@ export default defineAction({
|
|
|
40
40
|
}),
|
|
41
41
|
run: async (args, ctx) => {
|
|
42
42
|
const admin = await requireAnalyticsAdminContext(ctx);
|
|
43
|
-
return setWorkspaceFeatureFlag(admin, args);
|
|
43
|
+
return setWorkspaceFeatureFlag(admin, args, ctx);
|
|
44
44
|
},
|
|
45
45
|
});
|
|
@@ -89,6 +89,10 @@ function asDirectory(value: unknown): FlagDirectory {
|
|
|
89
89
|
return value && typeof value === "object" ? (value as FlagDirectory) : {};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
export function hasFleetApps(value: unknown): boolean {
|
|
93
|
+
return (asDirectory(value).apps?.length ?? 0) > 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
92
96
|
function isReady(app: FlagApp) {
|
|
93
97
|
return app.state === "ready" || app.status === "ready";
|
|
94
98
|
}
|
|
@@ -188,7 +192,10 @@ export function FeatureFlagsFleetPanel() {
|
|
|
188
192
|
const directory = asDirectory(flags.data);
|
|
189
193
|
const apps = directory.apps ?? [];
|
|
190
194
|
if (flags.isLoading) return <PanelLoading />;
|
|
191
|
-
if (
|
|
195
|
+
if (
|
|
196
|
+
flags.error ||
|
|
197
|
+
(directory.directoryStatus === "unavailable" && !hasFleetApps(directory))
|
|
198
|
+
)
|
|
192
199
|
return (
|
|
193
200
|
<StatusState
|
|
194
201
|
title={t("agents.flagsUnavailable")}
|
|
@@ -226,6 +233,14 @@ export function FeatureFlagsFleetPanel() {
|
|
|
226
233
|
{t("agents.reloadFlags")}
|
|
227
234
|
</Button>
|
|
228
235
|
</div>
|
|
236
|
+
{directory.directoryStatus === "unavailable" ? (
|
|
237
|
+
<p
|
|
238
|
+
className="rounded-lg border px-4 py-3 text-sm text-muted-foreground"
|
|
239
|
+
role="status"
|
|
240
|
+
>
|
|
241
|
+
{t("agents.flagsUnreachable")}
|
|
242
|
+
</p>
|
|
243
|
+
) : null}
|
|
229
244
|
{apps.map((app) => (
|
|
230
245
|
<section key={appId(app)} className="overflow-hidden rounded-lg border">
|
|
231
246
|
<div className="flex items-center justify-between gap-3 border-b bg-muted/30 px-4 py-3">
|
|
@@ -4,14 +4,18 @@ import { signA2AToken } from "@agent-native/core/a2a";
|
|
|
4
4
|
import {
|
|
5
5
|
ActionContractError,
|
|
6
6
|
AgentActionStopError,
|
|
7
|
+
type ActionRunContext,
|
|
7
8
|
} from "@agent-native/core/action";
|
|
8
9
|
import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
|
|
10
|
+
import listLocalFeatureFlagsAction from "@agent-native/core/feature-flags/actions/list-feature-flags";
|
|
11
|
+
import setLocalFeatureFlagAction from "@agent-native/core/feature-flags/actions/set-feature-flag";
|
|
9
12
|
import {
|
|
10
13
|
fetchOrgApps,
|
|
11
14
|
fetchOrgAppsResult,
|
|
12
15
|
type OrgApp,
|
|
13
16
|
} from "@agent-native/core/mcp";
|
|
14
17
|
import { getOrgDomain } from "@agent-native/core/org";
|
|
18
|
+
import { getAppConfig } from "@agent-native/core/server";
|
|
15
19
|
|
|
16
20
|
import {
|
|
17
21
|
RESILIENT_FLEET_FLAG_DIRECTORY,
|
|
@@ -23,6 +27,7 @@ const TARGET_TIMEOUT_MS = 3_000;
|
|
|
23
27
|
const DIRECTORY_ATTEMPTS = 2;
|
|
24
28
|
const VERIFICATION_ATTEMPTS = 2;
|
|
25
29
|
const CONCURRENCY = 4;
|
|
30
|
+
const ANALYTICS_APP_ID = "analytics";
|
|
26
31
|
|
|
27
32
|
export type FleetFlagState =
|
|
28
33
|
| "ready"
|
|
@@ -91,6 +96,37 @@ export function workspaceFeatureFlagTargetInput(
|
|
|
91
96
|
: rawTargetInput;
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
function mutationValidationExpectation(
|
|
100
|
+
admin: AnalyticsAdminContext,
|
|
101
|
+
input: WorkspaceFeatureFlagMutationInput,
|
|
102
|
+
orgDomain: string,
|
|
103
|
+
) {
|
|
104
|
+
return {
|
|
105
|
+
key: input.key,
|
|
106
|
+
orgDomain,
|
|
107
|
+
allowExplicitNoOrgTarget: true,
|
|
108
|
+
...(input.operation === "replace-rules" && input.rules
|
|
109
|
+
? {
|
|
110
|
+
rules: {
|
|
111
|
+
mode: input.rules.mode,
|
|
112
|
+
emails: input.rules.emails ?? [],
|
|
113
|
+
orgIds: input.rules.orgIds ?? [],
|
|
114
|
+
percentage: input.rules.percentage ?? 0,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
: input.operation === "off"
|
|
118
|
+
? {
|
|
119
|
+
rules: {
|
|
120
|
+
mode: "off",
|
|
121
|
+
emails: [],
|
|
122
|
+
orgIds: [],
|
|
123
|
+
percentage: 0,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
: { enabledForEmail: admin.userEmail }),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
94
130
|
export function validateWorkspaceFeatureFlagMutation(
|
|
95
131
|
body: unknown,
|
|
96
132
|
expected: {
|
|
@@ -179,6 +215,54 @@ function targetOrigin(app: OrgApp): string {
|
|
|
179
215
|
return new URL(app.url).origin;
|
|
180
216
|
}
|
|
181
217
|
|
|
218
|
+
function localAnalyticsApp(): OrgApp {
|
|
219
|
+
const configuredUrl = getAppConfig().app.url;
|
|
220
|
+
const url = configuredUrl ?? "http://analytics.local";
|
|
221
|
+
return {
|
|
222
|
+
id: ANALYTICS_APP_ID,
|
|
223
|
+
name: "Analytics",
|
|
224
|
+
url,
|
|
225
|
+
a2aUrl: url,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function localActionContext(
|
|
230
|
+
admin: AnalyticsAdminContext,
|
|
231
|
+
actionName: "list-feature-flags" | "set-feature-flag",
|
|
232
|
+
sourceContext?: ActionRunContext,
|
|
233
|
+
) {
|
|
234
|
+
return {
|
|
235
|
+
...sourceContext,
|
|
236
|
+
caller: sourceContext?.caller ?? ("http" as const),
|
|
237
|
+
userEmail: admin.userEmail,
|
|
238
|
+
orgId: admin.orgId,
|
|
239
|
+
appId: ANALYTICS_APP_ID,
|
|
240
|
+
actionName,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function listLocalAnalyticsFeatureFlags(admin: AnalyticsAdminContext) {
|
|
245
|
+
return classifyWorkspaceFeatureFlagList(localAnalyticsApp(), {
|
|
246
|
+
status: 200,
|
|
247
|
+
body: await listLocalFeatureFlagsAction.run(
|
|
248
|
+
{},
|
|
249
|
+
localActionContext(admin, "list-feature-flags"),
|
|
250
|
+
),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function unreachableLocalAnalyticsEntry(): FleetFlagApp {
|
|
255
|
+
const app = localAnalyticsApp();
|
|
256
|
+
return {
|
|
257
|
+
appId: app.id,
|
|
258
|
+
appName: app.name,
|
|
259
|
+
appOrigin: targetOrigin(app),
|
|
260
|
+
state: "unreachable",
|
|
261
|
+
flags: [],
|
|
262
|
+
reason: "target-execution",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
182
266
|
async function delegatedToken(
|
|
183
267
|
admin: AnalyticsAdminContext,
|
|
184
268
|
origin: string,
|
|
@@ -298,6 +382,18 @@ function targetFailure(error: unknown): WorkspaceFeatureFlagFailure {
|
|
|
298
382
|
return new WorkspaceFeatureFlagFailure(reason);
|
|
299
383
|
}
|
|
300
384
|
|
|
385
|
+
function localMutationFailure(error: unknown): WorkspaceFeatureFlagFailure {
|
|
386
|
+
const statusCode =
|
|
387
|
+
error && typeof error === "object" && "statusCode" in error
|
|
388
|
+
? (error as { statusCode?: unknown }).statusCode
|
|
389
|
+
: undefined;
|
|
390
|
+
return new WorkspaceFeatureFlagFailure(
|
|
391
|
+
statusCode === 401 || statusCode === 403
|
|
392
|
+
? "authorization"
|
|
393
|
+
: "target-action",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
301
397
|
async function resolveTargetApp(
|
|
302
398
|
admin: AnalyticsAdminContext,
|
|
303
399
|
appId: string,
|
|
@@ -471,13 +567,20 @@ async function mapBounded<T, R>(
|
|
|
471
567
|
export async function listWorkspaceFeatureFlags(
|
|
472
568
|
admin: AnalyticsAdminContext,
|
|
473
569
|
): Promise<WorkspaceFeatureFlagsResult> {
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
570
|
+
const [localEntry, directoryResult] = await Promise.all([
|
|
571
|
+
listLocalAnalyticsFeatureFlags(admin).catch(() =>
|
|
572
|
+
unreachableLocalAnalyticsEntry(),
|
|
573
|
+
),
|
|
574
|
+
fetchOrgAppsResult({
|
|
575
|
+
selfId: ANALYTICS_APP_ID,
|
|
576
|
+
includeDirectoryApp: true,
|
|
577
|
+
serviceOrgId: admin.orgId,
|
|
578
|
+
}),
|
|
579
|
+
]);
|
|
580
|
+
if (directoryResult.status === "unavailable")
|
|
581
|
+
return { directoryStatus: "unavailable", apps: [localEntry] };
|
|
582
|
+
const directoryApps = directoryResult.apps;
|
|
583
|
+
const entries = await mapBounded(directoryApps, async (app) => {
|
|
481
584
|
try {
|
|
482
585
|
return classifyWorkspaceFeatureFlagList(
|
|
483
586
|
app,
|
|
@@ -494,13 +597,101 @@ export async function listWorkspaceFeatureFlags(
|
|
|
494
597
|
};
|
|
495
598
|
}
|
|
496
599
|
});
|
|
497
|
-
return { directoryStatus: "available", apps: entries };
|
|
600
|
+
return { directoryStatus: "available", apps: [localEntry, ...entries] };
|
|
498
601
|
}
|
|
499
602
|
|
|
500
603
|
export async function setWorkspaceFeatureFlag(
|
|
501
604
|
admin: AnalyticsAdminContext,
|
|
502
605
|
input: WorkspaceFeatureFlagMutationInput,
|
|
606
|
+
sourceContext?: ActionRunContext,
|
|
503
607
|
): Promise<WorkspaceFeatureFlagMutationResult> {
|
|
608
|
+
if (input.appId === ANALYTICS_APP_ID) {
|
|
609
|
+
if (input.operation === "replace-rules" && !input.rules)
|
|
610
|
+
throw new WorkspaceFeatureFlagFailure("target-action");
|
|
611
|
+
const localRules = input.rules as
|
|
612
|
+
| {
|
|
613
|
+
mode: "off" | "on" | "rules";
|
|
614
|
+
emails?: string[];
|
|
615
|
+
orgIds?: string[];
|
|
616
|
+
percentage?: number;
|
|
617
|
+
}
|
|
618
|
+
| undefined;
|
|
619
|
+
const targetInput =
|
|
620
|
+
input.operation === "replace-rules"
|
|
621
|
+
? {
|
|
622
|
+
operation: input.operation,
|
|
623
|
+
key: input.key,
|
|
624
|
+
rules: localRules!,
|
|
625
|
+
}
|
|
626
|
+
: { operation: input.operation, key: input.key };
|
|
627
|
+
let result;
|
|
628
|
+
try {
|
|
629
|
+
result = await setLocalFeatureFlagAction.run(
|
|
630
|
+
targetInput,
|
|
631
|
+
localActionContext(admin, "set-feature-flag", sourceContext),
|
|
632
|
+
);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
throw localMutationFailure(error);
|
|
635
|
+
}
|
|
636
|
+
const orgDomain = result.scope.orgDomain ?? "";
|
|
637
|
+
let mutation: TargetFeatureFlagMutationResult;
|
|
638
|
+
try {
|
|
639
|
+
mutation = validateWorkspaceFeatureFlagMutation(
|
|
640
|
+
result,
|
|
641
|
+
mutationValidationExpectation(admin, input, orgDomain),
|
|
642
|
+
);
|
|
643
|
+
} catch {
|
|
644
|
+
throw new WorkspaceFeatureFlagFailure("persistence");
|
|
645
|
+
}
|
|
646
|
+
let verifiedApp: FleetFlagApp;
|
|
647
|
+
try {
|
|
648
|
+
verifiedApp = await listLocalAnalyticsFeatureFlags(admin);
|
|
649
|
+
} catch {
|
|
650
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
651
|
+
}
|
|
652
|
+
if (verifiedApp.state === "forbidden")
|
|
653
|
+
throw new WorkspaceFeatureFlagFailure("authorization");
|
|
654
|
+
const verifiedFlag = verifiedApp.flags.find(
|
|
655
|
+
(flag) => flag.key === input.key,
|
|
656
|
+
);
|
|
657
|
+
if (
|
|
658
|
+
verifiedApp.state !== "ready" ||
|
|
659
|
+
!verifiedFlag ||
|
|
660
|
+
typeof verifiedFlag.enabledForCurrentUser !== "boolean" ||
|
|
661
|
+
(input.operation === "enable-for-current-user" &&
|
|
662
|
+
!verifiedFlag.enabledForCurrentUser) ||
|
|
663
|
+
(input.operation === "off" && verifiedFlag.enabledForCurrentUser)
|
|
664
|
+
) {
|
|
665
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
validateWorkspaceFeatureFlagMutation(
|
|
669
|
+
{
|
|
670
|
+
contractVersion: 2,
|
|
671
|
+
status: "ready",
|
|
672
|
+
key: input.key,
|
|
673
|
+
rules: verifiedFlag.rules,
|
|
674
|
+
scope: mutation.scope,
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
key: input.key,
|
|
678
|
+
orgDomain,
|
|
679
|
+
allowExplicitNoOrgTarget: true,
|
|
680
|
+
rules: mutation.rules,
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
} catch {
|
|
684
|
+
throw new WorkspaceFeatureFlagFailure("verification");
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
contractVersion: 3,
|
|
688
|
+
status: "verified",
|
|
689
|
+
key: mutation.key,
|
|
690
|
+
rules: verifiedFlag.rules as Record<string, unknown>,
|
|
691
|
+
scope: mutation.scope,
|
|
692
|
+
enabledForCurrentUser: verifiedFlag.enabledForCurrentUser,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
504
695
|
const app = await resolveTargetApp(admin, input.appId);
|
|
505
696
|
const targetInput = workspaceFeatureFlagTargetInput(input);
|
|
506
697
|
let orgDomain: string | undefined;
|
|
@@ -533,30 +724,10 @@ export async function setWorkspaceFeatureFlag(
|
|
|
533
724
|
throw new WorkspaceFeatureFlagFailure("target-action");
|
|
534
725
|
let mutation: TargetFeatureFlagMutationResult;
|
|
535
726
|
try {
|
|
536
|
-
mutation = validateWorkspaceFeatureFlagMutation(
|
|
537
|
-
|
|
538
|
-
orgDomain,
|
|
539
|
-
|
|
540
|
-
...(input.operation === "replace-rules" && input.rules
|
|
541
|
-
? {
|
|
542
|
-
rules: {
|
|
543
|
-
mode: input.rules.mode,
|
|
544
|
-
emails: input.rules.emails ?? [],
|
|
545
|
-
orgIds: input.rules.orgIds ?? [],
|
|
546
|
-
percentage: input.rules.percentage ?? 0,
|
|
547
|
-
},
|
|
548
|
-
}
|
|
549
|
-
: input.operation === "off"
|
|
550
|
-
? {
|
|
551
|
-
rules: {
|
|
552
|
-
mode: "off",
|
|
553
|
-
emails: [],
|
|
554
|
-
orgIds: [],
|
|
555
|
-
percentage: 0,
|
|
556
|
-
},
|
|
557
|
-
}
|
|
558
|
-
: { enabledForEmail: admin.userEmail }),
|
|
559
|
-
});
|
|
727
|
+
mutation = validateWorkspaceFeatureFlagMutation(
|
|
728
|
+
result.body,
|
|
729
|
+
mutationValidationExpectation(admin, input, orgDomain),
|
|
730
|
+
);
|
|
560
731
|
} catch {
|
|
561
732
|
throw new WorkspaceFeatureFlagFailure("persistence");
|
|
562
733
|
}
|
|
@@ -632,6 +803,7 @@ export async function getWorkspaceFlagTarget(
|
|
|
632
803
|
admin: AnalyticsAdminContext,
|
|
633
804
|
appId: string,
|
|
634
805
|
): Promise<FleetFlagApp> {
|
|
806
|
+
if (appId === ANALYTICS_APP_ID) return listLocalAnalyticsFeatureFlags(admin);
|
|
635
807
|
const apps = await fetchOrgApps({
|
|
636
808
|
selfId: "analytics",
|
|
637
809
|
includeDirectoryApp: true,
|
|
@@ -1150,6 +1150,37 @@ export function CreateEventPopover({
|
|
|
1150
1150
|
</SelectContent>
|
|
1151
1151
|
</Select>
|
|
1152
1152
|
</div>
|
|
1153
|
+
{eventType === "default" && (
|
|
1154
|
+
<div className="flex items-center justify-between gap-3 py-1">
|
|
1155
|
+
<Label
|
|
1156
|
+
htmlFor="event-availability"
|
|
1157
|
+
className="text-sm text-muted-foreground"
|
|
1158
|
+
>
|
|
1159
|
+
{t("eventForm.showAs")}
|
|
1160
|
+
</Label>
|
|
1161
|
+
<Select
|
|
1162
|
+
value={availability}
|
|
1163
|
+
onValueChange={(value) =>
|
|
1164
|
+
setAvailability(value as Availability)
|
|
1165
|
+
}
|
|
1166
|
+
>
|
|
1167
|
+
<SelectTrigger
|
|
1168
|
+
id="event-availability"
|
|
1169
|
+
className="h-8 w-28 text-sm"
|
|
1170
|
+
>
|
|
1171
|
+
<SelectValue />
|
|
1172
|
+
</SelectTrigger>
|
|
1173
|
+
<SelectContent>
|
|
1174
|
+
<SelectItem value="opaque">
|
|
1175
|
+
{t("eventForm.busy")}
|
|
1176
|
+
</SelectItem>
|
|
1177
|
+
<SelectItem value="transparent">
|
|
1178
|
+
{t("eventForm.free")}
|
|
1179
|
+
</SelectItem>
|
|
1180
|
+
</SelectContent>
|
|
1181
|
+
</Select>
|
|
1182
|
+
</div>
|
|
1183
|
+
)}
|
|
1153
1184
|
{videoProvider === "zoom" && !zoomStatus.data?.connected && (
|
|
1154
1185
|
<div className="ml-7 rounded-md border border-border/60 bg-muted/20 p-2">
|
|
1155
1186
|
<div className="flex items-center justify-between gap-2">
|
|
@@ -1483,75 +1514,37 @@ export function CreateEventPopover({
|
|
|
1483
1514
|
</div>
|
|
1484
1515
|
)}
|
|
1485
1516
|
|
|
1486
|
-
<div className="
|
|
1487
|
-
<
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
disabled={eventType !== "default"}
|
|
1503
|
-
>
|
|
1504
|
-
<SelectTrigger
|
|
1505
|
-
id="event-availability"
|
|
1506
|
-
className="h-8 text-sm"
|
|
1507
|
-
>
|
|
1508
|
-
<SelectValue />
|
|
1509
|
-
</SelectTrigger>
|
|
1510
|
-
<SelectContent>
|
|
1511
|
-
<SelectItem value="opaque">
|
|
1512
|
-
{t("eventForm.busy")}
|
|
1513
|
-
</SelectItem>
|
|
1514
|
-
<SelectItem value="transparent">
|
|
1515
|
-
{t("eventForm.free")}
|
|
1516
|
-
</SelectItem>
|
|
1517
|
-
</SelectContent>
|
|
1518
|
-
</Select>
|
|
1519
|
-
</div>
|
|
1520
|
-
|
|
1521
|
-
<div className="space-y-1.5">
|
|
1522
|
-
<Label htmlFor="event-visibility" className="text-xs">
|
|
1523
|
-
{t("eventForm.visibility")}
|
|
1524
|
-
</Label>
|
|
1525
|
-
<Select
|
|
1526
|
-
value={
|
|
1527
|
-
eventType === "workingLocation"
|
|
1528
|
-
? "public"
|
|
1529
|
-
: visibility
|
|
1530
|
-
}
|
|
1531
|
-
onValueChange={(value) =>
|
|
1532
|
-
setVisibility(value as Visibility)
|
|
1533
|
-
}
|
|
1534
|
-
disabled={eventType === "workingLocation"}
|
|
1517
|
+
<div className="space-y-1.5">
|
|
1518
|
+
<Label htmlFor="event-visibility" className="text-xs">
|
|
1519
|
+
{t("eventForm.visibility")}
|
|
1520
|
+
</Label>
|
|
1521
|
+
<Select
|
|
1522
|
+
value={
|
|
1523
|
+
eventType === "workingLocation" ? "public" : visibility
|
|
1524
|
+
}
|
|
1525
|
+
onValueChange={(value) =>
|
|
1526
|
+
setVisibility(value as Visibility)
|
|
1527
|
+
}
|
|
1528
|
+
disabled={eventType === "workingLocation"}
|
|
1529
|
+
>
|
|
1530
|
+
<SelectTrigger
|
|
1531
|
+
id="event-visibility"
|
|
1532
|
+
className="h-8 text-sm"
|
|
1535
1533
|
>
|
|
1536
|
-
<
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
>
|
|
1540
|
-
|
|
1541
|
-
</
|
|
1542
|
-
<
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
{t("eventForm.private")}
|
|
1551
|
-
</SelectItem>
|
|
1552
|
-
</SelectContent>
|
|
1553
|
-
</Select>
|
|
1554
|
-
</div>
|
|
1534
|
+
<SelectValue />
|
|
1535
|
+
</SelectTrigger>
|
|
1536
|
+
<SelectContent>
|
|
1537
|
+
<SelectItem value="default">
|
|
1538
|
+
{t("eventForm.default")}
|
|
1539
|
+
</SelectItem>
|
|
1540
|
+
<SelectItem value="public">
|
|
1541
|
+
{t("eventForm.public")}
|
|
1542
|
+
</SelectItem>
|
|
1543
|
+
<SelectItem value="private">
|
|
1544
|
+
{t("eventForm.private")}
|
|
1545
|
+
</SelectItem>
|
|
1546
|
+
</SelectContent>
|
|
1547
|
+
</Select>
|
|
1555
1548
|
</div>
|
|
1556
1549
|
|
|
1557
1550
|
<div className="space-y-1.5">
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
getDateKeyInTimezone,
|
|
28
28
|
getEventDateKey,
|
|
29
29
|
getEventSegmentForCalendarDay,
|
|
30
|
+
isAllDayCalendarEvent,
|
|
30
31
|
} from "@/lib/calendar-timezone";
|
|
31
32
|
import { getEventDisplayColor, allOtherDeclined } from "@/lib/event-colors";
|
|
32
33
|
import {
|
|
@@ -556,7 +557,9 @@ export const DayView = memo(function DayView({
|
|
|
556
557
|
const allDayEvents = useMemo(
|
|
557
558
|
() =>
|
|
558
559
|
events.filter(
|
|
559
|
-
(event) =>
|
|
560
|
+
(event) =>
|
|
561
|
+
isAllDayCalendarEvent(event) ||
|
|
562
|
+
fullDayOutOfOfficeCoversDate(event, date),
|
|
560
563
|
),
|
|
561
564
|
[date, events],
|
|
562
565
|
);
|
|
@@ -568,14 +571,17 @@ export const DayView = memo(function DayView({
|
|
|
568
571
|
() =>
|
|
569
572
|
events.filter(
|
|
570
573
|
(event) =>
|
|
571
|
-
!event
|
|
574
|
+
!isAllDayCalendarEvent(event) &&
|
|
572
575
|
isOutOfOfficeEvent(event) &&
|
|
573
576
|
!isFullDayOutOfOfficeEvent(event),
|
|
574
577
|
),
|
|
575
578
|
[events],
|
|
576
579
|
);
|
|
577
580
|
const timedEvents = useMemo(
|
|
578
|
-
() =>
|
|
581
|
+
() =>
|
|
582
|
+
events.filter(
|
|
583
|
+
(event) => !isAllDayCalendarEvent(event) && !isOutOfOfficeEvent(event),
|
|
584
|
+
),
|
|
579
585
|
[events],
|
|
580
586
|
);
|
|
581
587
|
const layout = useMemo(
|
|
@@ -1524,7 +1524,7 @@ export function EventDetailPopover({
|
|
|
1524
1524
|
side={isMobile ? "bottom" : (popoverSide ?? "right")}
|
|
1525
1525
|
sideOffset={isMobile ? 6 : 8}
|
|
1526
1526
|
collisionPadding={12}
|
|
1527
|
-
className="flex max-h-[var(--radix-popover-content-available-height)] w-[min(
|
|
1527
|
+
className="flex max-h-[var(--radix-popover-content-available-height)] w-[min(420px,calc(100vw-2rem))] flex-col overflow-hidden p-0"
|
|
1528
1528
|
onClick={(e) => e.stopPropagation()}
|
|
1529
1529
|
onOpenAutoFocus={(e) => {
|
|
1530
1530
|
e.preventDefault();
|
|
@@ -1553,8 +1553,8 @@ export function EventDetailPopover({
|
|
|
1553
1553
|
>
|
|
1554
1554
|
<TooltipProvider>
|
|
1555
1555
|
{/* Header */}
|
|
1556
|
-
<div className="flex items-center justify-between border-b border-border
|
|
1557
|
-
<div className="text-
|
|
1556
|
+
<div className="flex items-center justify-between border-b border-border px-4 py-2.5">
|
|
1557
|
+
<div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
1558
1558
|
<span>
|
|
1559
1559
|
{isWorkingLocation
|
|
1560
1560
|
? t("eventForm.workingLocation")
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
getDateKeyInTimezone,
|
|
43
43
|
getEventDateKey,
|
|
44
44
|
getEventSegmentForCalendarDay,
|
|
45
|
+
isAllDayCalendarEvent,
|
|
45
46
|
} from "@/lib/calendar-timezone";
|
|
46
47
|
import { getEventDisplayColor, allOtherDeclined } from "@/lib/event-colors";
|
|
47
48
|
import {
|
|
@@ -633,7 +634,8 @@ export const WeekView = memo(function WeekView({
|
|
|
633
634
|
const allDayEvents = useMemo(
|
|
634
635
|
() =>
|
|
635
636
|
events.filter(
|
|
636
|
-
(event) =>
|
|
637
|
+
(event) =>
|
|
638
|
+
isAllDayCalendarEvent(event) || isFullDayOutOfOfficeEvent(event),
|
|
637
639
|
),
|
|
638
640
|
[events],
|
|
639
641
|
);
|
|
@@ -642,7 +644,7 @@ export const WeekView = memo(function WeekView({
|
|
|
642
644
|
() =>
|
|
643
645
|
events.filter(
|
|
644
646
|
(event) =>
|
|
645
|
-
!event
|
|
647
|
+
!isAllDayCalendarEvent(event) &&
|
|
646
648
|
isOutOfOfficeEvent(event) &&
|
|
647
649
|
!isFullDayOutOfOfficeEvent(event),
|
|
648
650
|
),
|
|
@@ -650,7 +652,10 @@ export const WeekView = memo(function WeekView({
|
|
|
650
652
|
);
|
|
651
653
|
|
|
652
654
|
const timedEvents = useMemo(
|
|
653
|
-
() =>
|
|
655
|
+
() =>
|
|
656
|
+
events.filter(
|
|
657
|
+
(event) => !isAllDayCalendarEvent(event) && !isOutOfOfficeEvent(event),
|
|
658
|
+
),
|
|
654
659
|
[events],
|
|
655
660
|
);
|
|
656
661
|
|
|
@@ -47,6 +47,15 @@ export function normalizeTimezone(timezone?: string): string {
|
|
|
47
47
|
return isCalendarTimezone(timezone) ? timezone : getBrowserTimezone();
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export function isAllDayCalendarEvent(
|
|
51
|
+
event: Pick<CalendarEvent, "allDay" | "start" | "end">,
|
|
52
|
+
): boolean {
|
|
53
|
+
return (
|
|
54
|
+
event.allDay ||
|
|
55
|
+
(DATE_ONLY_PATTERN.test(event.start) && DATE_ONLY_PATTERN.test(event.end))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
/** Date carriers are kept at local noon so browser DST never changes their date. */
|
|
51
60
|
export function dateKeyToDate(date: string): Date {
|
|
52
61
|
const [year, month, day] = date.split("-").map(Number);
|
|
@@ -205,7 +214,7 @@ function eventDateRange(
|
|
|
205
214
|
event: Pick<CalendarEvent, "start" | "end" | "allDay">,
|
|
206
215
|
timezone: string,
|
|
207
216
|
) {
|
|
208
|
-
if (event
|
|
217
|
+
if (isAllDayCalendarEvent(event)) {
|
|
209
218
|
const startDate = dateOnlyPart(event.start);
|
|
210
219
|
const endDate =
|
|
211
220
|
dateOnlyPart(event.end) ??
|
|
@@ -234,7 +243,7 @@ export function moveEventToCalendarDate(
|
|
|
234
243
|
const sourceStartDate = getEventDateKey(event, timezone);
|
|
235
244
|
if (!sourceStartDate) return null;
|
|
236
245
|
|
|
237
|
-
if (event
|
|
246
|
+
if (isAllDayCalendarEvent(event)) {
|
|
238
247
|
const sourceEndDate =
|
|
239
248
|
dateOnlyPart(event.end) ?? addCalendarDays(sourceStartDate, 1);
|
|
240
249
|
const spanDays = Math.max(
|
|
@@ -285,7 +294,7 @@ export function eventOverlapsCalendarDay(
|
|
|
285
294
|
): boolean {
|
|
286
295
|
const normalizedTimezone = normalizeTimezone(timezone);
|
|
287
296
|
const dayBounds = getCalendarDayBounds(day, normalizedTimezone);
|
|
288
|
-
if (event
|
|
297
|
+
if (isAllDayCalendarEvent(event)) {
|
|
289
298
|
const range = eventDateRange(event, normalizedTimezone);
|
|
290
299
|
const dayDate = dayBounds.date;
|
|
291
300
|
return Boolean(
|