Skip to content

Commit 3252639

Browse files
committed
add v2 upgrade command, rename commands, and enhance data table bulk actions
1 parent 845bd3f commit 3252639

File tree

20 files changed

+1247
-86
lines changed

20 files changed

+1247
-86
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ testbench.yaml
3434
resources/js/actions
3535
resources/js/routes
3636
resources/js/wayfinder
37+
/workbench/lang

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,15 @@ composer require outhebox/laravel-translations:^2.0
118118
2. **Run the upgrade command** to migrate your v1 data:
119119

120120
```bash
121-
php artisan translations:upgrade-v2
121+
php artisan translations:upgrade
122122
```
123123

124124
This will detect your v1 tables, migrate languages, groups, keys, and translations to the new structure.
125125

126126
3. **Clean up old tables** (optional):
127127

128128
```bash
129-
php artisan translations:upgrade-v2 --cleanup
129+
php artisan translations:upgrade --cleanup
130130
```
131131

132132
4. **Publish the new assets:**

dist/css/app.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/js/app.js

Lines changed: 22 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"private": true,
44
"type": "module",
55
"scripts": {
6-
"build": "vite build",
6+
"build": "vite build && php vendor/bin/testbench vendor:publish --tag=translations-assets --force --no-interaction",
77
"dev": "vite",
88
"format": "prettier --write resources/",
99
"lint": "eslint . --fix",

resources/js/components/data-table/admin-data-table.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,24 @@ export function AdminDataTable<T extends Record<string, any>>({
8181
useState<ColumnConfig[]>(initialColumnConfigs);
8282

8383
const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
84+
const [selectAllPages, setSelectAllPages] = useState(false);
8485
const [prevData, setPrevData] = useState(data);
86+
const [prevFilters, setPrevFilters] = useState(tableConfig.currentFilters);
8587

86-
if (prevData !== data) {
87-
setPrevData(data);
88-
setSelectedIds([]);
88+
const filtersChanged = prevFilters !== tableConfig.currentFilters;
89+
const dataChanged = prevData !== data;
90+
91+
if (filtersChanged || dataChanged) {
92+
if (filtersChanged) {
93+
setPrevFilters(tableConfig.currentFilters);
94+
setSelectAllPages(false);
95+
}
96+
if (dataChanged) {
97+
setPrevData(data);
98+
}
99+
if (filtersChanged || !selectAllPages) {
100+
setSelectedIds([]);
101+
}
89102
}
90103

91104
const bulkActions = tableConfig.bulkActions ?? [];
@@ -226,7 +239,18 @@ export function AdminDataTable<T extends Record<string, any>>({
226239
[rowActions],
227240
);
228241

242+
const handleSelectionChange = useCallback(
243+
(ids: (string | number)[]) => {
244+
if (selectAllPages) {
245+
setSelectAllPages(false);
246+
}
247+
setSelectedIds(ids);
248+
},
249+
[selectAllPages],
250+
);
251+
229252
const handleClearSelection = useCallback(() => {
253+
setSelectAllPages(false);
230254
setSelectedIds([]);
231255
}, []);
232256

@@ -316,8 +340,10 @@ export function AdminDataTable<T extends Record<string, any>>({
316340
rowActions={rowActions ? renderRowActions : undefined}
317341
selectable={isSelectable}
318342
selectedIds={selectedIds}
319-
onSelectionChange={setSelectedIds}
343+
onSelectionChange={handleSelectionChange}
320344
isRowSelectable={isRowSelectable}
345+
selectAllPages={selectAllPages}
346+
onSelectAllPages={setSelectAllPages}
321347
/>
322348

323349
{isSelectable && (
@@ -327,6 +353,8 @@ export function AdminDataTable<T extends Record<string, any>>({
327353
onClearSelection={handleClearSelection}
328354
resourceName={selectedIds.length === 1 ? singular : plural}
329355
onAction={onBulkAction}
356+
selectAllPages={selectAllPages}
357+
totalCount={data.total}
330358
/>
331359
)}
332360
</div>

resources/js/components/data-table/data-table-bulk-actions.tsx

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ interface DataTableBulkActionsProps {
2121
onClearSelection: () => void;
2222
resourceName: string;
2323
onAction?: (name: string, ids: (string | number)[]) => void;
24+
selectAllPages?: boolean;
25+
totalCount?: number;
2426
}
2527

2628
export function DataTableBulkActions({
@@ -29,13 +31,17 @@ export function DataTableBulkActions({
2931
onClearSelection,
3032
resourceName,
3133
onAction,
34+
selectAllPages = false,
35+
totalCount = 0,
3236
}: DataTableBulkActionsProps) {
3337
const [confirmAction, setConfirmAction] = useState<BulkActionConfig | null>(
3438
null,
3539
);
3640
const [isSubmitting, setIsSubmitting] = useState(false);
3741

38-
const isVisible = selectedIds.length > 0 && actions.length > 0;
42+
const isVisible =
43+
(selectedIds.length > 0 || selectAllPages) && actions.length > 0;
44+
const displayCount = selectAllPages ? totalCount : selectedIds.length;
3945

4046
const handleAction = (action: BulkActionConfig) => {
4147
if (action.confirm) {
@@ -45,6 +51,19 @@ export function DataTableBulkActions({
4551
}
4652
};
4753

54+
const buildActionUrl = (url: string): string => {
55+
if (!selectAllPages) return url;
56+
const queryString = window.location.search;
57+
return queryString ? `${url}${queryString}` : url;
58+
};
59+
60+
const buildPayload = () => {
61+
if (selectAllPages) {
62+
return { select_all: true };
63+
}
64+
return { ids: selectedIds };
65+
};
66+
4867
const executeAction = (action: BulkActionConfig) => {
4968
if (!action.url && onAction) {
5069
onAction(action.name, selectedIds);
@@ -58,7 +77,7 @@ export function DataTableBulkActions({
5877
if (action.download) {
5978
const form = document.createElement('form');
6079
form.method = 'POST';
61-
form.action = action.url;
80+
form.action = buildActionUrl(action.url);
6281
form.style.display = 'none';
6382

6483
const csrfToken = document.querySelector<HTMLMetaElement>(
@@ -72,40 +91,44 @@ export function DataTableBulkActions({
7291
form.appendChild(csrfInput);
7392
}
7493

75-
selectedIds.forEach((id) => {
94+
if (selectAllPages) {
7695
const input = document.createElement('input');
7796
input.type = 'hidden';
78-
input.name = 'ids[]';
79-
input.value = String(id);
97+
input.name = 'select_all';
98+
input.value = '1';
8099
form.appendChild(input);
81-
});
100+
} else {
101+
selectedIds.forEach((id) => {
102+
const input = document.createElement('input');
103+
input.type = 'hidden';
104+
input.name = 'ids[]';
105+
input.value = String(id);
106+
form.appendChild(input);
107+
});
108+
}
82109

83110
document.body.appendChild(form);
84111
form.submit();
85112
document.body.removeChild(form);
86113

87-
toast.success(`Exporting ${selectedIds.length} ${resourceName}...`);
114+
toast.success(`Exporting ${displayCount} ${resourceName}...`);
88115
onClearSelection();
89116
setConfirmAction(null);
90117
return;
91118
}
92119

93120
setIsSubmitting(true);
94121

95-
router.post(
96-
action.url,
97-
{ ids: selectedIds },
98-
{
99-
preserveScroll: true,
100-
onSuccess: () => {
101-
onClearSelection();
102-
setConfirmAction(null);
103-
},
104-
onFinish: () => {
105-
setIsSubmitting(false);
106-
},
122+
router.post(buildActionUrl(action.url), buildPayload(), {
123+
preserveScroll: true,
124+
onSuccess: () => {
125+
onClearSelection();
126+
setConfirmAction(null);
127+
},
128+
onFinish: () => {
129+
setIsSubmitting(false);
107130
},
108-
);
131+
});
109132
};
110133

111134
return (
@@ -120,8 +143,8 @@ export function DataTableBulkActions({
120143
>
121144
<div className="flex items-center gap-1.5 rounded-xl border border-neutral-700 bg-neutral-900 px-3 py-2 shadow-2xl dark:border-neutral-600 dark:bg-neutral-800">
122145
<div className="flex items-center gap-2 pr-1.5">
123-
<span className="flex size-6 items-center justify-center rounded-md bg-white text-xs font-semibold text-neutral-900 dark:bg-neutral-100">
124-
{selectedIds.length}
146+
<span className="flex h-6 min-w-6 items-center justify-center rounded-md bg-white px-1.5 text-xs font-semibold text-neutral-900 tabular-nums dark:bg-neutral-100">
147+
{displayCount.toLocaleString()}
125148
</span>
126149
<span className="text-sm font-medium text-neutral-300">
127150
selected
@@ -188,7 +211,7 @@ export function DataTableBulkActions({
188211
<p className="text-sm text-neutral-600 dark:text-neutral-400">
189212
This will affect{' '}
190213
<span className="font-medium text-neutral-900 dark:text-neutral-100">
191-
{selectedIds.length} {resourceName}
214+
{displayCount} {resourceName}
192215
</span>
193216
.
194217
</p>

resources/js/components/data-table/data-table.tsx

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback } from 'react';
1+
import { useCallback, useMemo } from 'react';
22
import {
33
DataTableEmptyState,
44
type EmptyStateConfig,
@@ -49,6 +49,8 @@ export function DataTable<T>({
4949
selectedIds = [],
5050
onSelectionChange,
5151
isRowSelectable,
52+
selectAllPages = false,
53+
onSelectAllPages,
5254
}: DataTableProps<T>) {
5355
const getRowIdValue = useCallback(
5456
(row: T, index: number): string | number => {
@@ -57,6 +59,23 @@ export function DataTable<T>({
5759
[getRowId],
5860
);
5961

62+
const effectiveSelectedIds = useMemo(() => {
63+
if (selectAllPages) {
64+
return data.map((row, index) => getRowIdValue(row, index));
65+
}
66+
return selectedIds;
67+
}, [selectAllPages, data, selectedIds, getRowIdValue]);
68+
69+
const handleSelectionChangeWrapper = useCallback(
70+
(ids: (string | number)[]) => {
71+
if (selectAllPages) {
72+
onSelectAllPages?.(false);
73+
}
74+
onSelectionChange?.(ids);
75+
},
76+
[selectAllPages, onSelectAllPages, onSelectionChange],
77+
);
78+
6079
const {
6180
allSelected,
6281
someSelected,
@@ -65,8 +84,8 @@ export function DataTable<T>({
6584
} = useDataTableSelection({
6685
data,
6786
selectable,
68-
selectedIds,
69-
onSelectionChange,
87+
selectedIds: effectiveSelectedIds,
88+
onSelectionChange: handleSelectionChangeWrapper,
7089
getRowIdValue,
7190
isRowSelectable,
7291
});
@@ -81,6 +100,16 @@ export function DataTable<T>({
81100
const totalPages =
82101
pagination && rowCount ? Math.ceil(rowCount / pagination.pageSize) : 0;
83102

103+
const totalColSpan =
104+
columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0);
105+
106+
const showSelectAllBanner =
107+
selectable &&
108+
allSelected &&
109+
!selectAllPages &&
110+
rowCount !== undefined &&
111+
rowCount > data.length;
112+
84113
const hasNoData = data.length === 0;
85114
const showPlaceholder = hasNoData && !isFiltered && !!placeholderData;
86115
const showNoResults = hasNoData && isFiltered;
@@ -146,6 +175,51 @@ export function DataTable<T>({
146175
thClassName={thClassName}
147176
/>
148177
<tbody>
178+
{(showSelectAllBanner || selectAllPages) && (
179+
<tr>
180+
<td
181+
colSpan={totalColSpan}
182+
className="border-b border-neutral-200 bg-muted/50 py-2 text-center text-sm text-neutral-600 dark:border-neutral-800 dark:text-neutral-400"
183+
>
184+
{selectAllPages ? (
185+
<>
186+
All{' '}
187+
<span className="font-medium text-neutral-900 dark:text-neutral-100">
188+
{rowCount?.toLocaleString()}
189+
</span>{' '}
190+
{resourceName(true)} are selected.{' '}
191+
<button
192+
type="button"
193+
className="font-medium text-primary hover:underline"
194+
onClick={() => {
195+
onSelectAllPages?.(false);
196+
onSelectionChange?.([]);
197+
}}
198+
>
199+
Clear selection
200+
</button>
201+
</>
202+
) : (
203+
<>
204+
All {data.length}{' '}
205+
{resourceName(data.length !== 1)} on
206+
this page are selected.{' '}
207+
<button
208+
type="button"
209+
className="font-medium text-primary hover:underline"
210+
onClick={() =>
211+
onSelectAllPages?.(true)
212+
}
213+
>
214+
Select all{' '}
215+
{rowCount?.toLocaleString()}{' '}
216+
{resourceName(true)}
217+
</button>
218+
</>
219+
)}
220+
</td>
221+
</tr>
222+
)}
149223
{tableData.map((row, rowIndex) => {
150224
const rowId = getRowIdValue(row, rowIndex);
151225

@@ -159,7 +233,8 @@ export function DataTable<T>({
159233
isClickable={isClickable}
160234
isSelected={
161235
selectable &&
162-
selectedIds.includes(rowId)
236+
(selectAllPages ||
237+
selectedIds.includes(rowId))
163238
}
164239
isSelectable={
165240
isRowSelectable?.(row) ?? true

resources/js/components/data-table/data-table.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,6 @@ export interface DataTableProps<T> {
5151
selectedIds?: (string | number)[];
5252
onSelectionChange?: (ids: (string | number)[]) => void;
5353
isRowSelectable?: (row: T) => boolean;
54+
selectAllPages?: boolean;
55+
onSelectAllPages?: (value: boolean) => void;
5456
}

src/Concerns/HasDataTable.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,14 @@ public function bulkAction(Request $request, string $action): mixed
276276
abort(404, "Bulk action [{$action}] not found.");
277277
}
278278

279-
$ids = $request->input('ids', []);
279+
if ($request->boolean('select_all')) {
280+
$model = $this->tableModel();
281+
$keyName = (new $model)->getKeyName();
282+
$ids = $this->tableQuery($request)->pluck($keyName)->all();
283+
} else {
284+
$ids = $request->input('ids', []);
285+
}
286+
280287
$records = $this->resolveBulkActionRecords($ids);
281288

282289
return ($bulkAction->getHandler())($records, $request);

0 commit comments

Comments
 (0)