@agent-native/core 0.130.0 → 0.130.2
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/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +481 -122
- package/corpus/core/src/integrations/a2a-continuations-store.ts +345 -14
- package/corpus/core/src/integrations/adapters/slack.ts +243 -53
- package/corpus/core/src/integrations/integration-campaign-recovery.ts +4 -1
- package/corpus/core/src/integrations/plugin.ts +39 -21
- package/corpus/core/src/integrations/task-queue-stats.ts +146 -0
- package/corpus/core/src/integrations/types.ts +25 -4
- package/corpus/core/src/integrations/webhook-handler.ts +18 -5
- package/corpus/core/src/server/email-template.ts +43 -13
- package/corpus/core/src/sharing/actions/share-resource.ts +26 -4
- package/corpus/core/src/sharing/index.ts +1 -0
- package/corpus/core/src/sharing/registry.ts +23 -0
- package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +3 -1
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
- package/corpus/templates/clips/app/components/meetings/day-header.tsx +1 -1
- package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +1 -22
- package/corpus/templates/clips/app/components/workspace/branding-editor.tsx +26 -1
- package/corpus/templates/clips/app/components/workspace/organization-identity-card.tsx +107 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -4
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +0 -10
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +23 -51
- package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +1 -8
- package/corpus/templates/clips/changelog/2026-07-29-clearing-all-recording-edits-no-longer-leaves-the-editor-unr.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-clip-share-emails-now-open-with-who-shared-the-clip-add-a-su.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-finished-meeting-pills-now-have-a-clear-close-button.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-meeting-cards-no-longer-flicker-or-disappear-when-hovered-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-organization-name-logo-brand-color-and-default-visibility-no.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-transcript-lines-on-the-meeting-page-no-longer-jump-to-the-r.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-when-a-meeting-wraps-up-the-recording-pill-now-shows-a-green.md +6 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +6 -3
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +112 -32
- package/corpus/templates/clips/desktop/src/styles.css +42 -2
- package/corpus/templates/clips/server/db/index.ts +6 -0
- package/corpus/templates/clips/server/lib/share-email-hero.ts +33 -0
- package/corpus/templates/clips/shared/clips-ai-prefs.ts +0 -1
- package/corpus/templates/dispatch/changelog/2026-07-26-slack-replies-recover-after-connected-app-updates.md +6 -0
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +15 -0
- package/corpus/templates/slides/actions/delete-design-system.ts +145 -0
- package/corpus/templates/slides/actions/list-design-systems.ts +94 -2
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +43 -1
- package/corpus/templates/slides/app/hooks/use-design-systems.ts +2 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +7 -0
- package/corpus/templates/slides/app/pages/DesignSystems.tsx +56 -1
- package/corpus/templates/slides/changelog/2026-07-28-delete-a-design-system-you-own-from-the-design-systems-page-.md +6 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/a2a-continuation-processor.d.ts +5 -0
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +293 -79
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts +23 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +249 -15
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +155 -31
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/integration-campaign-recovery.d.ts.map +1 -1
- package/dist/integrations/integration-campaign-recovery.js +3 -1
- package/dist/integrations/integration-campaign-recovery.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +29 -12
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/task-queue-stats.d.ts +12 -0
- package/dist/integrations/task-queue-stats.d.ts.map +1 -1
- package/dist/integrations/task-queue-stats.js +106 -0
- package/dist/integrations/task-queue-stats.js.map +1 -1
- package/dist/integrations/types.d.ts +24 -6
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.d.ts.map +1 -1
- package/dist/integrations/webhook-handler.js +12 -3
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/email-template.d.ts +4 -0
- package/dist/server/email-template.d.ts.map +1 -1
- package/dist/server/email-template.js +25 -8
- package/dist/server/email-template.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/sharing/actions/share-resource.d.ts.map +1 -1
- package/dist/sharing/actions/share-resource.js +23 -4
- package/dist/sharing/actions/share-resource.js.map +1 -1
- package/dist/sharing/index.d.ts +1 -1
- package/dist/sharing/index.d.ts.map +1 -1
- package/dist/sharing/index.js.map +1 -1
- package/dist/sharing/registry.d.ts +23 -0
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js.map +1 -1
- package/package.json +1 -1
- package/src/integrations/a2a-continuation-processor.ts +481 -122
- package/src/integrations/a2a-continuations-store.ts +345 -14
- package/src/integrations/adapters/slack.ts +243 -53
- package/src/integrations/integration-campaign-recovery.ts +4 -1
- package/src/integrations/plugin.ts +39 -21
- package/src/integrations/task-queue-stats.ts +146 -0
- package/src/integrations/types.ts +25 -4
- package/src/integrations/webhook-handler.ts +18 -5
- package/src/server/email-template.ts +43 -13
- package/src/sharing/actions/share-resource.ts +26 -4
- package/src/sharing/index.ts +1 -0
- package/src/sharing/registry.ts +23 -0
- package/corpus/templates/clips/app/routes/_app.settings.organization.tsx +0 -232
|
@@ -5652,7 +5652,8 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
5652
5652
|
opacity: 0.6;
|
|
5653
5653
|
}
|
|
5654
5654
|
|
|
5655
|
-
.pill-expand-btn
|
|
5655
|
+
.pill-expand-btn,
|
|
5656
|
+
.pill-close-btn {
|
|
5656
5657
|
margin-left: 4px;
|
|
5657
5658
|
display: inline-flex;
|
|
5658
5659
|
height: 28px;
|
|
@@ -5667,7 +5668,8 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
5667
5668
|
transition: background 120ms;
|
|
5668
5669
|
}
|
|
5669
5670
|
|
|
5670
|
-
.pill-expand-btn:hover
|
|
5671
|
+
.pill-expand-btn:hover,
|
|
5672
|
+
.pill-close-btn:hover {
|
|
5671
5673
|
background: rgba(255, 255, 255, 0.1);
|
|
5672
5674
|
}
|
|
5673
5675
|
|
|
@@ -5862,6 +5864,44 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
5862
5864
|
background: rgba(255, 255, 255, 0.1);
|
|
5863
5865
|
}
|
|
5864
5866
|
|
|
5867
|
+
.pill-finished-banner {
|
|
5868
|
+
display: flex;
|
|
5869
|
+
align-items: center;
|
|
5870
|
+
gap: 8px;
|
|
5871
|
+
flex-shrink: 0;
|
|
5872
|
+
margin: 8px 12px 2px;
|
|
5873
|
+
padding: 8px 10px;
|
|
5874
|
+
border: 1px solid rgba(34, 197, 94, 0.45);
|
|
5875
|
+
border-radius: 8px;
|
|
5876
|
+
background: rgba(34, 197, 94, 0.16);
|
|
5877
|
+
}
|
|
5878
|
+
|
|
5879
|
+
.pill-finished-text {
|
|
5880
|
+
min-width: 0;
|
|
5881
|
+
flex: 1;
|
|
5882
|
+
font-size: 12px;
|
|
5883
|
+
font-weight: 600;
|
|
5884
|
+
line-height: 1.35;
|
|
5885
|
+
color: #86efac;
|
|
5886
|
+
}
|
|
5887
|
+
|
|
5888
|
+
.pill-finished-open {
|
|
5889
|
+
flex-shrink: 0;
|
|
5890
|
+
padding: 5px 9px;
|
|
5891
|
+
border: none;
|
|
5892
|
+
border-radius: 6px;
|
|
5893
|
+
background: #22c55e;
|
|
5894
|
+
color: #052e16;
|
|
5895
|
+
font-size: 11px;
|
|
5896
|
+
font-weight: 700;
|
|
5897
|
+
cursor: pointer;
|
|
5898
|
+
transition: background 120ms;
|
|
5899
|
+
}
|
|
5900
|
+
|
|
5901
|
+
.pill-finished-open:hover {
|
|
5902
|
+
background: #4ade80;
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5865
5905
|
.pill-transcript-area {
|
|
5866
5906
|
min-height: 0;
|
|
5867
5907
|
flex: 1;
|
|
@@ -5,6 +5,7 @@ import { eq } from "drizzle-orm";
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
absoluteUrl,
|
|
8
|
+
recordingShareEmailExtras,
|
|
8
9
|
recordingShareHeroHtml,
|
|
9
10
|
} from "../lib/share-email-hero.js";
|
|
10
11
|
import * as schema from "./schema.js";
|
|
@@ -58,6 +59,11 @@ registerShareableResource({
|
|
|
58
59
|
replyTo: ctx.sender.email,
|
|
59
60
|
}),
|
|
60
61
|
getHeroHtml: (recording, ctx) => recordingShareHeroHtml(recording, ctx),
|
|
62
|
+
getShareEmailExtras: (_recording, ctx) =>
|
|
63
|
+
recordingShareEmailExtras({
|
|
64
|
+
href: ctx.href,
|
|
65
|
+
senderEmail: ctx.sender.email,
|
|
66
|
+
}),
|
|
61
67
|
getDb,
|
|
62
68
|
ownerAccessIgnoresOrg: true,
|
|
63
69
|
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
emailLink,
|
|
2
3
|
getAppProductionUrl,
|
|
3
4
|
withConfiguredAppBasePath,
|
|
4
5
|
} from "@agent-native/core/server";
|
|
6
|
+
import type { ShareEmailExtras } from "@agent-native/core/sharing";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Origin plus the configured mount path. Deployments served under
|
|
@@ -52,6 +54,37 @@ function isHttpUrl(value: string): boolean {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Clips-specific body for the share-notification email: the AI companion CTA,
|
|
59
|
+
* the copyable agent link, and the closing note that routes replies to the
|
|
60
|
+
* person who shared the clip.
|
|
61
|
+
*/
|
|
62
|
+
export function recordingShareEmailExtras(ctx: {
|
|
63
|
+
href: string;
|
|
64
|
+
senderEmail: string;
|
|
65
|
+
}): ShareEmailExtras {
|
|
66
|
+
const summarizeUrl = new URL(ctx.href);
|
|
67
|
+
summarizeUrl.searchParams.set("panel", "agent");
|
|
68
|
+
return {
|
|
69
|
+
// The heading already says who shared what, so the clip thumbnail follows
|
|
70
|
+
// it directly.
|
|
71
|
+
paragraphs: [],
|
|
72
|
+
secondaryCta: { label: "Summarize with AI", url: summarizeUrl.toString() },
|
|
73
|
+
linkBlock: {
|
|
74
|
+
intro: "Copy and paste this link for your own AI agent to summarize:",
|
|
75
|
+
url: ctx.href,
|
|
76
|
+
placement: "after-cta",
|
|
77
|
+
},
|
|
78
|
+
closingParagraphs: [
|
|
79
|
+
"Clips is a 100% free, open-source, Agent-Native app for sharing screengrabs with friends and colleagues. No download required.",
|
|
80
|
+
`Just reply to this email if you want to get back to ${emailLink(
|
|
81
|
+
ctx.senderEmail,
|
|
82
|
+
`mailto:${ctx.senderEmail}`,
|
|
83
|
+
)} directly.`,
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
55
88
|
export type ShareHeroRecording = {
|
|
56
89
|
thumbnailUrl?: string | null;
|
|
57
90
|
animatedThumbnailUrl?: string | null;
|
|
@@ -40,6 +40,21 @@ Do not call `create-design-system` locally from `.fig` uploads. Do not call
|
|
|
40
40
|
`import-document` for `.fig` files; it only handles metadata and will miss the
|
|
41
41
|
Builder indexing flow.
|
|
42
42
|
|
|
43
|
+
## Deleting a Design System
|
|
44
|
+
|
|
45
|
+
`delete-design-system` requires admin access or higher (owner or admin share
|
|
46
|
+
role) and removes the system, its shares, and the `designSystemId` link on
|
|
47
|
+
every linked deck the caller can edit — decks the caller can't edit keep a
|
|
48
|
+
dangling reference instead of being silently mutated, reported back as
|
|
49
|
+
`decksSkippedForAccess` (clear it later with `patch-deck`'s
|
|
50
|
+
`patch-deck-fields`, `designSystemId: null`). Those decks keep the tokens
|
|
51
|
+
already baked into their slides — deletion never rewrites deck content — so a
|
|
52
|
+
deck can look on-brand while no longer being linked to a system. If the
|
|
53
|
+
deleted system was the caller's default, another of their design systems is
|
|
54
|
+
promoted to default so future deck creation doesn't silently drop to "no
|
|
55
|
+
design system". Deletion does not remove an upstream Builder-indexed design
|
|
56
|
+
system.
|
|
57
|
+
|
|
43
58
|
## Applying to Slides
|
|
44
59
|
|
|
45
60
|
Before creating or extending a system, read the `creative-context` skill and
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
assertAccess,
|
|
4
|
+
resolveAccess,
|
|
5
|
+
type ShareRole,
|
|
6
|
+
} from "@agent-native/core/sharing";
|
|
7
|
+
import { desc, eq } from "drizzle-orm";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
11
|
+
import { withDeckLock } from "./patch-deck.js";
|
|
12
|
+
|
|
13
|
+
function canEditDeckRole(role: "owner" | ShareRole) {
|
|
14
|
+
return role === "owner" || role === "admin" || role === "editor";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type UnlinkResult =
|
|
18
|
+
| { deckId: string; status: "unlinked" }
|
|
19
|
+
| { deckId: string; status: "skipped-no-access" };
|
|
20
|
+
|
|
21
|
+
// Runs under the same per-deck lock patch-deck/save-deck/update-slide use for
|
|
22
|
+
// all deck writes, re-reading the row inside the lock so a concurrent slide
|
|
23
|
+
// edit can't be clobbered by this read-modify-write. Decks the caller can't
|
|
24
|
+
// edit are left pointing at the deleted id rather than silently mutated —
|
|
25
|
+
// admin access on a shared design system does not imply edit access on every
|
|
26
|
+
// deck that happens to reference it.
|
|
27
|
+
function unlinkDeck(
|
|
28
|
+
deckId: string,
|
|
29
|
+
designSystemId: string,
|
|
30
|
+
): Promise<UnlinkResult> {
|
|
31
|
+
return withDeckLock(deckId, async () => {
|
|
32
|
+
const access = await resolveAccess("deck", deckId);
|
|
33
|
+
if (!access || !canEditDeckRole(access.role)) {
|
|
34
|
+
return { deckId, status: "skipped-no-access" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const db = getDb();
|
|
38
|
+
const [deck] = await db
|
|
39
|
+
.select({
|
|
40
|
+
designSystemId: schema.decks.designSystemId,
|
|
41
|
+
data: schema.decks.data,
|
|
42
|
+
})
|
|
43
|
+
.from(schema.decks)
|
|
44
|
+
.where(eq(schema.decks.id, deckId));
|
|
45
|
+
if (!deck || deck.designSystemId !== designSystemId) {
|
|
46
|
+
return { deckId, status: "unlinked" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const data = JSON.parse(deck.data);
|
|
50
|
+
if ("designSystemId" in data) delete data.designSystemId;
|
|
51
|
+
await db
|
|
52
|
+
.update(schema.decks)
|
|
53
|
+
.set({
|
|
54
|
+
designSystemId: null,
|
|
55
|
+
data: JSON.stringify(data),
|
|
56
|
+
updatedAt: new Date().toISOString(),
|
|
57
|
+
})
|
|
58
|
+
.where(eq(schema.decks.id, deckId));
|
|
59
|
+
return { deckId, status: "unlinked" };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default defineAction({
|
|
64
|
+
description:
|
|
65
|
+
"Delete a design system. Requires admin access or higher. Decks linked to it that " +
|
|
66
|
+
"the caller can edit are unlinked; others keep a dangling reference (clear it with " +
|
|
67
|
+
"patch-deck's patch-deck-fields, designSystemId: null). If the deleted system was the " +
|
|
68
|
+
"caller's default, another of their design systems is promoted to default.",
|
|
69
|
+
schema: z.object({
|
|
70
|
+
id: z.string().min(1).describe("Design system ID to delete"),
|
|
71
|
+
}),
|
|
72
|
+
run: async ({ id }) => {
|
|
73
|
+
const access = await assertAccess("design-system", id, "admin");
|
|
74
|
+
|
|
75
|
+
const db = getDb();
|
|
76
|
+
|
|
77
|
+
const linkedDeckIds = (
|
|
78
|
+
await db
|
|
79
|
+
.select({ id: schema.decks.id })
|
|
80
|
+
.from(schema.decks)
|
|
81
|
+
.where(eq(schema.decks.designSystemId, id))
|
|
82
|
+
).map((row) => row.id);
|
|
83
|
+
|
|
84
|
+
// Delete the design system (and its shares) before touching linked decks.
|
|
85
|
+
// Once the row is gone, apply-design-system/create-deck's
|
|
86
|
+
// assertAccess("design-system", ...) check fails for anyone trying to
|
|
87
|
+
// attach a fresh link, shrinking the window for a deck to end up pointing
|
|
88
|
+
// at a design system we're about to remove. If this was the owner's
|
|
89
|
+
// default, promote another of their design systems in the same
|
|
90
|
+
// transaction so deck creation never silently drops to "no design
|
|
91
|
+
// system" just because the default happened to be deleted.
|
|
92
|
+
await db.transaction(async (tx) => {
|
|
93
|
+
await tx
|
|
94
|
+
.delete(schema.designSystemShares)
|
|
95
|
+
.where(eq(schema.designSystemShares.resourceId, id));
|
|
96
|
+
|
|
97
|
+
await tx
|
|
98
|
+
.delete(schema.designSystems)
|
|
99
|
+
.where(eq(schema.designSystems.id, id));
|
|
100
|
+
|
|
101
|
+
if (access.resource.isDefault) {
|
|
102
|
+
const [next] = await tx
|
|
103
|
+
.select({ id: schema.designSystems.id })
|
|
104
|
+
.from(schema.designSystems)
|
|
105
|
+
.where(
|
|
106
|
+
eq(schema.designSystems.ownerEmail, access.resource.ownerEmail),
|
|
107
|
+
)
|
|
108
|
+
.orderBy(desc(schema.designSystems.updatedAt))
|
|
109
|
+
.limit(1);
|
|
110
|
+
if (next) {
|
|
111
|
+
await tx
|
|
112
|
+
.update(schema.designSystems)
|
|
113
|
+
.set({ isDefault: true, updatedAt: new Date().toISOString() })
|
|
114
|
+
.where(eq(schema.designSystems.id, next.id));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Best-effort cleanup: the design system is already gone, so a deck we
|
|
120
|
+
// can't touch (missing access) or fail to update (write error) is left
|
|
121
|
+
// dangling rather than retried here. Both cases are reported back instead
|
|
122
|
+
// of swallowed, so the caller can decide whether to intervene.
|
|
123
|
+
const settled = await Promise.allSettled(
|
|
124
|
+
linkedDeckIds.map((deckId) => unlinkDeck(deckId, id)),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const decksSkippedForAccess: string[] = [];
|
|
128
|
+
const decksFailedToUnlink: string[] = [];
|
|
129
|
+
settled.forEach((result, index) => {
|
|
130
|
+
const deckId = linkedDeckIds[index];
|
|
131
|
+
if (result.status === "rejected") {
|
|
132
|
+
decksFailedToUnlink.push(deckId);
|
|
133
|
+
} else if (result.value.status === "skipped-no-access") {
|
|
134
|
+
decksSkippedForAccess.push(deckId);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
id,
|
|
140
|
+
deleted: true,
|
|
141
|
+
...(decksSkippedForAccess.length > 0 ? { decksSkippedForAccess } : {}),
|
|
142
|
+
...(decksFailedToUnlink.length > 0 ? { decksFailedToUnlink } : {}),
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
});
|
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
getRequestOrgId,
|
|
4
|
+
getRequestUserEmail,
|
|
5
|
+
} from "@agent-native/core/server/request-context";
|
|
6
|
+
import {
|
|
7
|
+
accessFilter,
|
|
8
|
+
ROLE_RANK,
|
|
9
|
+
type ShareRole,
|
|
10
|
+
} from "@agent-native/core/sharing";
|
|
11
|
+
import { and, desc, eq, inArray, or, sql } from "drizzle-orm";
|
|
4
12
|
import { z } from "zod";
|
|
5
13
|
|
|
6
14
|
import { getDb, schema } from "../server/db/index.js";
|
|
7
15
|
|
|
16
|
+
type EffectiveRole = "owner" | ShareRole;
|
|
17
|
+
|
|
18
|
+
function canManageRole(role: EffectiveRole) {
|
|
19
|
+
return role === "owner" || role === "admin";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Mirrors the core access model (assertAccess/resolveAccess), which compares
|
|
23
|
+
// emails with `lower(column) = lowercased-input` so a share or ownership
|
|
24
|
+
// grant survives casing differences between the stored principal and the
|
|
25
|
+
// caller's session email.
|
|
26
|
+
function normalizeEmail(email: string | undefined): string | null {
|
|
27
|
+
const normalized = email?.trim().toLowerCase();
|
|
28
|
+
return normalized || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function strongerRole(current: ShareRole | null, next: ShareRole): ShareRole {
|
|
32
|
+
if (!current || ROLE_RANK[next] > ROLE_RANK[current]) return next;
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
|
|
8
36
|
export default defineAction({
|
|
9
37
|
description:
|
|
10
38
|
"List all design systems accessible to the current user. " +
|
|
@@ -19,6 +47,8 @@ export default defineAction({
|
|
|
19
47
|
http: { method: "GET" },
|
|
20
48
|
run: async (args) => {
|
|
21
49
|
const db = getDb();
|
|
50
|
+
const userEmail = normalizeEmail(getRequestUserEmail());
|
|
51
|
+
const orgId = getRequestOrgId();
|
|
22
52
|
// Project only the columns this list returns. The default path returns
|
|
23
53
|
// `data`, but neither path returns the heavy `assets` blob — a bare
|
|
24
54
|
// `.select()` would load it off every row for nothing.
|
|
@@ -30,6 +60,8 @@ export default defineAction({
|
|
|
30
60
|
data: schema.designSystems.data,
|
|
31
61
|
isDefault: schema.designSystems.isDefault,
|
|
32
62
|
visibility: schema.designSystems.visibility,
|
|
63
|
+
ownerEmail: schema.designSystems.ownerEmail,
|
|
64
|
+
orgId: schema.designSystems.orgId,
|
|
33
65
|
createdAt: schema.designSystems.createdAt,
|
|
34
66
|
updatedAt: schema.designSystems.updatedAt,
|
|
35
67
|
})
|
|
@@ -41,12 +73,70 @@ export default defineAction({
|
|
|
41
73
|
return { count: 0, designSystems: [] };
|
|
42
74
|
}
|
|
43
75
|
|
|
76
|
+
// Resolve every row's role from a single batched shares query instead of
|
|
77
|
+
// calling resolveAccess() per row, which would re-load each resource and
|
|
78
|
+
// its shares (N+1) and fan out an unbounded Promise.all as the list grows.
|
|
79
|
+
const principalClauses: NonNullable<ReturnType<typeof and>>[] = [];
|
|
80
|
+
if (userEmail) {
|
|
81
|
+
principalClauses.push(
|
|
82
|
+
and(
|
|
83
|
+
eq(schema.designSystemShares.principalType, "user"),
|
|
84
|
+
sql`lower(${schema.designSystemShares.principalId}) = ${userEmail}`,
|
|
85
|
+
)!,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (orgId) {
|
|
89
|
+
principalClauses.push(
|
|
90
|
+
and(
|
|
91
|
+
eq(schema.designSystemShares.principalType, "org"),
|
|
92
|
+
eq(schema.designSystemShares.principalId, orgId),
|
|
93
|
+
)!,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const shareRoleById = new Map<string, ShareRole>();
|
|
98
|
+
if (principalClauses.length > 0) {
|
|
99
|
+
const shareRows = await db
|
|
100
|
+
.select({
|
|
101
|
+
resourceId: schema.designSystemShares.resourceId,
|
|
102
|
+
role: schema.designSystemShares.role,
|
|
103
|
+
})
|
|
104
|
+
.from(schema.designSystemShares)
|
|
105
|
+
.where(
|
|
106
|
+
and(
|
|
107
|
+
inArray(
|
|
108
|
+
schema.designSystemShares.resourceId,
|
|
109
|
+
rows.map((row) => row.id),
|
|
110
|
+
),
|
|
111
|
+
or(...principalClauses),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
for (const share of shareRows) {
|
|
115
|
+
shareRoleById.set(
|
|
116
|
+
share.resourceId,
|
|
117
|
+
strongerRole(shareRoleById.get(share.resourceId) ?? null, share.role),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
44
122
|
const items = rows.map((row) => {
|
|
123
|
+
let role: EffectiveRole = shareRoleById.get(row.id) ?? "viewer";
|
|
124
|
+
if (
|
|
125
|
+
userEmail &&
|
|
126
|
+
normalizeEmail(row.ownerEmail) === userEmail &&
|
|
127
|
+
(!row.orgId || row.orgId === orgId)
|
|
128
|
+
) {
|
|
129
|
+
role = "owner";
|
|
130
|
+
}
|
|
131
|
+
const canManage = canManageRole(role);
|
|
132
|
+
|
|
45
133
|
if (args.compact === "true") {
|
|
46
134
|
return {
|
|
47
135
|
id: row.id,
|
|
48
136
|
title: row.title,
|
|
49
137
|
isDefault: row.isDefault,
|
|
138
|
+
accessRole: role,
|
|
139
|
+
canManage,
|
|
50
140
|
};
|
|
51
141
|
}
|
|
52
142
|
return {
|
|
@@ -56,6 +146,8 @@ export default defineAction({
|
|
|
56
146
|
data: row.data,
|
|
57
147
|
isDefault: row.isDefault,
|
|
58
148
|
visibility: row.visibility,
|
|
149
|
+
accessRole: role,
|
|
150
|
+
canManage,
|
|
59
151
|
createdAt: row.createdAt,
|
|
60
152
|
updatedAt: row.updatedAt,
|
|
61
153
|
};
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
1
2
|
import { ShareButton } from "@agent-native/core/client/sharing";
|
|
2
3
|
import { VisibilityBadge } from "@agent-native/toolkit/sharing";
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
IconDots,
|
|
6
|
+
IconPalette,
|
|
7
|
+
IconStar,
|
|
8
|
+
IconStarFilled,
|
|
9
|
+
IconTrash,
|
|
10
|
+
} from "@tabler/icons-react";
|
|
4
11
|
|
|
12
|
+
import { Button } from "@/components/ui/button";
|
|
13
|
+
import {
|
|
14
|
+
DropdownMenu,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuItem,
|
|
17
|
+
DropdownMenuTrigger,
|
|
18
|
+
} from "@/components/ui/dropdown-menu";
|
|
5
19
|
import {
|
|
6
20
|
Tooltip,
|
|
7
21
|
TooltipContent,
|
|
@@ -16,8 +30,10 @@ interface DesignSystemCardProps {
|
|
|
16
30
|
data: DesignSystemData;
|
|
17
31
|
isDefault: boolean;
|
|
18
32
|
visibility?: "private" | "org" | "public" | null;
|
|
33
|
+
canManage?: boolean;
|
|
19
34
|
onClick: () => void;
|
|
20
35
|
onSetDefault: () => void;
|
|
36
|
+
onDelete: () => void;
|
|
21
37
|
}
|
|
22
38
|
|
|
23
39
|
function firstFontName(stack: string): string {
|
|
@@ -30,9 +46,12 @@ export function DesignSystemCard({
|
|
|
30
46
|
data,
|
|
31
47
|
isDefault,
|
|
32
48
|
visibility,
|
|
49
|
+
canManage,
|
|
33
50
|
onClick,
|
|
34
51
|
onSetDefault,
|
|
52
|
+
onDelete,
|
|
35
53
|
}: DesignSystemCardProps) {
|
|
54
|
+
const t = useT();
|
|
36
55
|
const swatchColors = [
|
|
37
56
|
{ label: "Primary", color: data.colors.primary },
|
|
38
57
|
{ label: "Secondary", color: data.colors.secondary },
|
|
@@ -93,6 +112,29 @@ export function DesignSystemCard({
|
|
|
93
112
|
resourceId={id}
|
|
94
113
|
resourceTitle={title}
|
|
95
114
|
/>
|
|
115
|
+
{canManage && (
|
|
116
|
+
<DropdownMenu modal={false}>
|
|
117
|
+
<DropdownMenuTrigger asChild>
|
|
118
|
+
<Button
|
|
119
|
+
variant="ghost"
|
|
120
|
+
size="icon"
|
|
121
|
+
aria-label={t("designSystems.moreActions")}
|
|
122
|
+
className="h-9 w-9 rounded-md bg-background/80 backdrop-blur-sm border border-border/40 hover:bg-background cursor-pointer"
|
|
123
|
+
>
|
|
124
|
+
<IconDots className="w-4 h-4 text-muted-foreground" />
|
|
125
|
+
</Button>
|
|
126
|
+
</DropdownMenuTrigger>
|
|
127
|
+
<DropdownMenuContent align="end">
|
|
128
|
+
<DropdownMenuItem
|
|
129
|
+
onClick={onDelete}
|
|
130
|
+
className="text-red-400 focus:text-red-400"
|
|
131
|
+
>
|
|
132
|
+
<IconTrash className="w-3.5 h-3.5 me-2" />
|
|
133
|
+
{t("designSystems.delete")}
|
|
134
|
+
</DropdownMenuItem>
|
|
135
|
+
</DropdownMenuContent>
|
|
136
|
+
</DropdownMenu>
|
|
137
|
+
)}
|
|
96
138
|
</div>
|
|
97
139
|
|
|
98
140
|
{/* Typography preview */}
|
|
@@ -162,6 +162,13 @@ const messages = {
|
|
|
162
162
|
designSystems: {
|
|
163
163
|
new: "New Design System",
|
|
164
164
|
setupBrand: "Set up your brand",
|
|
165
|
+
delete: "Delete",
|
|
166
|
+
cancel: "Cancel",
|
|
167
|
+
moreActions: "More actions",
|
|
168
|
+
deleteDialogTitle: "Delete design system?",
|
|
169
|
+
deleteDialogDescription:
|
|
170
|
+
"This permanently deletes the design system. Decks using it keep their current look but are no longer linked to it.",
|
|
171
|
+
deleteError: "Failed to delete design system",
|
|
165
172
|
emptyTitle: "Set up your brand identity",
|
|
166
173
|
emptyDescription:
|
|
167
174
|
"Create a design system with your brand colors, typography, and logos. Every new deck will follow your visual identity.",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { callAction } from "@agent-native/core/client/hooks";
|
|
1
|
+
import { callAction, useActionMutation } from "@agent-native/core/client/hooks";
|
|
2
2
|
import { useT } from "@agent-native/core/client/i18n";
|
|
3
3
|
import {
|
|
4
4
|
useSetHeaderActions,
|
|
@@ -11,9 +11,20 @@ import {
|
|
|
11
11
|
IconRefresh,
|
|
12
12
|
} from "@tabler/icons-react";
|
|
13
13
|
import { useMemo, useState } from "react";
|
|
14
|
+
import { toast } from "sonner";
|
|
14
15
|
|
|
15
16
|
import { DesignSystemCard } from "@/components/design-system/DesignSystemCard";
|
|
16
17
|
import { DesignSystemSetup } from "@/components/design-system/DesignSystemSetup";
|
|
18
|
+
import {
|
|
19
|
+
AlertDialog,
|
|
20
|
+
AlertDialogAction,
|
|
21
|
+
AlertDialogCancel,
|
|
22
|
+
AlertDialogContent,
|
|
23
|
+
AlertDialogDescription,
|
|
24
|
+
AlertDialogFooter,
|
|
25
|
+
AlertDialogHeader,
|
|
26
|
+
AlertDialogTitle,
|
|
27
|
+
} from "@/components/ui/alert-dialog";
|
|
17
28
|
import { Button } from "@/components/ui/button";
|
|
18
29
|
import { useDesignSystems } from "@/hooks/use-design-systems";
|
|
19
30
|
|
|
@@ -25,6 +36,8 @@ export default function DesignSystems() {
|
|
|
25
36
|
const { designSystems, isLoading, error, refetch } = useDesignSystems();
|
|
26
37
|
const [showSetup, setShowSetup] = useState(false);
|
|
27
38
|
const [editingId, setEditingId] = useState<string | null>(null);
|
|
39
|
+
const [deleteId, setDeleteId] = useState<string | null>(null);
|
|
40
|
+
const deleteMutation = useActionMutation("delete-design-system");
|
|
28
41
|
|
|
29
42
|
const handleCardClick = (id: string) => {
|
|
30
43
|
setEditingId(id);
|
|
@@ -51,6 +64,21 @@ export default function DesignSystems() {
|
|
|
51
64
|
setEditingId(null);
|
|
52
65
|
};
|
|
53
66
|
|
|
67
|
+
const handleConfirmDelete = () => {
|
|
68
|
+
if (deleteId === null) return;
|
|
69
|
+
const id = deleteId;
|
|
70
|
+
setDeleteId(null);
|
|
71
|
+
deleteMutation.mutate({ id } as never, {
|
|
72
|
+
onSuccess: () => refetch(),
|
|
73
|
+
onError: (err: unknown) => {
|
|
74
|
+
void refetch();
|
|
75
|
+
toast.error(t("designSystems.deleteError"), {
|
|
76
|
+
description: err instanceof Error ? err.message : undefined,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
54
82
|
const parseDesignData = (dataStr: string): DesignSystemData | null => {
|
|
55
83
|
try {
|
|
56
84
|
const parsed = JSON.parse(dataStr) as DesignSystemData;
|
|
@@ -172,8 +200,10 @@ export default function DesignSystems() {
|
|
|
172
200
|
data={parsed}
|
|
173
201
|
isDefault={ds.isDefault}
|
|
174
202
|
visibility={ds.visibility}
|
|
203
|
+
canManage={ds.canManage}
|
|
175
204
|
onClick={() => handleCardClick(ds.id)}
|
|
176
205
|
onSetDefault={() => handleSetDefault(ds.id)}
|
|
206
|
+
onDelete={() => setDeleteId(ds.id)}
|
|
177
207
|
/>
|
|
178
208
|
);
|
|
179
209
|
})}
|
|
@@ -182,6 +212,31 @@ export default function DesignSystems() {
|
|
|
182
212
|
)}
|
|
183
213
|
</main>
|
|
184
214
|
|
|
215
|
+
<AlertDialog
|
|
216
|
+
open={deleteId !== null}
|
|
217
|
+
onOpenChange={(open) => !open && setDeleteId(null)}
|
|
218
|
+
>
|
|
219
|
+
<AlertDialogContent>
|
|
220
|
+
<AlertDialogHeader>
|
|
221
|
+
<AlertDialogTitle>
|
|
222
|
+
{t("designSystems.deleteDialogTitle")}
|
|
223
|
+
</AlertDialogTitle>
|
|
224
|
+
<AlertDialogDescription>
|
|
225
|
+
{t("designSystems.deleteDialogDescription")}
|
|
226
|
+
</AlertDialogDescription>
|
|
227
|
+
</AlertDialogHeader>
|
|
228
|
+
<AlertDialogFooter>
|
|
229
|
+
<AlertDialogCancel>{t("designSystems.cancel")}</AlertDialogCancel>
|
|
230
|
+
<AlertDialogAction
|
|
231
|
+
onClick={handleConfirmDelete}
|
|
232
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
233
|
+
>
|
|
234
|
+
{t("designSystems.delete")}
|
|
235
|
+
</AlertDialogAction>
|
|
236
|
+
</AlertDialogFooter>
|
|
237
|
+
</AlertDialogContent>
|
|
238
|
+
</AlertDialog>
|
|
239
|
+
|
|
185
240
|
{/* Setup/Edit Dialog */}
|
|
186
241
|
<DesignSystemSetup
|
|
187
242
|
open={showSetup}
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
states: {
|
|
66
67
|
clientId: number;
|
|
67
68
|
state: string;
|
|
68
69
|
}[];
|
|
69
|
-
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,10 +77,10 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
+
error?: undefined;
|
|
80
81
|
users: {
|
|
81
82
|
clientId: number;
|
|
82
83
|
lastSeen: number;
|
|
83
84
|
}[];
|
|
84
|
-
error?: undefined;
|
|
85
85
|
}>>;
|
|
86
86
|
//# sourceMappingURL=awareness.d.ts.map
|