@allbluecn/web-app 0.4.16 → 0.4.18
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/package.json +10 -10
- package/src/components/billing/checkout-redirect.ts +1 -1
- package/src/components/changelog/changelog-page.tsx +1 -1
- package/src/components/layout/sidebar-03/app-sidebar.tsx +2 -3
- package/src/components/layout/sidebar-03/nav-notifications.tsx +115 -35
- package/src/components/layout/sidebar-03/user-menu.tsx +5 -4
- package/src/components/notifications/notification-list.tsx +232 -0
- package/src/components/notifications/notification-preferences.tsx +156 -0
- package/src/components/settings/ai/create-custom-provider-dialog.tsx +145 -0
- package/src/components/settings/ai/provider-grid.tsx +46 -25
- package/src/components/settings/personal-team/team/team-client.tsx +118 -15
- package/src/components/settings/personal-team/team/team-invitations.tsx +887 -416
- package/src/components/settings/personal-team/team/team-member-list.tsx +122 -136
- package/src/components/story/display/__tests__/display-dropdown.test.tsx +30 -7
- package/src/components/story/display/display-dropdown.tsx +6 -7
- package/src/components/story/inline-editors/assignee-editor.tsx +72 -57
- package/src/components/story/story-list-view.tsx +13 -2
- package/src/hooks/__tests__/use-notifications.test.ts +41 -0
- package/src/hooks/use-notifications.ts +119 -0
- package/src/lib/app-version.ts +4 -1
- package/src/lib/auth/auth.config.ts +17 -1
- package/src/lib/changelog/sdk-versions-types.ts +1 -0
- package/src/lib/changelog/sdk-versions.ts +29 -19
- package/src/lib/collections/ai/index.ts +0 -1
- package/src/lib/collections/index.ts +0 -1
- package/src/plugins/.gen-manifest.json +13 -0
- package/src/plugins/modules.gen.ts +20 -0
- package/src/plugins/plugins.gen.ts +20 -0
- package/src/plugins/ui.gen.ts +10 -0
- package/src/routeTree.gen.ts +70 -0
- package/src/routes/_login/login/route.tsx +10 -1
- package/src/routes/_login/register/route.lazy.tsx +3 -2
- package/src/routes/_login/register/route.tsx +5 -0
- package/src/routes/_nav/inspiration/route.tsx +8 -0
- package/src/routes/_nav/notifications/route.lazy.tsx +23 -0
- package/src/routes/_nav/notifications/route.tsx +8 -0
- package/src/routes/_nav/prd/$id/route.tsx +9 -0
- package/src/routes/_nav/prd/index.tsx +9 -0
- package/src/routes/_nav/prd/route.tsx +8 -0
- package/src/routes/_nav/settings/-settings-tabs.tsx +2 -1
- package/src/routes/_nav/settings/design/route.lazy.tsx +1 -1
- package/src/routes/_nav/settings/notifications/route.lazy.tsx +37 -0
- package/src/routes/_nav/settings/notifications/route.tsx +20 -0
- package/src/routes/_nav/settings/privacy/route.lazy.tsx +2 -2
- package/src/routes/_nav/settings/route.lazy.tsx +1 -1
- package/src/routes/_nav/stories/$storyId/route.tsx +2 -3
- package/src/routes/_nav/tags/$id/route.tsx +8 -0
- package/src/routes/_nav/tags/index.tsx +8 -0
- package/src/routes/_nav/tags/route.tsx +8 -0
- package/src/routes/_nav/tags/settings/route.tsx +9 -0
- package/src/routes/api/notifications/stream.ts +88 -0
- package/src/routes/invite/$token/route.lazy.tsx +308 -44
- package/src/routes/invite/$token/route.tsx +16 -8
- package/src/routes/share/prd/$prdId/route.tsx +10 -0
- package/src/server/notifications/__tests__/notification-service.test.ts +114 -0
- package/src/server/notifications/__tests__/stream.test.ts +30 -0
- package/src/server/notifications/event-bus.ts +16 -0
- package/src/server/notifications/notification-service.ts +86 -0
- package/src/server/resources.ts +10 -2
- package/src/server/serverFns/__tests__/compliance.test.ts +6 -6
- package/src/server/serverFns/__tests__/notifications.test.ts +63 -0
- package/src/server/serverFns/__tests__/team-accept.test.ts +100 -12
- package/src/server/serverFns/__tests__/team-invitation.test.ts +130 -60
- package/src/server/serverFns/__tests__/team-join-request.test.ts +281 -0
- package/src/server/serverFns/__tests__/team-members.test.ts +39 -118
- package/src/{lib/collections/ai/ai-providers-collection.ts → server/serverFns/auth/setup.ts} +7 -12
- package/src/server/serverFns/billing.ts +23 -2
- package/src/server/serverFns/notifications.ts +155 -0
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +4 -0
- package/src/server/serverFns/story/story-comments.ts +42 -0
- package/src/server/serverFns/story/story-crud/labels.ts +45 -6
- package/src/server/serverFns/story/story-crud.ts +8 -2
- package/src/server/serverFns/story/story-groups.ts +26 -7
- package/src/server/serverFns/story/story-search.ts +4 -0
- package/src/server/serverFns/story-display-options.ts +15 -4
- package/src/server/serverFns/team.ts +573 -53
- package/src/start.ts +23 -1
- package/vite.shared.ts +13 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
3
|
+
import { getRequest } from '@tanstack/react-start/server'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { prisma } from '@allbluecn/database'
|
|
6
|
+
import { requireAuth } from '@/server/auth-helpers'
|
|
7
|
+
import {
|
|
8
|
+
NOTIFICATION_CATEGORIES,
|
|
9
|
+
type NotificationCategory,
|
|
10
|
+
type CategoryPreferences,
|
|
11
|
+
type NotificationItem,
|
|
12
|
+
type NotificationListResult,
|
|
13
|
+
} from '@allbluecn/shared'
|
|
14
|
+
import {
|
|
15
|
+
getOrCreatePreferences,
|
|
16
|
+
DEFAULT_CATEGORIES,
|
|
17
|
+
} from '@/server/notifications/notification-service'
|
|
18
|
+
|
|
19
|
+
const PAGE_SIZE = 20
|
|
20
|
+
|
|
21
|
+
const listSchema = z.object({
|
|
22
|
+
category: z.enum(NOTIFICATION_CATEGORIES).optional(),
|
|
23
|
+
cursor: z.string().optional(),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const listNotificationsFn = createServerFn({ method: 'GET' })
|
|
27
|
+
.validator(listSchema)
|
|
28
|
+
.handler(async ({ data }): Promise<NotificationListResult> => {
|
|
29
|
+
const request = getRequest()
|
|
30
|
+
const session = await requireAuth(request)
|
|
31
|
+
const userId = session.user!.id!
|
|
32
|
+
|
|
33
|
+
const items = await prisma.notification.findMany({
|
|
34
|
+
where: {
|
|
35
|
+
userId,
|
|
36
|
+
...(data.category ? { category: data.category } : {}),
|
|
37
|
+
},
|
|
38
|
+
orderBy: { createdAt: 'desc' },
|
|
39
|
+
take: PAGE_SIZE + 1,
|
|
40
|
+
...(data.cursor ? { cursor: { id: data.cursor }, skip: 1 } : {}),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const hasMore = items.length > PAGE_SIZE
|
|
44
|
+
const page = (hasMore ? items.slice(0, PAGE_SIZE) : items).map(
|
|
45
|
+
(n): NotificationItem => ({
|
|
46
|
+
id: n.id,
|
|
47
|
+
category: n.category as NotificationCategory,
|
|
48
|
+
type: n.type,
|
|
49
|
+
title: n.title,
|
|
50
|
+
body: n.body,
|
|
51
|
+
link: n.link,
|
|
52
|
+
readAt: n.readAt ? n.readAt.toISOString() : null,
|
|
53
|
+
createdAt: n.createdAt.toISOString(),
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
return { items: page, total: page.length, hasMore }
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const markReadSchema = z.object({ id: z.string().min(1) })
|
|
60
|
+
|
|
61
|
+
export const markNotificationReadFn = createServerFn({ method: 'POST' })
|
|
62
|
+
.validator(markReadSchema)
|
|
63
|
+
.handler(async ({ data }) => {
|
|
64
|
+
const request = getRequest()
|
|
65
|
+
const session = await requireAuth(request)
|
|
66
|
+
const userId = session.user!.id!
|
|
67
|
+
await prisma.notification.updateMany({
|
|
68
|
+
where: { id: data.id, userId, readAt: null },
|
|
69
|
+
data: { readAt: new Date() },
|
|
70
|
+
})
|
|
71
|
+
return { success: true }
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
export const markAllNotificationsReadFn = createServerFn({ method: 'POST' })
|
|
75
|
+
.validator(z.object({}).optional())
|
|
76
|
+
.handler(async () => {
|
|
77
|
+
const request = getRequest()
|
|
78
|
+
const session = await requireAuth(request)
|
|
79
|
+
const userId = session.user!.id!
|
|
80
|
+
const result = await prisma.notification.updateMany({
|
|
81
|
+
where: { userId, readAt: null },
|
|
82
|
+
data: { readAt: new Date() },
|
|
83
|
+
})
|
|
84
|
+
return { success: true, count: result.count }
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const deleteSchema = z.object({ id: z.string().min(1) })
|
|
88
|
+
|
|
89
|
+
export const deleteNotificationFn = createServerFn({ method: 'POST' })
|
|
90
|
+
.validator(deleteSchema)
|
|
91
|
+
.handler(async ({ data }) => {
|
|
92
|
+
const request = getRequest()
|
|
93
|
+
const session = await requireAuth(request)
|
|
94
|
+
const userId = session.user!.id!
|
|
95
|
+
await prisma.notification.deleteMany({ where: { id: data.id, userId } })
|
|
96
|
+
return { success: true }
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
export const getUnreadCountFn = createServerFn({ method: 'GET' })
|
|
100
|
+
.validator(z.object({}).optional())
|
|
101
|
+
.handler(async () => {
|
|
102
|
+
const request = getRequest()
|
|
103
|
+
const session = await requireAuth(request)
|
|
104
|
+
const userId = session.user!.id!
|
|
105
|
+
const count = await prisma.notification.count({
|
|
106
|
+
where: { userId, readAt: null },
|
|
107
|
+
})
|
|
108
|
+
return { count }
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
export const getNotificationPreferencesFn = createServerFn({ method: 'GET' })
|
|
112
|
+
.validator(z.object({}).optional())
|
|
113
|
+
.handler(async () => {
|
|
114
|
+
const request = getRequest()
|
|
115
|
+
const session = await requireAuth(request)
|
|
116
|
+
return getOrCreatePreferences(session.user!.id!)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const preferencesSchema = z.object({
|
|
120
|
+
categories: z.record(z.string(), z.boolean()),
|
|
121
|
+
dndEnabled: z.boolean(),
|
|
122
|
+
dndStart: z.string().regex(/^\d{2}:\d{2}$/),
|
|
123
|
+
dndEnd: z.string().regex(/^\d{2}:\d{2}$/),
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
export const updateNotificationPreferencesFn = createServerFn({
|
|
127
|
+
method: 'POST',
|
|
128
|
+
})
|
|
129
|
+
.validator(preferencesSchema)
|
|
130
|
+
.handler(async ({ data }) => {
|
|
131
|
+
const request = getRequest()
|
|
132
|
+
const session = await requireAuth(request)
|
|
133
|
+
const userId = session.user!.id!
|
|
134
|
+
const categories = { ...DEFAULT_CATEGORIES } as CategoryPreferences
|
|
135
|
+
for (const c of NOTIFICATION_CATEGORIES) {
|
|
136
|
+
if (typeof data.categories[c] === 'boolean') categories[c] = data.categories[c]
|
|
137
|
+
}
|
|
138
|
+
await prisma.notificationPreference.upsert({
|
|
139
|
+
where: { userId },
|
|
140
|
+
create: {
|
|
141
|
+
userId,
|
|
142
|
+
categories,
|
|
143
|
+
dndEnabled: data.dndEnabled,
|
|
144
|
+
dndStart: data.dndStart,
|
|
145
|
+
dndEnd: data.dndEnd,
|
|
146
|
+
},
|
|
147
|
+
update: {
|
|
148
|
+
categories,
|
|
149
|
+
dndEnabled: data.dndEnabled,
|
|
150
|
+
dndStart: data.dndStart,
|
|
151
|
+
dndEnd: data.dndEnd,
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
return { success: true }
|
|
155
|
+
})
|
|
@@ -50,12 +50,14 @@ const mocks = vi.hoisted(() => ({
|
|
|
50
50
|
groupBy: vi.fn(),
|
|
51
51
|
storyLabelGroupBy: vi.fn(),
|
|
52
52
|
currentSession: vi.fn(),
|
|
53
|
+
projectFindFirst: vi.fn(),
|
|
53
54
|
}));
|
|
54
55
|
|
|
55
56
|
vi.mock("@allbluecn/database", () => ({
|
|
56
57
|
prisma: {
|
|
57
58
|
story: { findMany: mocks.findMany, count: mocks.count, groupBy: mocks.groupBy },
|
|
58
59
|
storyLabel: { groupBy: mocks.storyLabelGroupBy },
|
|
60
|
+
project: { findFirst: mocks.projectFindFirst },
|
|
59
61
|
},
|
|
60
62
|
Prisma: {},
|
|
61
63
|
}));
|
|
@@ -70,6 +72,7 @@ describe("listStoriesInGroupFn", () => {
|
|
|
70
72
|
beforeEach(() => {
|
|
71
73
|
vi.clearAllMocks();
|
|
72
74
|
mocks.currentSession.mockResolvedValue({ user: { id: "u1" } });
|
|
75
|
+
mocks.projectFindFirst.mockResolvedValue({ id: "proj-independent-1" });
|
|
73
76
|
});
|
|
74
77
|
|
|
75
78
|
it("构建含 group 过滤的 where 并返回 total/hasMore", async () => {
|
|
@@ -170,6 +173,7 @@ describe("listStoryGroupsFn", () => {
|
|
|
170
173
|
beforeEach(() => {
|
|
171
174
|
vi.clearAllMocks();
|
|
172
175
|
mocks.currentSession.mockResolvedValue({ user: { id: "u1" } });
|
|
176
|
+
mocks.projectFindFirst.mockResolvedValue({ id: "proj-independent-1" });
|
|
173
177
|
});
|
|
174
178
|
|
|
175
179
|
it("按 groupBy 聚合返回组 key 列表", async () => {
|
|
@@ -22,6 +22,7 @@ import { prisma } from "@allbluecn/database";
|
|
|
22
22
|
import { requireAuth } from "@/server/auth-helpers";
|
|
23
23
|
import type { CommentWithReplies, ReactionGroup } from "@/components/story/types";
|
|
24
24
|
import { USER_BRIEF_SELECT, castUserBrief, aggregateReactions } from "./types";
|
|
25
|
+
import { createNotification } from '@/server/notifications/notification-service'
|
|
25
26
|
|
|
26
27
|
export const listCommentsFn = createServerFn({ method: "GET" })
|
|
27
28
|
.validator(z.object({ storyId: z.string() }))
|
|
@@ -101,6 +102,47 @@ export const createCommentFn = createServerFn({ method: "POST" })
|
|
|
101
102
|
},
|
|
102
103
|
});
|
|
103
104
|
|
|
105
|
+
const commenter = await prisma.user.findUnique({
|
|
106
|
+
where: { id: userId },
|
|
107
|
+
select: { username: true },
|
|
108
|
+
})
|
|
109
|
+
const story = await prisma.story.findUnique({
|
|
110
|
+
where: { id: data.storyId },
|
|
111
|
+
select: { createdById: true },
|
|
112
|
+
})
|
|
113
|
+
if (story?.createdById && story.createdById !== userId) {
|
|
114
|
+
void createNotification({
|
|
115
|
+
userId: story.createdById,
|
|
116
|
+
category: 'collaboration',
|
|
117
|
+
type: 'collaboration.comment',
|
|
118
|
+
title: `${commenter?.username ?? '有人'} 评论了你的 Story`,
|
|
119
|
+
link: `/stories/${data.storyId}`,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const mentionMatches = data.content.match(/@([a-zA-Z0-9_\u4e00-\u9fa5]+)/g) ?? []
|
|
124
|
+
if (mentionMatches.length > 0) {
|
|
125
|
+
const mentionedNames = [...new Set(mentionMatches.map((m) => m.slice(1)))].filter(
|
|
126
|
+
(name) => name.toLowerCase() !== commenter?.username?.toLowerCase(),
|
|
127
|
+
)
|
|
128
|
+
if (mentionedNames.length > 0) {
|
|
129
|
+
const mentionedUsers = await prisma.user.findMany({
|
|
130
|
+
where: { username: { in: mentionedNames } },
|
|
131
|
+
select: { id: true },
|
|
132
|
+
})
|
|
133
|
+
for (const u of mentionedUsers) {
|
|
134
|
+
if (u.id === userId) continue
|
|
135
|
+
void createNotification({
|
|
136
|
+
userId: u.id,
|
|
137
|
+
category: 'collaboration',
|
|
138
|
+
type: 'collaboration.mention',
|
|
139
|
+
title: `${commenter?.username ?? '有人'} 在评论中提到了你`,
|
|
140
|
+
link: `/stories/${data.storyId}`,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
104
146
|
await prisma.storyActivity.create({
|
|
105
147
|
data: {
|
|
106
148
|
storyId: data.storyId,
|
|
@@ -27,12 +27,52 @@ const labelCreateSchema = z.object({
|
|
|
27
27
|
color: z.string().regex(/^#[0-9a-fA-F]{6}$/).default("#6b7280"),
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
export const listUsersBriefFn = createServerFn({ method: "GET" })
|
|
31
|
-
|
|
30
|
+
export const listUsersBriefFn = createServerFn({ method: "GET" })
|
|
31
|
+
.validator((data?: { projectId?: string }) => data)
|
|
32
|
+
.handler(async ({ data }) => {
|
|
32
33
|
const request = getRequest();
|
|
33
|
-
await requireAuth(request);
|
|
34
|
+
const session = await requireAuth(request);
|
|
35
|
+
const currentUserId = session.user!.id!;
|
|
36
|
+
|
|
37
|
+
if (data?.projectId) {
|
|
38
|
+
// 项目级别:只返回项目成员
|
|
39
|
+
const project = await prisma.project.findUnique({
|
|
40
|
+
where: { id: data.projectId },
|
|
41
|
+
select: { ownerId: true, workspaceId: true },
|
|
42
|
+
});
|
|
43
|
+
if (!project) return [];
|
|
44
|
+
|
|
45
|
+
const members = await prisma.projectMember.findMany({
|
|
46
|
+
where: { projectId: data.projectId },
|
|
47
|
+
select: { userId: true },
|
|
48
|
+
});
|
|
49
|
+
const memberIds = new Set(members.map((m) => m.userId));
|
|
50
|
+
memberIds.add(project.ownerId);
|
|
51
|
+
|
|
52
|
+
const users = await prisma.user.findMany({
|
|
53
|
+
where: { id: { in: Array.from(memberIds) }, status: "ACTIVE" },
|
|
54
|
+
orderBy: { username: "asc" },
|
|
55
|
+
select: USER_BRIEF_SELECT,
|
|
56
|
+
});
|
|
57
|
+
return users.map((u) => ({
|
|
58
|
+
...u,
|
|
59
|
+
avatarConfig: (u.avatarConfig as import("@allbluecn/shared").AvatarConfig | null) ?? null,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 团队级别:只返回当前用户团队(team_memberships.ownerId = 当前用户)的成员
|
|
64
|
+
const memberships = await prisma.teamMembership.findMany({
|
|
65
|
+
where: { ownerId: currentUserId },
|
|
66
|
+
select: { memberId: true },
|
|
67
|
+
});
|
|
68
|
+
if (memberships.length === 0) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const memberIds = new Set(memberships.map((m) => m.memberId));
|
|
72
|
+
memberIds.add(currentUserId); // 包含自己
|
|
73
|
+
|
|
34
74
|
const users = await prisma.user.findMany({
|
|
35
|
-
where: { status: "ACTIVE" },
|
|
75
|
+
where: { id: { in: Array.from(memberIds) }, status: "ACTIVE" },
|
|
36
76
|
orderBy: { username: "asc" },
|
|
37
77
|
select: USER_BRIEF_SELECT,
|
|
38
78
|
});
|
|
@@ -40,8 +80,7 @@ export const listUsersBriefFn = createServerFn({ method: "GET" }).handler(
|
|
|
40
80
|
...u,
|
|
41
81
|
avatarConfig: (u.avatarConfig as import("@allbluecn/shared").AvatarConfig | null) ?? null,
|
|
42
82
|
}));
|
|
43
|
-
}
|
|
44
|
-
);
|
|
83
|
+
});
|
|
45
84
|
|
|
46
85
|
export const listLabelsFn = createServerFn({ method: "GET" }).handler(
|
|
47
86
|
async () => {
|
|
@@ -56,9 +56,15 @@ export const listStoriesFn = createServerFn({ method: "GET" })
|
|
|
56
56
|
.validator(listSchema)
|
|
57
57
|
.handler(async ({ data }) => {
|
|
58
58
|
const request = getRequest();
|
|
59
|
-
await requireAuth(request);
|
|
59
|
+
const session = await requireAuth(request);
|
|
60
|
+
const userId = session.user!.id!;
|
|
61
|
+
|
|
62
|
+
let projectId = data.projectId;
|
|
63
|
+
if (!projectId) {
|
|
64
|
+
projectId = await ensureBuiltinIndependentProject(userId);
|
|
65
|
+
}
|
|
60
66
|
|
|
61
|
-
const where = buildListWhere(data);
|
|
67
|
+
const where = buildListWhere({ ...data, projectId });
|
|
62
68
|
|
|
63
69
|
const [items, total] = await Promise.all([
|
|
64
70
|
prisma.story.findMany({
|
|
@@ -20,6 +20,7 @@ import { getRequest } from "@tanstack/react-start/server";
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { prisma, Prisma } from "@allbluecn/database";
|
|
22
22
|
import { requireAuth } from "@/server/auth-helpers";
|
|
23
|
+
import { ensureBuiltinIndependentProject } from "@/server/builtin-project";
|
|
23
24
|
import { GROUP_KEYS, ORDER_KEYS, ORDER_DIRECTIONS, type GroupKey, type OrderKey } from "@/components/story/display/types";
|
|
24
25
|
import { STORY_STATES, STORY_PRIORITIES } from "@/components/story/constants";
|
|
25
26
|
import { STORY_LIST_SELECT_LIGHT, castStoryUsersLight, type StoryListRowLight } from "./types";
|
|
@@ -57,9 +58,15 @@ export const listStoriesInGroupFn = createServerFn({ method: "GET" })
|
|
|
57
58
|
.validator(schema)
|
|
58
59
|
.handler(async ({ data }) => {
|
|
59
60
|
const request = getRequest();
|
|
60
|
-
await requireAuth(request);
|
|
61
|
+
const session = await requireAuth(request);
|
|
62
|
+
const userId = session.user!.id!;
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
let projectId = data.projectId;
|
|
65
|
+
if (!projectId) {
|
|
66
|
+
projectId = await ensureBuiltinIndependentProject(userId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const base = buildListWhere({ ...data, projectId });
|
|
63
70
|
const baseAnd = Array.isArray(base.AND) ? base.AND : base.AND ? [base.AND] : [];
|
|
64
71
|
const extras: Prisma.StoryWhereInput[] = [];
|
|
65
72
|
const groupWhere = buildGroupFilter(data.groupBy, data.groupKey);
|
|
@@ -114,9 +121,15 @@ export const listAllGroupsItemsFn = createServerFn({ method: "GET" })
|
|
|
114
121
|
.validator(allGroupsItemsSchema)
|
|
115
122
|
.handler(async ({ data }) => {
|
|
116
123
|
const request = getRequest();
|
|
117
|
-
await requireAuth(request);
|
|
124
|
+
const session = await requireAuth(request);
|
|
125
|
+
const userId = session.user!.id!;
|
|
126
|
+
|
|
127
|
+
let projectId = data.projectId;
|
|
128
|
+
if (!projectId) {
|
|
129
|
+
projectId = await ensureBuiltinIndependentProject(userId);
|
|
130
|
+
}
|
|
118
131
|
|
|
119
|
-
const base = buildListWhere(data);
|
|
132
|
+
const base = buildListWhere({ ...data, projectId });
|
|
120
133
|
const field = ORDER_FIELD[data.orderBy];
|
|
121
134
|
const dir: "asc" | "desc" = data.orderDir === "asc" ? "asc" : "desc";
|
|
122
135
|
const orderBy: Prisma.StoryOrderByWithRelationInput = field
|
|
@@ -207,10 +220,16 @@ export const listStoryGroupsFn = createServerFn({ method: "GET" })
|
|
|
207
220
|
.validator(listGroupsSchema)
|
|
208
221
|
.handler(async ({ data }) => {
|
|
209
222
|
const request = getRequest();
|
|
210
|
-
await requireAuth(request);
|
|
211
|
-
|
|
223
|
+
const session = await requireAuth(request);
|
|
224
|
+
const userId = session.user!.id!;
|
|
225
|
+
|
|
226
|
+
let projectId = data.projectId;
|
|
227
|
+
if (!projectId) {
|
|
228
|
+
projectId = await ensureBuiltinIndependentProject(userId);
|
|
229
|
+
}
|
|
212
230
|
|
|
213
|
-
const where = buildListWhere(data);
|
|
231
|
+
const where = buildListWhere({ ...data, projectId });
|
|
232
|
+
if (data.groupBy === "none") return { groups: [] };
|
|
214
233
|
if (data.parentGroupBy !== "none" && data.parentGroupKey) {
|
|
215
234
|
const parentWhere = buildGroupFilter(data.parentGroupBy, data.parentGroupKey);
|
|
216
235
|
if (Object.keys(parentWhere).length) {
|
|
@@ -20,16 +20,20 @@ import { getRequest } from "@tanstack/react-start/server";
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { prisma } from "@allbluecn/database";
|
|
22
22
|
import { requireAuth } from "@/server/auth-helpers";
|
|
23
|
+
import { ensureBuiltinIndependentProject } from "@/server/builtin-project";
|
|
23
24
|
|
|
24
25
|
export const searchStoriesFn = createServerFn({ method: "GET" })
|
|
25
26
|
.validator(z.object({ query: z.string(), excludeIds: z.array(z.string()).optional() }))
|
|
26
27
|
.handler(async ({ data }) => {
|
|
27
28
|
const request = getRequest();
|
|
28
29
|
const session = await requireAuth(request);
|
|
30
|
+
const userId = session.user!.id!;
|
|
31
|
+
const projectId = await ensureBuiltinIndependentProject(userId);
|
|
29
32
|
|
|
30
33
|
const stories = await prisma.story.findMany({
|
|
31
34
|
where: {
|
|
32
35
|
id: data.excludeIds?.length ? { notIn: data.excludeIds } : undefined,
|
|
36
|
+
projectId,
|
|
33
37
|
OR: [
|
|
34
38
|
{ name: { contains: data.query, mode: "insensitive" } },
|
|
35
39
|
{ identifier: { contains: data.query, mode: "insensitive" } },
|
|
@@ -22,6 +22,7 @@ import { z } from "zod"
|
|
|
22
22
|
import { prisma, type Prisma } from "@allbluecn/database"
|
|
23
23
|
import { requireAuth } from "@/server/auth-helpers"
|
|
24
24
|
import { requireFeature } from "@/server/entitlements"
|
|
25
|
+
import { isProEdition } from "@/server/pro-services"
|
|
25
26
|
import {
|
|
26
27
|
DEFAULT_DISPLAY_OPTIONS,
|
|
27
28
|
GROUP_KEYS,
|
|
@@ -123,11 +124,16 @@ export const getStoryDisplayOptionsFn = createServerFn({ method: "GET" }).handle
|
|
|
123
124
|
const options = parseStoredOptions(user?.storyDisplayOptions)
|
|
124
125
|
|
|
125
126
|
// 订阅过期/降级:已持久化的 500/1000 无权限时降级为 200
|
|
127
|
+
// OSS 环境直接 cap 到 200;Pro 环境检查 feature
|
|
126
128
|
if (options.pageSize > 200) {
|
|
127
|
-
|
|
128
|
-
await requireFeature(request, "largePageSize")
|
|
129
|
-
} catch {
|
|
129
|
+
if (!isProEdition()) {
|
|
130
130
|
options.pageSize = 200
|
|
131
|
+
} else {
|
|
132
|
+
try {
|
|
133
|
+
await requireFeature(request, "largePageSize")
|
|
134
|
+
} catch {
|
|
135
|
+
options.pageSize = 200
|
|
136
|
+
}
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
139
|
return options
|
|
@@ -143,8 +149,13 @@ export const updateStoryDisplayOptionsFn = createServerFn({ method: "POST" })
|
|
|
143
149
|
const userId = session.user!.id!
|
|
144
150
|
|
|
145
151
|
// 服务端强制校验:pageSize > 200(500/1000)为 Pro 专属,前端锁定只是 UX
|
|
152
|
+
// OSS 环境直接 cap 到 200;Pro 环境走 feature gate
|
|
146
153
|
if (data.pageSize > 200) {
|
|
147
|
-
|
|
154
|
+
if (!isProEdition()) {
|
|
155
|
+
data.pageSize = 200
|
|
156
|
+
} else {
|
|
157
|
+
await requireFeature(request, "largePageSize")
|
|
158
|
+
}
|
|
148
159
|
}
|
|
149
160
|
|
|
150
161
|
const options: DisplayOptions = data
|