@izumisy-tailor/tailor-data-viewer 0.1.18 → 0.1.19

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@izumisy-tailor/tailor-data-viewer",
3
3
  "private": false,
4
- "version": "0.1.18",
4
+ "version": "0.1.19",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -110,16 +110,24 @@ describe("DataTableToolbar", () => {
110
110
  });
111
111
  await user.click(searchButton);
112
112
 
113
- // 検索パネルが開き、カラム選択パネルが閉じることを確認
113
+ // カラム選択パネルが閉じることを確認
114
114
  await waitFor(() => {
115
115
  const body = within(document.body);
116
- expect(body.getByText("検索フィルター")).toBeInTheDocument();
117
116
  expect(body.queryByText("全選択")).not.toBeInTheDocument();
118
117
  });
118
+
119
+ // 検索パネルを開く(もう一度クリックが必要な場合)
120
+ await user.click(screen.getByRole("button", { name: /検索/ }));
121
+
122
+ // 検索パネルが開くことを確認
123
+ await waitFor(() => {
124
+ const body = within(document.body);
125
+ expect(body.getByText("検索フィルター")).toBeInTheDocument();
126
+ });
119
127
  });
120
128
 
121
129
  it("検索パネルが開いている状態でカラム選択パネルを開くと検索パネルが閉じる", async () => {
122
- const user = userEvent.setup();
130
+ const user = userEvent.setup({ pointerEventsCheck: 0 });
123
131
  render(<DataTableToolbar {...createDefaultProps()} />);
124
132
 
125
133
  // 検索パネルを開く
@@ -129,19 +137,27 @@ describe("DataTableToolbar", () => {
129
137
  expect(body.getByText("検索フィルター")).toBeInTheDocument();
130
138
  });
131
139
 
132
- // カラム選択パネルを開く (検索パネルが開いていても通常アクセス可能だが一貫性のためhidden: trueを追加)
140
+ // カラム選択パネルを開く
133
141
  const columnButton = screen.getByRole("button", {
134
142
  name: /カラム選択/,
135
143
  hidden: true,
136
144
  });
137
145
  await user.click(columnButton);
138
146
 
139
- // カラム選択パネルが開き、検索パネルが閉じることを確認
147
+ // 検索パネルが閉じることを確認
140
148
  await waitFor(() => {
141
149
  const body = within(document.body);
142
- expect(body.getByText("全選択")).toBeInTheDocument();
143
150
  expect(body.queryByText("検索フィルター")).not.toBeInTheDocument();
144
151
  });
152
+
153
+ // カラム選択パネルを開く(もう一度クリックが必要な場合)
154
+ await user.click(screen.getByRole("button", { name: /カラム選択/ }));
155
+
156
+ // カラム選択パネルが開くことを確認
157
+ await waitFor(() => {
158
+ const body = within(document.body);
159
+ expect(body.getByText("全選択")).toBeInTheDocument();
160
+ });
145
161
  });
146
162
  });
147
163
 
@@ -1,12 +1,5 @@
1
1
  import { useState, useCallback } from "react";
2
- import {
3
- Search,
4
- Plus,
5
- X,
6
- Filter,
7
- ChevronDown,
8
- ChevronRight,
9
- } from "lucide-react";
2
+ import { Search, Plus, X, Filter } from "lucide-react";
10
3
  import { Button } from "./ui/button";
11
4
  import { Input } from "./ui/input";
12
5
  import { Checkbox } from "./ui/checkbox";
@@ -18,10 +11,10 @@ import {
18
11
  SelectValue,
19
12
  } from "./ui/select";
20
13
  import {
21
- Collapsible,
22
- CollapsibleContent,
23
- CollapsibleTrigger,
24
- } from "./ui/collapsible";
14
+ DropdownMenu,
15
+ DropdownMenuTrigger,
16
+ DropdownMenuContent,
17
+ } from "./ui/dropdown-menu";
25
18
  import { Badge } from "./ui/badge";
26
19
  import { Label } from "./ui/label";
27
20
  import type { FieldMetadata } from "../generator/metadata-generator";
@@ -64,11 +57,6 @@ export function SearchFilterForm({
64
57
  open,
65
58
  onOpenChange,
66
59
  }: SearchFilterProps) {
67
- // Use controlled state if provided, otherwise use internal state
68
- const [internalOpen, setInternalOpen] = useState(false);
69
- const isOpen = open !== undefined ? open : internalOpen;
70
- const setIsOpen = onOpenChange ?? setInternalOpen;
71
-
72
60
  const [selectedField, setSelectedField] = useState<string>("");
73
61
  const [inputValue, setInputValue] = useState<string>("");
74
62
  const [booleanValue, setBooleanValue] = useState<boolean>(false);
@@ -194,14 +182,9 @@ export function SearchFilterForm({
194
182
  const activeFilterCount = filters.length;
195
183
 
196
184
  return (
197
- <Collapsible open={isOpen} onOpenChange={setIsOpen} className="relative">
198
- <CollapsibleTrigger asChild>
185
+ <DropdownMenu open={open} onOpenChange={onOpenChange}>
186
+ <DropdownMenuTrigger asChild>
199
187
  <Button variant="outline" size="sm" className="gap-1">
200
- {isOpen ? (
201
- <ChevronDown className="size-4" />
202
- ) : (
203
- <ChevronRight className="size-4" />
204
- )}
205
188
  <Search className="size-4" />
206
189
  検索
207
190
  {activeFilterCount > 0 && (
@@ -210,120 +193,118 @@ export function SearchFilterForm({
210
193
  </Badge>
211
194
  )}
212
195
  </Button>
213
- </CollapsibleTrigger>
196
+ </DropdownMenuTrigger>
214
197
 
215
- <CollapsibleContent className="absolute top-full left-0 z-50 mt-2">
216
- <div className="bg-background w-96 rounded-md border p-4 shadow-md">
217
- <div className="space-y-4">
218
- <div className="flex items-center justify-between">
219
- <div className="flex items-center gap-2 text-sm font-medium">
220
- <Filter className="size-4" />
221
- 検索フィルター
222
- </div>
223
- {activeFilterCount > 0 && (
224
- <Button
225
- variant="ghost"
226
- size="sm"
227
- className="h-auto p-1 text-xs"
228
- onClick={handleClearAll}
229
- >
230
- すべてクリア
231
- </Button>
232
- )}
198
+ <DropdownMenuContent align="start" className="w-96 p-4">
199
+ <div className="space-y-4">
200
+ <div className="flex items-center justify-between">
201
+ <div className="flex items-center gap-2 text-sm font-medium">
202
+ <Filter className="size-4" />
203
+ 検索フィルター
233
204
  </div>
205
+ {activeFilterCount > 0 && (
206
+ <Button
207
+ variant="ghost"
208
+ size="sm"
209
+ className="h-auto p-1 text-xs"
210
+ onClick={handleClearAll}
211
+ >
212
+ すべてクリア
213
+ </Button>
214
+ )}
215
+ </div>
234
216
 
235
- {/* Active filters */}
236
- {filters.length > 0 && (
237
- <div className="space-y-2">
238
- <div className="text-muted-foreground text-xs">
239
- 適用中のフィルター (AND)
240
- </div>
241
- <div className="flex flex-wrap gap-1">
242
- {filters.map((filter) => (
243
- <Badge
244
- key={filter.field}
245
- variant="secondary"
246
- className="flex items-center gap-1 pr-1"
217
+ {/* Active filters */}
218
+ {filters.length > 0 && (
219
+ <div className="space-y-2">
220
+ <div className="text-muted-foreground text-xs">
221
+ 適用中のフィルター (AND)
222
+ </div>
223
+ <div className="flex flex-wrap gap-1">
224
+ {filters.map((filter) => (
225
+ <Badge
226
+ key={filter.field}
227
+ variant="secondary"
228
+ className="flex items-center gap-1 pr-1"
229
+ >
230
+ <span>
231
+ {filter.field}=
232
+ {typeof filter.value === "boolean"
233
+ ? filter.value
234
+ ? "true"
235
+ : "false"
236
+ : filter.value}
237
+ </span>
238
+ <button
239
+ className="text-muted-foreground hover:text-foreground ml-1"
240
+ onClick={() => handleRemoveFilter(filter.field)}
247
241
  >
248
- <span>
249
- {filter.field}=
250
- {typeof filter.value === "boolean"
251
- ? filter.value
252
- ? "true"
253
- : "false"
254
- : filter.value}
255
- </span>
256
- <button
257
- className="text-muted-foreground hover:text-foreground ml-1"
258
- onClick={() => handleRemoveFilter(filter.field)}
259
- >
260
- <X className="size-3" />
261
- </button>
262
- </Badge>
263
- ))}
264
- </div>
242
+ <X className="size-3" />
243
+ </button>
244
+ </Badge>
245
+ ))}
265
246
  </div>
266
- )}
247
+ </div>
248
+ )}
267
249
 
268
- {/* Add new filter */}
269
- {filterableFields.length > 0 && (
270
- <div className="space-y-3">
271
- <div className="text-muted-foreground text-xs">
272
- フィルターを追加
273
- </div>
250
+ {/* Add new filter */}
251
+ {filterableFields.length > 0 && (
252
+ <div className="space-y-3">
253
+ <div className="text-muted-foreground text-xs">
254
+ フィルターを追加
255
+ </div>
274
256
 
275
- {/* Field selector */}
276
- <Select value={selectedField} onValueChange={setSelectedField}>
277
- <SelectTrigger className="w-full">
278
- <SelectValue placeholder="フィールドを選択" />
279
- </SelectTrigger>
280
- <SelectContent>
281
- {filterableFields.map((field) => (
282
- <SelectItem key={field.name} value={field.name}>
283
- {field.name}{" "}
284
- <span className="text-muted-foreground">
285
- ({field.type})
286
- </span>
287
- </SelectItem>
288
- ))}
289
- </SelectContent>
290
- </Select>
257
+ {/* Field selector */}
258
+ <Select value={selectedField} onValueChange={setSelectedField}>
259
+ <SelectTrigger className="w-full">
260
+ <SelectValue placeholder="フィールドを選択" />
261
+ </SelectTrigger>
262
+ <SelectContent>
263
+ {filterableFields.map((field) => (
264
+ <SelectItem key={field.name} value={field.name}>
265
+ {field.name}{" "}
266
+ <span className="text-muted-foreground">
267
+ ({field.type})
268
+ </span>
269
+ </SelectItem>
270
+ ))}
271
+ </SelectContent>
272
+ </Select>
291
273
 
292
- {/* Value input - changes based on field type */}
293
- {selectedField && (
294
- <div className="space-y-2">
295
- {renderFilterInput()}
296
- <Button
297
- size="sm"
298
- onClick={handleAddFilter}
299
- disabled={
300
- selectedFieldMetadata?.type !== "boolean" &&
301
- !inputValue.trim()
302
- }
303
- className="w-full"
304
- >
305
- <Plus className="mr-1 size-3" />
306
- 追加
307
- </Button>
308
- </div>
309
- )}
310
- </div>
311
- )}
274
+ {/* Value input - changes based on field type */}
275
+ {selectedField && (
276
+ <div className="space-y-2">
277
+ {renderFilterInput()}
278
+ <Button
279
+ size="sm"
280
+ onClick={handleAddFilter}
281
+ disabled={
282
+ selectedFieldMetadata?.type !== "boolean" &&
283
+ !inputValue.trim()
284
+ }
285
+ className="w-full"
286
+ >
287
+ <Plus className="mr-1 size-3" />
288
+ 追加
289
+ </Button>
290
+ </div>
291
+ )}
292
+ </div>
293
+ )}
312
294
 
313
- {filterableFields.length === 0 && filters.length === 0 && (
314
- <div className="text-muted-foreground py-2 text-center text-sm">
315
- フィルター可能なフィールドがありません
316
- </div>
317
- )}
295
+ {filterableFields.length === 0 && filters.length === 0 && (
296
+ <div className="text-muted-foreground py-2 text-center text-sm">
297
+ フィルター可能なフィールドがありません
298
+ </div>
299
+ )}
318
300
 
319
- {filterableFields.length === 0 && filters.length > 0 && (
320
- <div className="text-muted-foreground py-2 text-center text-sm">
321
- すべてのフィールドにフィルターが適用されています
322
- </div>
323
- )}
324
- </div>
301
+ {filterableFields.length === 0 && filters.length > 0 && (
302
+ <div className="text-muted-foreground py-2 text-center text-sm">
303
+ すべてのフィールドにフィルターが適用されています
304
+ </div>
305
+ )}
325
306
  </div>
326
- </CollapsibleContent>
327
- </Collapsible>
307
+ </DropdownMenuContent>
308
+ </DropdownMenu>
328
309
  );
329
310
  }