@allbluecn/web-app 0.4.15 → 0.4.17
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 +7 -7
- package/src/components/changelog/changelog-page.tsx +87 -38
- package/src/components/layout/sidebar-03/user-menu.tsx +4 -3
- 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/lib/changelog/sdk-versions.ts +19 -19
- package/src/routes/_nav/stories/$storyId/route.tsx +2 -3
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +4 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allbluecn/web-app",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.17",
|
|
4
4
|
"license": "AGPL-3.0-or-later",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -135,14 +135,14 @@
|
|
|
135
135
|
"three": "^0.180.0",
|
|
136
136
|
"tw-animate-css": "^1.4.0",
|
|
137
137
|
"zod": "^4.4.3",
|
|
138
|
-
"@allbluecn/ai-gateway": "0.4.
|
|
139
|
-
"@allbluecn/database": "0.4.11",
|
|
138
|
+
"@allbluecn/ai-gateway": "0.4.12",
|
|
140
139
|
"@allbluecn/kernel": "0.3.0",
|
|
141
|
-
"@allbluecn/
|
|
140
|
+
"@allbluecn/database": "0.4.12",
|
|
141
|
+
"@allbluecn/plugins-core": "0.4.12",
|
|
142
142
|
"@allbluecn/prototype": "0.1.1",
|
|
143
|
-
"@allbluecn/
|
|
144
|
-
"@allbluecn/
|
|
145
|
-
"@allbluecn/
|
|
143
|
+
"@allbluecn/ui": "0.4.12",
|
|
144
|
+
"@allbluecn/shared": "0.4.12",
|
|
145
|
+
"@allbluecn/tiptap": "0.4.12"
|
|
146
146
|
},
|
|
147
147
|
"devDependencies": {
|
|
148
148
|
"@babel/parser": "^8.0.4",
|
|
@@ -19,12 +19,7 @@ import { cn } from '@/lib/utils'
|
|
|
19
19
|
import { ScrollText, AlertTriangle, ArrowLeft, ChevronDown } from 'lucide-react'
|
|
20
20
|
import ReactMarkdown from 'react-markdown'
|
|
21
21
|
import { useTranslation } from 'react-i18next'
|
|
22
|
-
import { useMemo, useState } from 'react'
|
|
23
|
-
import {
|
|
24
|
-
Collapsible,
|
|
25
|
-
CollapsibleContent,
|
|
26
|
-
CollapsibleTrigger,
|
|
27
|
-
} from '@/components/ui/collapsible'
|
|
22
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
28
23
|
import { Route } from '@/routes/_nav/changelog/route'
|
|
29
24
|
import type { DeveloperLogEntry } from '@allbluecn/shared'
|
|
30
25
|
import { SDK_GROUPS } from '@/lib/changelog/sdk-versions'
|
|
@@ -36,22 +31,68 @@ const REQUIRES_ACTION_TAGS = new Set(['breaking'])
|
|
|
36
31
|
|
|
37
32
|
function ChangelogSidebar() {
|
|
38
33
|
const { t } = useTranslation('common')
|
|
39
|
-
const [
|
|
40
|
-
|
|
41
|
-
)
|
|
34
|
+
const [activeGroup, setActiveGroup] = useState<string | null>(null)
|
|
35
|
+
const [isOverflowing, setIsOverflowing] = useState(false)
|
|
36
|
+
const outerRef = useRef<HTMLDivElement>(null)
|
|
37
|
+
const isOverflowingRef = useRef(false)
|
|
38
|
+
const expandedHeightRef = useRef(0)
|
|
39
|
+
|
|
40
|
+
const checkOverflow = useCallback(() => {
|
|
41
|
+
if (!outerRef.current) return
|
|
42
|
+
|
|
43
|
+
let isOverflow: boolean
|
|
44
|
+
if (!isOverflowingRef.current) {
|
|
45
|
+
// Expanded: trust DOM scrollHeight
|
|
46
|
+
isOverflow = outerRef.current.scrollHeight > outerRef.current.clientHeight
|
|
47
|
+
if (isOverflow) {
|
|
48
|
+
expandedHeightRef.current = outerRef.current.scrollHeight
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
// Collapsed: compare cached expanded height against container
|
|
52
|
+
isOverflow = expandedHeightRef.current > outerRef.current.clientHeight
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isOverflow !== isOverflowingRef.current) {
|
|
56
|
+
isOverflowingRef.current = isOverflow
|
|
57
|
+
setIsOverflowing(isOverflow)
|
|
58
|
+
if (isOverflow) setActiveGroup(null)
|
|
59
|
+
}
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
checkOverflow()
|
|
64
|
+
const ro = new ResizeObserver(checkOverflow)
|
|
65
|
+
if (outerRef.current) ro.observe(outerRef.current)
|
|
66
|
+
window.addEventListener('resize', checkOverflow)
|
|
67
|
+
return () => {
|
|
68
|
+
ro.disconnect()
|
|
69
|
+
window.removeEventListener('resize', checkOverflow)
|
|
70
|
+
}
|
|
71
|
+
}, [checkOverflow])
|
|
72
|
+
|
|
73
|
+
const handleGroupToggle = (groupId: string) => {
|
|
74
|
+
if (!isOverflowing) return
|
|
75
|
+
setActiveGroup((prev) => (prev === groupId ? null : groupId))
|
|
76
|
+
}
|
|
42
77
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
next.has(id) ? next.delete(id) : next.add(id)
|
|
47
|
-
return next
|
|
48
|
-
})
|
|
78
|
+
const isGroupOpen = (groupId: string) => {
|
|
79
|
+
if (!isOverflowing) return true
|
|
80
|
+
return activeGroup === groupId
|
|
49
81
|
}
|
|
50
82
|
|
|
83
|
+
const outerClasses = useMemo(
|
|
84
|
+
() =>
|
|
85
|
+
cn(
|
|
86
|
+
'flex-1 flex flex-col gap-6 justify-center min-h-0',
|
|
87
|
+
isOverflowing && 'overflow-y-auto'
|
|
88
|
+
),
|
|
89
|
+
[isOverflowing]
|
|
90
|
+
)
|
|
91
|
+
|
|
51
92
|
return (
|
|
52
|
-
<aside className="shrink-0 w-1/2 max-w-130 h-full flex flex-col py-6 pr-12 pl-10 border-r border-border bg-muted/50">
|
|
53
|
-
<div className=
|
|
54
|
-
<div>
|
|
93
|
+
<aside className="shrink-0 w-1/2 max-w-130 h-full flex flex-col py-6 pr-12 pl-10 border-r border-border bg-muted/50 overflow-hidden">
|
|
94
|
+
<div ref={outerRef} className={outerClasses}>
|
|
95
|
+
<div className="shrink-0">
|
|
55
96
|
<h1 className="text-4xl font-medium tracking-tight">
|
|
56
97
|
{t('changelog.title')}
|
|
57
98
|
</h1>
|
|
@@ -60,24 +101,25 @@ function ChangelogSidebar() {
|
|
|
60
101
|
</p>
|
|
61
102
|
</div>
|
|
62
103
|
|
|
63
|
-
<div className="flex flex-col gap-3
|
|
64
|
-
<h2 className="text-sm font-semibold tracking-tight
|
|
104
|
+
<div className="flex flex-col gap-3">
|
|
105
|
+
<h2 className="shrink-0 text-sm font-semibold tracking-tight">
|
|
65
106
|
{t('changelog.sdkVersions')}
|
|
66
107
|
</h2>
|
|
67
|
-
<div className="flex flex-col gap-3
|
|
108
|
+
<div className="flex flex-col gap-3">
|
|
68
109
|
{SDK_GROUPS.map((group) => (
|
|
69
110
|
<SdkGroupSection
|
|
70
111
|
key={group.id}
|
|
71
112
|
group={group}
|
|
72
113
|
t={t}
|
|
73
|
-
open={
|
|
74
|
-
onToggle={() =>
|
|
114
|
+
open={isGroupOpen(group.id)}
|
|
115
|
+
onToggle={() => handleGroupToggle(group.id)}
|
|
116
|
+
isAccordion={isOverflowing}
|
|
75
117
|
/>
|
|
76
118
|
))}
|
|
77
119
|
</div>
|
|
78
120
|
</div>
|
|
79
121
|
|
|
80
|
-
<div>
|
|
122
|
+
<div className="shrink-0">
|
|
81
123
|
<h3 className="text-sm font-semibold tracking-tight">
|
|
82
124
|
{t('changelog.subscribe.title')}
|
|
83
125
|
</h3>
|
|
@@ -278,21 +320,23 @@ function SdkGroupSection({
|
|
|
278
320
|
t,
|
|
279
321
|
open,
|
|
280
322
|
onToggle,
|
|
323
|
+
isAccordion,
|
|
281
324
|
}: {
|
|
282
325
|
group: (typeof SDK_GROUPS)[0]
|
|
283
326
|
t: (key: string) => string
|
|
284
327
|
open: boolean
|
|
285
328
|
onToggle: () => void
|
|
329
|
+
isAccordion: boolean
|
|
286
330
|
}) {
|
|
287
331
|
const isTanstack = group.id === 'tanstack'
|
|
288
332
|
|
|
289
333
|
return (
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
334
|
+
<div className="flex flex-col">
|
|
335
|
+
<button
|
|
336
|
+
type="button"
|
|
337
|
+
onClick={onToggle}
|
|
338
|
+
className="flex items-center gap-1.5 w-full py-1 group"
|
|
339
|
+
>
|
|
296
340
|
<span className="flex-1 min-w-0 flex items-center gap-1.5">
|
|
297
341
|
{isTanstack ? (
|
|
298
342
|
<span className="shrink-0">
|
|
@@ -314,12 +358,17 @@ function SdkGroupSection({
|
|
|
314
358
|
{t(`changelog.sdkGroups.${group.label}`)}
|
|
315
359
|
</span>
|
|
316
360
|
</span>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
361
|
+
{isAccordion && (
|
|
362
|
+
<ChevronDown
|
|
363
|
+
className={cn(
|
|
364
|
+
'size-3.5 shrink-0 text-muted-foreground transition-transform group-hover:text-foreground',
|
|
365
|
+
open && 'rotate-180'
|
|
366
|
+
)}
|
|
367
|
+
/>
|
|
368
|
+
)}
|
|
369
|
+
</button>
|
|
370
|
+
{open && (
|
|
371
|
+
<ul className="mt-1 flex flex-col">
|
|
323
372
|
{group.items.map((sdk) => (
|
|
324
373
|
<li key={sdk.name}>
|
|
325
374
|
<a
|
|
@@ -338,8 +387,8 @@ function SdkGroupSection({
|
|
|
338
387
|
</li>
|
|
339
388
|
))}
|
|
340
389
|
</ul>
|
|
341
|
-
|
|
342
|
-
</
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
343
392
|
)
|
|
344
393
|
}
|
|
345
394
|
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
LogIn,
|
|
24
24
|
LogOut,
|
|
25
25
|
Settings,
|
|
26
|
+
Users,
|
|
26
27
|
} from "lucide-react";
|
|
27
28
|
import { logoutRemoteFn } from '@/server/serverFns/auth/session'
|
|
28
29
|
import { toast } from 'sonner'
|
|
@@ -160,9 +161,9 @@ export function UserMenu() {
|
|
|
160
161
|
<Settings />
|
|
161
162
|
{t('sidebar.userMenu.settings')}
|
|
162
163
|
</DropdownMenuItem>
|
|
163
|
-
<DropdownMenuItem onClick={async () => { await router.navigate({ to: "/settings/
|
|
164
|
-
<
|
|
165
|
-
{t('sidebar.userMenu.
|
|
164
|
+
<DropdownMenuItem onClick={async () => { await router.navigate({ to: "/settings/team" }) }}>
|
|
165
|
+
<Users />
|
|
166
|
+
{t('sidebar.userMenu.teamManagement')}
|
|
166
167
|
</DropdownMenuItem>
|
|
167
168
|
|
|
168
169
|
|
|
@@ -29,10 +29,6 @@ vi.mock("@/components/billing/use-feature", () => ({
|
|
|
29
29
|
useFeature: vi.fn(),
|
|
30
30
|
}));
|
|
31
31
|
|
|
32
|
-
vi.mock("@/components/billing/upgrade-prompt", () => ({
|
|
33
|
-
UpgradePrompt: () => <div data-testid="upgrade-prompt">升级提示</div>,
|
|
34
|
-
}));
|
|
35
|
-
|
|
36
32
|
import { useFeature } from "@/components/billing/use-feature";
|
|
37
33
|
|
|
38
34
|
const mockUseFeature = useFeature as ReturnType<typeof vi.fn>;
|
|
@@ -214,7 +210,28 @@ describe("DisplayDropdown", () => {
|
|
|
214
210
|
expect(patch.showEmptyGroups).toBe(true);
|
|
215
211
|
});
|
|
216
212
|
|
|
217
|
-
it("
|
|
213
|
+
it("OSS 环境——只显示 100/200,不显示 500/1000", () => {
|
|
214
|
+
mockUseFeature.mockReturnValue({ enabled: false, loading: false });
|
|
215
|
+
const onChange = vi.fn();
|
|
216
|
+
renderWithTooltip(<DisplayDropdown filters={makeFilters()} onChange={onChange} layout="list" {...viewProps} />);
|
|
217
|
+
fireEvent.click(screen.getByRole("button", { name: /显示/ }));
|
|
218
|
+
const options = document.querySelector('[data-section="options"]') as HTMLElement;
|
|
219
|
+
const sizeButtons = within(options).getAllByRole("button").filter((b) => /^\d+$/.test(b.textContent ?? ""));
|
|
220
|
+
const labels = sizeButtons.map((b) => b.textContent);
|
|
221
|
+
expect(labels).toContain("100");
|
|
222
|
+
expect(labels).toContain("200");
|
|
223
|
+
expect(labels).not.toContain("500");
|
|
224
|
+
expect(labels).not.toContain("1000");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("Pro 环境且 free 用户——500/1000 锁定但可见", async () => {
|
|
228
|
+
// mock implementedModules.billing = true(Pro 环境)
|
|
229
|
+
const modules = await import("@/plugins/modules.gen");
|
|
230
|
+
vi.spyOn(modules, "implementedModules", "get").mockReturnValue({
|
|
231
|
+
...modules.implementedModules,
|
|
232
|
+
billing: true,
|
|
233
|
+
} as any);
|
|
234
|
+
|
|
218
235
|
mockUseFeature.mockReturnValue({ enabled: false, loading: false });
|
|
219
236
|
const onChange = vi.fn();
|
|
220
237
|
renderWithTooltip(<DisplayDropdown filters={makeFilters()} onChange={onChange} layout="list" {...viewProps} />);
|
|
@@ -229,10 +246,16 @@ describe("DisplayDropdown", () => {
|
|
|
229
246
|
expect(btn1000).toBeDisabled();
|
|
230
247
|
fireEvent.click(btn500 as HTMLElement);
|
|
231
248
|
expect(onChange).not.toHaveBeenCalled();
|
|
232
|
-
expect(screen.
|
|
249
|
+
expect(screen.queryByTestId("upgrade-prompt")).not.toBeInTheDocument();
|
|
233
250
|
});
|
|
234
251
|
|
|
235
|
-
it("
|
|
252
|
+
it("Pro 环境且 pro 用户——点击 500 触发 onChange", async () => {
|
|
253
|
+
const modules = await import("@/plugins/modules.gen");
|
|
254
|
+
vi.spyOn(modules, "implementedModules", "get").mockReturnValue({
|
|
255
|
+
...modules.implementedModules,
|
|
256
|
+
billing: true,
|
|
257
|
+
} as any);
|
|
258
|
+
|
|
236
259
|
mockUseFeature.mockReturnValue({ enabled: true, loading: false });
|
|
237
260
|
const onChange = vi.fn();
|
|
238
261
|
renderWithTooltip(<DisplayDropdown filters={makeFilters()} onChange={onChange} layout="list" {...viewProps} />);
|
|
@@ -22,7 +22,6 @@ import { Button } from "@/components/ui/button";
|
|
|
22
22
|
import { Switch } from "@/components/ui/switch";
|
|
23
23
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
24
24
|
import { useFeature } from "@/components/billing/use-feature";
|
|
25
|
-
import { UpgradePrompt } from "@/components/billing/upgrade-prompt";
|
|
26
25
|
import {
|
|
27
26
|
Popover,
|
|
28
27
|
PopoverContent,
|
|
@@ -34,6 +33,7 @@ import type { SavedView, ViewProjectInfo } from "../views/view-types";
|
|
|
34
33
|
import type { StoryView } from "../views/view-switcher";
|
|
35
34
|
import { DEFAULT_DISPLAY_OPTIONS, GROUP_KEYS, ORDER_KEYS, PAGE_SIZE_OPTIONS, type DisplayOptions, type DisplayProperty, type GroupKey, type OrderDirection, type OrderKey, type PageSize } from "./types";
|
|
36
35
|
import { GROUP_LABELS, ORDER_LABELS, PROPERTY_LABELS, LAYOUT_SUPPORT, LAYOUT_PROPERTIES, GROUP_ICONS, ORDER_ICONS } from "./constants";
|
|
36
|
+
import { implementedModules } from "@/plugins/modules.gen";
|
|
37
37
|
|
|
38
38
|
/** 分组选项:布局不支持分组时返回空数组;选中特定项目时隐藏"项目"分组 */
|
|
39
39
|
export function getGroupOptions(layout: StoryView, projectId?: string): GroupKey[] {
|
|
@@ -377,7 +377,7 @@ function PropertiesSection({ selected, onChange, layout, t }: PropertiesSectionP
|
|
|
377
377
|
};
|
|
378
378
|
|
|
379
379
|
return (
|
|
380
|
-
<div data-section="properties" className="
|
|
380
|
+
<div data-section="properties" className=" px-3 py-2">
|
|
381
381
|
<button
|
|
382
382
|
type="button"
|
|
383
383
|
onClick={() => setCollapsed(!collapsed)}
|
|
@@ -432,13 +432,15 @@ function OptionsSection({
|
|
|
432
432
|
onPageSizeChange,
|
|
433
433
|
}: OptionsSectionProps) {
|
|
434
434
|
const { t } = useTranslation("display");
|
|
435
|
-
|
|
435
|
+
// OSS 环境(billing 模块未实现)只显示 100/200;Pro 环境显示全部
|
|
436
|
+
const sizeOptions = implementedModules.billing
|
|
437
|
+
? (PAGE_SIZE_OPTIONS as readonly PageSize[])
|
|
438
|
+
: ([100, 200] as readonly PageSize[]);
|
|
436
439
|
const { enabled: largePageSizeEnabled, loading: largePageSizeLoading } =
|
|
437
440
|
useFeature("largePageSize");
|
|
438
441
|
|
|
439
442
|
const isPageSizeLocked = (v: PageSize) =>
|
|
440
443
|
v > 200 && !largePageSizeEnabled && !largePageSizeLoading;
|
|
441
|
-
const showPageSizeUpgrade = !largePageSizeEnabled && !largePageSizeLoading;
|
|
442
444
|
|
|
443
445
|
return (
|
|
444
446
|
<div data-section="options" className="border-t px-3 py-2">
|
|
@@ -484,9 +486,6 @@ function OptionsSection({
|
|
|
484
486
|
})}
|
|
485
487
|
</div>
|
|
486
488
|
</div>
|
|
487
|
-
{showPageSizeUpgrade && (
|
|
488
|
-
<UpgradePrompt feature="largePageSize" />
|
|
489
|
-
)}
|
|
490
489
|
</div>
|
|
491
490
|
</div>
|
|
492
491
|
);
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { useMemo, useState } from "react";
|
|
19
|
-
import { CircleUser, Search, Check } from "lucide-react";
|
|
19
|
+
import { CircleUser, Search, Check, ArrowDownUp } from "lucide-react";
|
|
20
20
|
import { useTranslation } from "react-i18next";
|
|
21
21
|
import { Input } from "@/components/ui/input";
|
|
22
22
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
@@ -38,18 +38,22 @@ interface AssigneeEditorProps {
|
|
|
38
38
|
className?: string;
|
|
39
39
|
/** 渲染形态:chip=有值时显示 chip 边框(默认,筛选场景),button=始终 Button(列表/详情场景) */
|
|
40
40
|
variant?: "chip" | "button";
|
|
41
|
+
/** 成员范围:'project'=仅项目成员(默认),'team'=所有团队成员 */
|
|
42
|
+
scope?: "project" | "team";
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
export function AssigneeEditor({ assignee, users, onChange, mode = "single", selected, onMultiChange, className, variant = "chip" }: AssigneeEditorProps) {
|
|
45
|
+
export function AssigneeEditor({ assignee, users, onChange, mode = "single", selected, onMultiChange, className, variant = "chip", scope = "project" }: AssigneeEditorProps) {
|
|
44
46
|
const { t } = useTranslation("story");
|
|
45
47
|
const [open, setOpen] = useState(false);
|
|
46
48
|
const [search, setSearch] = useState("");
|
|
47
49
|
const isMulti = mode === "multi";
|
|
48
50
|
const showChip = variant === "chip";
|
|
51
|
+
const [currentScope, setCurrentScope] = useState(scope);
|
|
49
52
|
const filtered = useMemo(
|
|
50
53
|
() => (search ? users.filter((u) => u.username.includes(search)) : users),
|
|
51
54
|
[users, search],
|
|
52
55
|
);
|
|
56
|
+
const isProjectScope = currentScope === "project";
|
|
53
57
|
|
|
54
58
|
return (
|
|
55
59
|
<Popover open={open} onOpenChange={(v) => { setOpen(v); if (v) setSearch(""); }}>
|
|
@@ -110,69 +114,80 @@ export function AssigneeEditor({ assignee, users, onChange, mode = "single", sel
|
|
|
110
114
|
className={className}
|
|
111
115
|
/>
|
|
112
116
|
)}
|
|
113
|
-
<PopoverContent className="w-52 p-1" align="start">
|
|
114
|
-
<div className="relative mb-1">
|
|
115
|
-
<Search className="absolute top-1/2 left-
|
|
117
|
+
<PopoverContent className="w-52 p-1 flex flex-col gap-0 max-h-72" align="start">
|
|
118
|
+
<div className="relative mb-1 shrink-0 sticky top-0 z-10 bg-popover py-0.5 -mx-1 px-1 -mt-1 flex items-center gap-1">
|
|
119
|
+
<Search className="absolute top-1/2 left-4 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
116
120
|
<Input
|
|
117
121
|
value={search}
|
|
118
122
|
onChange={(e) => setSearch(e.target.value)}
|
|
119
|
-
placeholder={t("inline.
|
|
120
|
-
className="h-
|
|
123
|
+
placeholder={isProjectScope ? t("inline.searchProjectMembers") : t("inline.searchTeamMembers")}
|
|
124
|
+
className="h-6 pl-8 pr-2 text-xs md:text-xs"
|
|
121
125
|
/>
|
|
122
|
-
</div>
|
|
123
|
-
{!isMulti && (
|
|
124
126
|
<button
|
|
125
127
|
type="button"
|
|
126
|
-
onClick={() =>
|
|
127
|
-
className=
|
|
128
|
-
|
|
129
|
-
!assignee ? "bg-accent font-medium" : "hover:bg-muted",
|
|
130
|
-
)}
|
|
128
|
+
onClick={() => setCurrentScope(isProjectScope ? "team" : "project")}
|
|
129
|
+
className="shrink-0 flex items-center gap-0.5 text-[10px] text-muted-foreground hover:text-foreground px-1.5 py-1 rounded hover:bg-accent"
|
|
130
|
+
title={isProjectScope ? t("inline.switchToTeam") : t("inline.switchToProject")}
|
|
131
131
|
>
|
|
132
|
-
<
|
|
133
|
-
{t("
|
|
132
|
+
<ArrowDownUp className="size-3" />
|
|
133
|
+
{isProjectScope ? t("inline.scopeTeam") : t("inline.scopeProject")}
|
|
134
134
|
</button>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
135
|
+
</div>
|
|
136
|
+
<div className="overflow-y-auto min-h-0 flex-1 -mx-1 -mb-1 p-1">
|
|
137
|
+
{!isMulti && (
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
onClick={() => { onChange(null); setOpen(false); }}
|
|
141
|
+
className={cn(
|
|
142
|
+
"flex w-full items-center gap-2 rounded px-2 py-1.5 text-xs",
|
|
143
|
+
!assignee ? "bg-accent font-medium" : "hover:bg-muted",
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
<CircleUser strokeWidth={1.5} className="size-6 text-muted-foreground" />
|
|
147
|
+
{t("filter.unassignedAssignee")}
|
|
148
|
+
</button>
|
|
149
|
+
)}
|
|
150
|
+
{filtered.length === 0 ? (
|
|
151
|
+
<div className="px-2 py-1.5 text-xs text-muted-foreground">{t("inline.noMatchMembers")}</div>
|
|
152
|
+
) : (
|
|
153
|
+
filtered.map((u) => {
|
|
154
|
+
const active = isMulti ? (selected ?? []).includes(u.id) : assignee?.id === u.id;
|
|
155
|
+
return (
|
|
156
|
+
<button
|
|
157
|
+
key={u.id}
|
|
158
|
+
type="button"
|
|
159
|
+
onClick={() => {
|
|
160
|
+
if (isMulti) {
|
|
161
|
+
onMultiChange?.(u.id);
|
|
162
|
+
} else {
|
|
163
|
+
onChange(u.id);
|
|
164
|
+
setOpen(false);
|
|
165
|
+
}
|
|
166
|
+
}}
|
|
167
|
+
className={cn(
|
|
168
|
+
"flex w-full items-center gap-2 rounded px-2 py-1.5 text-xs",
|
|
169
|
+
active ? "bg-accent font-medium" : "hover:bg-muted",
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
{isMulti && (
|
|
173
|
+
<div className={cn("flex size-4 shrink-0 items-center justify-center rounded-sm border", active ? "border-primary bg-primary text-primary-foreground" : "border-input")}>
|
|
174
|
+
{active && <Check className="size-3" />}
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
<UserAvatar
|
|
178
|
+
avatarConfig={u.avatarConfig}
|
|
179
|
+
bgShape={u.bgShape as "circle" | "rounded" | "square" | null}
|
|
180
|
+
bgColor={u.bgColor}
|
|
181
|
+
name={u.username}
|
|
182
|
+
size="sm"
|
|
183
|
+
/>
|
|
184
|
+
{u.username}
|
|
185
|
+
{!isMulti && active && <Check className="ml-auto size-3.5 shrink-0 text-primary" />}
|
|
186
|
+
</button>
|
|
187
|
+
);
|
|
188
|
+
})
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
176
191
|
</PopoverContent>
|
|
177
192
|
</Popover>
|
|
178
193
|
);
|
|
@@ -28,7 +28,7 @@ import { CalendarView } from "./views/calendar-view";
|
|
|
28
28
|
import { TimelineView } from "./views/timeline-view";
|
|
29
29
|
import { StoryList } from "./views/story-list";
|
|
30
30
|
import { serializeFilters, type FilterCondition } from "./filters";
|
|
31
|
-
import { listStoriesFn, archiveStoryFn } from "@/server/serverFns/story";
|
|
31
|
+
import { listStoriesFn, archiveStoryFn, listUsersBriefFn } from "@/server/serverFns/story";
|
|
32
32
|
import { listProjectsBriefFn } from "@/server/serverFns/project";
|
|
33
33
|
import {
|
|
34
34
|
listStoryViewsFn, createStoryViewFn, updateStoryViewFn, deleteStoryViewFn,
|
|
@@ -64,7 +64,7 @@ interface StoryListViewProps {
|
|
|
64
64
|
const STORY_PROJECT_FILTER_KEY = "story.projectFilter";
|
|
65
65
|
const STORY_SEARCH_KEY = "story.search";
|
|
66
66
|
|
|
67
|
-
export function StoryListView({ stories: initialStories, users, projects: initialProjects, labels }: StoryListViewProps) {
|
|
67
|
+
export function StoryListView({ stories: initialStories, users: initialUsers, projects: initialProjects, labels }: StoryListViewProps) {
|
|
68
68
|
const { t } = useTranslation("story");
|
|
69
69
|
const { session } = useRouteContext({ from: "/_nav/stories/" });
|
|
70
70
|
const navigate = useNavigate({ from: "/stories" });
|
|
@@ -77,6 +77,7 @@ export function StoryListView({ stories: initialStories, users, projects: initia
|
|
|
77
77
|
const [filterConditions, setFilterConditions] = useState<FilterCondition[]>([]);
|
|
78
78
|
const [stories, setStories] = useState(initialStories);
|
|
79
79
|
const [projects, setProjects] = useState(initialProjects);
|
|
80
|
+
const [users, setUsers] = useState<UserBrief[]>(initialUsers);
|
|
80
81
|
const [projectId, setProjectId] = useState<string | undefined>(() => {
|
|
81
82
|
try {
|
|
82
83
|
return localStorage.getItem(STORY_PROJECT_FILTER_KEY) ?? undefined;
|
|
@@ -105,6 +106,16 @@ export function StoryListView({ stories: initialStories, users, projects: initia
|
|
|
105
106
|
setProjects(initialProjects);
|
|
106
107
|
}, [initialStories, initialProjects]);
|
|
107
108
|
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!projectId || projectId === "__archived__") {
|
|
111
|
+
setUsers(initialUsers);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
listUsersBriefFn({ data: { projectId } })
|
|
115
|
+
.then((u: UserBrief[]) => setUsers(u))
|
|
116
|
+
.catch(() => {});
|
|
117
|
+
}, [projectId]);
|
|
118
|
+
|
|
108
119
|
useEffect(() => {
|
|
109
120
|
if (!projectId && !searchText) return;
|
|
110
121
|
const isArchived = projectId === "__archived__";
|
|
@@ -9,70 +9,70 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
9
9
|
{
|
|
10
10
|
"name": "@tanstack/react-start",
|
|
11
11
|
"version": "v1.168.46",
|
|
12
|
-
"lastUpdate": "Aug
|
|
12
|
+
"lastUpdate": "Aug 24, 2026",
|
|
13
13
|
"source": "apps/web/package.json#dependencies",
|
|
14
14
|
"docsUrl": "https://tanstack.com/start/latest"
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
"name": "@tanstack/react-router",
|
|
18
18
|
"version": "v1.170.29",
|
|
19
|
-
"lastUpdate": "Aug
|
|
19
|
+
"lastUpdate": "Aug 24, 2026",
|
|
20
20
|
"source": "apps/web/package.json#dependencies",
|
|
21
21
|
"docsUrl": "https://tanstack.com/router/latest"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"name": "@tanstack/react-query",
|
|
25
25
|
"version": "v5.101.2",
|
|
26
|
-
"lastUpdate": "Aug
|
|
26
|
+
"lastUpdate": "Aug 24, 2026",
|
|
27
27
|
"source": "apps/web/package.json#dependencies",
|
|
28
28
|
"docsUrl": "https://tanstack.com/query/latest"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"name": "@tanstack/ai",
|
|
32
32
|
"version": "v0.43.0",
|
|
33
|
-
"lastUpdate": "Aug
|
|
33
|
+
"lastUpdate": "Aug 24, 2026",
|
|
34
34
|
"source": "apps/web/package.json#dependencies",
|
|
35
35
|
"docsUrl": "https://tanstack.com/ai/latest"
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
"name": "@tanstack/react-table",
|
|
39
39
|
"version": "v9.1.0",
|
|
40
|
-
"lastUpdate": "Aug
|
|
40
|
+
"lastUpdate": "Aug 24, 2026",
|
|
41
41
|
"source": "apps/web/package.json#dependencies",
|
|
42
42
|
"docsUrl": "https://tanstack.com/table/latest"
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"name": "@tanstack/react-form",
|
|
46
46
|
"version": "v2.0.0-alpha.0",
|
|
47
|
-
"lastUpdate": "Aug
|
|
47
|
+
"lastUpdate": "Aug 24, 2026",
|
|
48
48
|
"source": "apps/web/package.json#dependencies",
|
|
49
49
|
"docsUrl": "https://tanstack.com/form/latest"
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
"name": "@tanstack/charts",
|
|
53
53
|
"version": "v0.9.0",
|
|
54
|
-
"lastUpdate": "Aug
|
|
54
|
+
"lastUpdate": "Aug 24, 2026",
|
|
55
55
|
"source": "apps/admin/package.json#dependencies",
|
|
56
56
|
"docsUrl": "https://tanstack.com/charts/latest"
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
"name": "@tanstack/react-pacer",
|
|
60
60
|
"version": "v0.22.1",
|
|
61
|
-
"lastUpdate": "Aug
|
|
61
|
+
"lastUpdate": "Aug 24, 2026",
|
|
62
62
|
"source": "apps/web/package.json#dependencies",
|
|
63
63
|
"docsUrl": "https://tanstack.com/pacer/latest"
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
"name": "@tanstack/react-hotkeys",
|
|
67
67
|
"version": "v0.10.0",
|
|
68
|
-
"lastUpdate": "Aug
|
|
68
|
+
"lastUpdate": "Aug 24, 2026",
|
|
69
69
|
"source": "apps/web/package.json#dependencies",
|
|
70
70
|
"docsUrl": "https://tanstack.com/hotkeys/latest"
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"name": "@tanstack/react-virtual",
|
|
74
74
|
"version": "v3.14.9",
|
|
75
|
-
"lastUpdate": "Aug
|
|
75
|
+
"lastUpdate": "Aug 24, 2026",
|
|
76
76
|
"source": "apps/admin/package.json#dependencies",
|
|
77
77
|
"docsUrl": "https://tanstack.com/virtual/latest"
|
|
78
78
|
}
|
|
@@ -85,15 +85,15 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
85
85
|
"items": [
|
|
86
86
|
{
|
|
87
87
|
"name": "@allbluecn/web-app",
|
|
88
|
-
"version": "v0.4.
|
|
89
|
-
"lastUpdate": "Aug
|
|
88
|
+
"version": "v0.4.17",
|
|
89
|
+
"lastUpdate": "Aug 24, 2026",
|
|
90
90
|
"source": "apps/web/package.json",
|
|
91
91
|
"docsUrl": "https://github.com/allbluecn/allblue"
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
"name": "@allbluecn/kernel",
|
|
95
95
|
"version": "v0.3.0",
|
|
96
|
-
"lastUpdate": "Aug
|
|
96
|
+
"lastUpdate": "Aug 24, 2026",
|
|
97
97
|
"source": "packages/kernel/package.json",
|
|
98
98
|
"docsUrl": "https://github.com/allbluecn/allblue"
|
|
99
99
|
}
|
|
@@ -107,42 +107,42 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
107
107
|
{
|
|
108
108
|
"name": "Vite",
|
|
109
109
|
"version": "v8.1.5",
|
|
110
|
-
"lastUpdate": "Aug
|
|
110
|
+
"lastUpdate": "Aug 24, 2026",
|
|
111
111
|
"source": "apps/web/package.json#devDependencies",
|
|
112
112
|
"docsUrl": "https://vite.dev/"
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "Node.js",
|
|
116
116
|
"version": "v24.19.0",
|
|
117
|
-
"lastUpdate": "Aug
|
|
117
|
+
"lastUpdate": "Aug 24, 2026",
|
|
118
118
|
"source": ".nvmrc",
|
|
119
119
|
"docsUrl": "https://nodejs.org/"
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
"name": "Prisma",
|
|
123
123
|
"version": "v7.8.0",
|
|
124
|
-
"lastUpdate": "Aug
|
|
124
|
+
"lastUpdate": "Aug 24, 2026",
|
|
125
125
|
"source": "packages/database/package.json#devDependencies",
|
|
126
126
|
"docsUrl": "https://www.prisma.io/docs"
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
"name": "shadcn UI",
|
|
130
130
|
"version": "v1.2.4",
|
|
131
|
-
"lastUpdate": "Aug
|
|
131
|
+
"lastUpdate": "Aug 24, 2026",
|
|
132
132
|
"source": "apps/web/package.json#dependencies",
|
|
133
133
|
"docsUrl": "https://ui.shadcn.com/"
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
"name": "TipTap",
|
|
137
137
|
"version": "v3.27.4",
|
|
138
|
-
"lastUpdate": "Aug
|
|
138
|
+
"lastUpdate": "Aug 24, 2026",
|
|
139
139
|
"source": "packages/shared/package.json#devDependencies",
|
|
140
140
|
"docsUrl": "https://tiptap.dev/"
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
"name": "Lucide React",
|
|
144
144
|
"version": "v1.24.0",
|
|
145
|
-
"lastUpdate": "Aug
|
|
145
|
+
"lastUpdate": "Aug 24, 2026",
|
|
146
146
|
"source": "packages/ui/package.json#dependencies",
|
|
147
147
|
"docsUrl": "https://lucide.dev/"
|
|
148
148
|
}
|
|
@@ -83,8 +83,8 @@ function StoryDetailSkeleton() {
|
|
|
83
83
|
|
|
84
84
|
export const Route = createFileRoute("/_nav/stories/$storyId")({
|
|
85
85
|
loader: async ({ params }) => {
|
|
86
|
+
const story = await getStoryDetailFn({ data: { storyId: params.storyId } });
|
|
86
87
|
const [
|
|
87
|
-
story,
|
|
88
88
|
users,
|
|
89
89
|
comments,
|
|
90
90
|
activities,
|
|
@@ -99,8 +99,7 @@ export const Route = createFileRoute("/_nav/stories/$storyId")({
|
|
|
99
99
|
prds,
|
|
100
100
|
docs,
|
|
101
101
|
] = await Promise.all([
|
|
102
|
-
|
|
103
|
-
listUsersBriefFn(),
|
|
102
|
+
listUsersBriefFn({ data: story.projectId ? { projectId: story.projectId } : undefined }),
|
|
104
103
|
listCommentsFn({ data: { storyId: params.storyId } }),
|
|
105
104
|
listActivitiesFn({ data: { storyId: params.storyId, take: 50 } }),
|
|
106
105
|
getStoryNeighborsFn({ data: { storyId: params.storyId } }),
|
|
@@ -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 () => {
|
|
@@ -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
|