Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit19745a2

Browse files
1 parent9fbccc0 commit19745a2

File tree

3 files changed

+244
-29
lines changed

3 files changed

+244
-29
lines changed

‎site/src/pages/TasksPage/TasksPage.stories.tsx

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
importtype{Meta,StoryObj}from"@storybook/react";
22
import{expect,spyOn,userEvent,within}from"@storybook/test";
3+
import{API}from"api/api";
4+
import{MockUsers}from"pages/UsersPage/storybookData/users";
35
import{
46
MockTemplate,
57
MockUserOwner,
@@ -20,6 +22,15 @@ const meta: Meta<typeof TasksPage> = {
2022
decorators:[withAuthProvider],
2123
parameters:{
2224
user:MockUserOwner,
25+
permissions:{
26+
viewDeploymentConfig:true,
27+
},
28+
},
29+
beforeEach:()=>{
30+
spyOn(API,"getUsers").mockResolvedValue({
31+
users:MockUsers,
32+
count:MockUsers.length,
33+
});
2334
},
2435
};
2536

@@ -62,7 +73,8 @@ export const LoadingTasks: Story = {
6273
constcanvas=within(canvasElement);
6374

6475
awaitstep("Select the first AI template",async()=>{
65-
constcombobox=awaitcanvas.findByRole("combobox");
76+
constform=awaitcanvas.findByRole("form");
77+
constcombobox=awaitwithin(form).findByRole("combobox");
6678
expect(combobox).toHaveTextContent(MockTemplate.display_name);
6779
});
6880
},
@@ -94,37 +106,40 @@ export const LoadedTasks: Story = {
94106
},
95107
};
96108

109+
constnewTaskData={
110+
prompt:"Create a new task",
111+
workspace:{
112+
...MockWorkspace,
113+
id:"workspace-4",
114+
latest_app_status:{
115+
...MockWorkspaceAppStatus,
116+
message:"Task created successfully!",
117+
},
118+
},
119+
};
120+
97121
exportconstCreateTaskSuccessfully:Story={
98122
decorators:[withProxyProvider()],
99123
beforeEach:()=>{
100124
spyOn(data,"fetchAITemplates").mockResolvedValue([MockTemplate]);
101-
spyOn(data,"fetchTasks").mockResolvedValue(MockTasks);
102-
spyOn(data,"createTask").mockImplementation((prompt:string)=>{
103-
returnPromise.resolve({
104-
prompt,
105-
workspace:{
106-
...MockWorkspace,
107-
latest_app_status:{
108-
...MockWorkspaceAppStatus,
109-
message:"Task created successfully!",
110-
},
111-
},
112-
});
113-
});
125+
spyOn(data,"fetchTasks")
126+
.mockResolvedValueOnce(MockTasks)
127+
.mockResolvedValue([newTaskData, ...MockTasks]);
128+
spyOn(data,"createTask").mockResolvedValue(newTaskData);
114129
},
115130
play:async({ canvasElement, step})=>{
116131
constcanvas=within(canvasElement);
117132

118133
awaitstep("Run task",async()=>{
119134
constprompt=awaitcanvas.findByLabelText(/prompt/i);
120-
awaituserEvent.type(prompt,"Create a new task");
135+
awaituserEvent.type(prompt,newTaskData.prompt);
121136
constsubmitButton=canvas.getByRole("button",{name:/runtask/i});
122137
awaituserEvent.click(submitButton);
123138
});
124139

125140
awaitstep("Verify task in the table",async()=>{
126141
awaitcanvas.findByRole("row",{
127-
name:/createanewtask/i,
142+
name:newRegExp(newTaskData.prompt,"i"),
128143
});
129144
});
130145
},
@@ -158,6 +173,29 @@ export const CreateTaskError: Story = {
158173
},
159174
};
160175

176+
exportconstNonAdmin:Story={
177+
decorators:[withProxyProvider()],
178+
parameters:{
179+
permissions:{
180+
viewDeploymentConfig:false,
181+
},
182+
},
183+
beforeEach:()=>{
184+
spyOn(data,"fetchAITemplates").mockResolvedValue([MockTemplate]);
185+
spyOn(data,"fetchTasks").mockResolvedValue(MockTasks);
186+
},
187+
play:async({ canvasElement, step})=>{
188+
constcanvas=within(canvasElement);
189+
190+
awaitstep("Can't see filters",async()=>{
191+
awaitcanvas.findByRole("table");
192+
expect(
193+
canvas.queryByRole("region",{name:/filters/i}),
194+
).not.toBeInTheDocument();
195+
});
196+
},
197+
};
198+
161199
constMockTasks=[
162200
{
163201
workspace:{

‎site/src/pages/TasksPage/TasksPage.tsx

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,18 @@ import { useAuthenticated } from "hooks";
3232
import{ExternalLinkIcon,RotateCcwIcon,SendIcon}from"lucide-react";
3333
import{AI_PROMPT_PARAMETER_NAME,typeTask}from"modules/tasks/tasks";
3434
import{WorkspaceAppStatus}from"modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus";
35-
importtype{FC,ReactNode}from"react";
35+
import{typeFC,typeReactNode,useState}from"react";
3636
import{Helmet}from"react-helmet-async";
3737
import{useMutation,useQuery,useQueryClient}from"react-query";
3838
import{LinkasRouterLink}from"react-router-dom";
3939
importTextareaAutosizefrom"react-textarea-autosize";
4040
import{pageTitle}from"utils/page";
4141
import{relativeTime}from"utils/time";
42+
import{typeUserOption,UsersCombobox}from"./UsersCombobox";
43+
44+
typeTasksFilter={
45+
user:UserOption|undefined;
46+
};
4247

4348
constTasksPage:FC=()=>{
4449
const{
@@ -50,6 +55,14 @@ const TasksPage: FC = () => {
5055
queryFn:data.fetchAITemplates,
5156
...disabledRefetchOptions,
5257
});
58+
const{ user, permissions}=useAuthenticated();
59+
const[filter,setFilter]=useState<TasksFilter>({
60+
user:{
61+
value:user.username,
62+
label:user.name||user.username,
63+
avatarUrl:user.avatar_url,
64+
},
65+
});
5366

5467
letcontent:ReactNode=null;
5568

@@ -91,7 +104,10 @@ const TasksPage: FC = () => {
91104
) :(
92105
<>
93106
<TaskFormtemplates={templates}/>
94-
<TasksTabletemplates={templates}/>
107+
{permissions.viewDeploymentConfig&&(
108+
<TasksFilterfilter={filter}onFilterChange={setFilter}/>
109+
)}
110+
<TasksTabletemplates={templates}filter={filter}/>
95111
</>
96112
);
97113
}else{
@@ -147,12 +163,9 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
147163
constcreateTaskMutation=useMutation({
148164
mutationFn:async({ prompt, templateId}:CreateTaskMutationFnProps)=>
149165
data.createTask(prompt,user.id,templateId),
150-
onSuccess:(newTask)=>{
151-
// The current data loading is heavy, so we manually update the cache to
152-
// avoid re-fetching. Once we improve data loading, we can replace the
153-
// manual update with queryClient.invalidateQueries.
154-
queryClient.setQueryData<Task[]>(["tasks"],(oldTasks=[])=>{
155-
return[newTask, ...oldTasks];
166+
onSuccess:async()=>{
167+
awaitqueryClient.invalidateQueries({
168+
queryKey:["tasks"],
156169
});
157170
},
158171
});
@@ -186,6 +199,7 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
186199
<form
187200
className="border border-border border-solid rounded-lg p-4"
188201
onSubmit={onSubmit}
202+
aria-label="Create AI task"
189203
>
190204
<fieldsetdisabled={createTaskMutation.isPending}>
191205
<labelhtmlFor="prompt"className="sr-only">
@@ -229,18 +243,43 @@ const TaskForm: FC<TaskFormProps> = ({ templates }) => {
229243
);
230244
};
231245

246+
typeTasksFilterProps={
247+
filter:TasksFilter;
248+
onFilterChange:(filter:TasksFilter)=>void;
249+
};
250+
251+
constTasksFilter:FC<TasksFilterProps>=({ filter, onFilterChange})=>{
252+
return(
253+
<sectionclassName="mt-6"aria-labelledby="filters-title">
254+
<h3id="filters-title"className="sr-only">
255+
Filters
256+
</h3>
257+
<UsersCombobox
258+
selectedOption={filter.user}
259+
onSelect={(userOption)=>
260+
onFilterChange({
261+
...filter,
262+
user:userOption,
263+
})
264+
}
265+
/>
266+
</section>
267+
);
268+
};
269+
232270
typeTasksTableProps={
233271
templates:Template[];
272+
filter:TasksFilter;
234273
};
235274

236-
constTasksTable:FC<TasksTableProps>=({ templates})=>{
275+
constTasksTable:FC<TasksTableProps>=({ templates, filter})=>{
237276
const{
238277
data:tasks,
239278
error,
240279
refetch,
241280
}=useQuery({
242-
queryKey:["tasks"],
243-
queryFn:()=>data.fetchTasks(templates),
281+
queryKey:["tasks",filter],
282+
queryFn:()=>data.fetchTasks(templates,filter),
244283
refetchInterval:10_000,
245284
});
246285

@@ -397,11 +436,16 @@ export const data = {
397436
// template individually and its build parameters resulting in excessive API
398437
// calls and slow performance. Consider implementing a backend endpoint that
399438
// returns all AI-related workspaces in a single request to improve efficiency.
400-
asyncfetchTasks(aiTemplates:Template[]){
439+
asyncfetchTasks(aiTemplates:Template[],filter:TasksFilter){
401440
constworkspaces=awaitPromise.all(
402441
aiTemplates.map((template)=>{
442+
constqueryParts=[`template:${template.name}`];
443+
if(filter.user){
444+
queryParts.push(`owner:${filter.user.value}`);
445+
}
446+
403447
returnAPI.getWorkspaces({
404-
q:`template:${template.name}`,
448+
q:queryParts.join(" "),
405449
limit:100,
406450
});
407451
}),
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
importSkeletonfrom"@mui/material/Skeleton";
2+
import{users}from"api/queries/users";
3+
import{Avatar}from"components/Avatar/Avatar";
4+
import{Button}from"components/Button/Button";
5+
import{
6+
Command,
7+
CommandEmpty,
8+
CommandGroup,
9+
CommandInput,
10+
CommandItem,
11+
CommandList,
12+
}from"components/Command/Command";
13+
import{
14+
Popover,
15+
PopoverContent,
16+
PopoverTrigger,
17+
}from"components/Popover/Popover";
18+
import{useDebouncedValue}from"hooks/debounce";
19+
import{CheckIcon,ChevronsUpDownIcon}from"lucide-react";
20+
import{typeFC,useState}from"react";
21+
import{keepPreviousData,useQuery}from"react-query";
22+
import{cn}from"utils/cn";
23+
24+
exporttypeUserOption={
25+
label:string;
26+
value:string;// Username
27+
avatarUrl?:string;
28+
};
29+
30+
typeUsersComboboxProps={
31+
selectedOption:UserOption|undefined;
32+
onSelect:(option:UserOption|undefined)=>void;
33+
};
34+
35+
exportconstUsersCombobox:FC<UsersComboboxProps>=({
36+
selectedOption,
37+
onSelect,
38+
})=>{
39+
const[open,setOpen]=useState(false);
40+
const[search,setSearch]=useState("");
41+
constdebouncedSearch=useDebouncedValue(search,250);
42+
constusersQuery=useQuery({
43+
...users({q:debouncedSearch}),
44+
select:(data)=>
45+
data.users.toSorted((a,b)=>{
46+
returnselectedOption&&a.username===selectedOption.value ?-1 :0;
47+
}),
48+
placeholderData:keepPreviousData,
49+
});
50+
51+
constoptions=usersQuery.data?.map((user)=>({
52+
label:user.name||user.username,
53+
value:user.username,
54+
avatarUrl:user.avatar_url,
55+
}));
56+
57+
return(
58+
<Popoveropen={open}onOpenChange={setOpen}>
59+
<PopoverTriggerasChild>
60+
<Button
61+
disabled={!options}
62+
variant="outline"
63+
role="combobox"
64+
aria-expanded={open}
65+
className="w-[280px] justify-between"
66+
>
67+
{options ?(
68+
selectedOption ?(
69+
<UserItemoption={selectedOption}className="-ml-1"/>
70+
) :(
71+
"Select user..."
72+
)
73+
) :(
74+
<Skeletonvariant="text"className="w-[120px] h-3"/>
75+
)}
76+
<ChevronsUpDownIconclassName="ml-2 h-4 w-4 shrink-0 opacity-50"/>
77+
</Button>
78+
</PopoverTrigger>
79+
<PopoverContentclassName="w-[280px] p-0">
80+
<Command>
81+
<CommandInput
82+
placeholder="Search user..."
83+
value={search}
84+
onValueChange={setSearch}
85+
/>
86+
<CommandList>
87+
<CommandEmpty>No users found.</CommandEmpty>
88+
<CommandGroup>
89+
{options?.map((option)=>(
90+
<CommandItem
91+
key={option.value}
92+
value={option.value}
93+
onSelect={()=>{
94+
onSelect(
95+
option.value===selectedOption?.value
96+
?undefined
97+
:option,
98+
);
99+
setOpen(false);
100+
}}
101+
>
102+
<UserItemoption={option}/>
103+
<CheckIcon
104+
className={cn(
105+
"ml-2 h-4 w-4",
106+
option.value===selectedOption?.value
107+
?"opacity-100"
108+
:"opacity-0",
109+
)}
110+
/>
111+
</CommandItem>
112+
))}
113+
</CommandGroup>
114+
</CommandList>
115+
</Command>
116+
</PopoverContent>
117+
</Popover>
118+
);
119+
};
120+
121+
typeUserItemProps={
122+
option:UserOption;
123+
className?:string;
124+
};
125+
126+
constUserItem:FC<UserItemProps>=({ option, className})=>{
127+
return(
128+
<divclassName={cn("flex flex-1 items-center gap-2",className)}>
129+
<Avatarsrc={option.avatarUrl}fallback={option.label}/>
130+
{option.label}
131+
</div>
132+
);
133+
};

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp