diff --git a/.storybook/storybook.css b/.storybook/storybook.css index ef88592e72..6cdf7c8c4c 100644 --- a/.storybook/storybook.css +++ b/.storybook/storybook.css @@ -85,6 +85,14 @@ p:not(.sb-anchor, .sb-unstyled, .sb-unstyled p) { font-weight: 300; } + + ul { + list-style: circle; + } + + ol { + list-style: decimal; + } } } diff --git a/resources/js/components/ui/Listing/Listing.vue b/resources/js/components/ui/Listing/Listing.vue index 798c517433..2b7218abe3 100644 --- a/resources/js/components/ui/Listing/Listing.vue +++ b/resources/js/components/ui/Listing/Listing.vue @@ -5,7 +5,11 @@ export const [injectListingContext, provideListingContext] = createContext('List diff --git a/resources/js/stories/Listing.stories.ts b/resources/js/stories/Listing.stories.ts index 81cec4d013..5bb3c9f0bd 100644 --- a/resources/js/stories/Listing.stories.ts +++ b/resources/js/stories/Listing.stories.ts @@ -14,7 +14,9 @@ import { ListingTable, ListingTableBody, ListingTableHead, - ListingToggleAll + ListingToggleAll, + Panel, + PanelFooter, } from '@ui'; const meta = { @@ -208,4 +210,65 @@ export const _WithActions: Story = { components: { Listing, Badge, DropdownItem }, template: actionsCode, }), +}; + +const customLayoutCode = ` ++
+ + + +
+ + + + + + + + +
+`; + +export const _CustomLayout: Story = { + tags: ['!dev'], + parameters: { + docs: { + source: { code: customLayoutCode } + } + }, + render: () => ({ + components: { + Listing, + ListingPresets, + ListingSearch, + ListingFilters, + ListingCustomizeColumns, + ListingTable, + ListingPagination, + Panel, + PanelFooter, + Badge, + DropdownItem, + }, + template: customLayoutCode, + }), }; \ No newline at end of file diff --git a/resources/js/stories/docs/Listing.mdx b/resources/js/stories/docs/Listing.mdx index dd2fe0d3c1..d044fd9c3a 100644 --- a/resources/js/stories/docs/Listing.mdx +++ b/resources/js/stories/docs/Listing.mdx @@ -9,14 +9,157 @@ A full-featured data listing component with sorting, searching, pagination, colu ## Customizing the cells Customize how individual cells are rendered using cell slots. Use the pattern `#cell-{fieldName}` to target specific columns. + +The cell slots expose a few props: +- `row` - the current row's data +- `value` - the current column's value +- `isColumnVisible(column)` - determine whether a column is currently visible + ## With Actions You can add your own dropdown items via the `prepended-row-actions` slot. +## Customizing the layout +The `Listing` component includes a sensible default layout, but you can fully control the structure by providing your own markup in the default slot. + +Inside the slot, you can compose your layout using the provided `Listing` sub-components, which automatically connect to the parent `Listing` via shared context. + +If you need to customize table columns or row actions, you can do so inside the `ListingTable` component's slots. + + + +### Available components +You can use the following components to build your own layout: + +- `ListingCustomizeColumns` +- `ListingFilters` +- `ListingPagination` +- `ListingPresets` +- `ListingSearch` +- `ListingTable` + +### Slot props +The default slot exposes a few props: + +- `items` - the items for the current page +- `isColumnVisible(column)` - determine whether a column is currently visible +- `loading` - whether results are being fetched from the server + ## Using a JSON endpoint -The `Listing` component can pull data from a JSON endpoint. Simply specify a `url` and return an [API Resource](https://laravel.com/docs/12.x/eloquent-resources#main-content) from your controller. +The `Listing` component can pull data from a JSON endpoint. + +1. You'll need to make a controller to query your data. The Listing component will provide `page`, `perPage`, `sort`, `order`, `search`, `columns` and `filters` as query parameters. + + You may also want to make a [JSON resource](https://laravel.com/docs/master/eloquent-resources#main-content) to have more control over the response. + + ```php + use Illuminate\Http\Request; + + public function json(Request $request) + { + $query = TeamMember::query(); + + if ($search $request->input('search')) { + $query->where('name', 'like', "%{$search}%"); + } + + $query->orderBy( + $request->input('sort', 'name'), // column + $request->input('order', 'asc') // direction + ); + + return TeamMemberResource::collection($query->paginate($request->input('perPage'))); + } + ``` + +2. Create a route and provide the `url` prop instead of `items`: + + ```php + Route::get('team-members/json', [TeamMemberController::class, 'json']); + ``` + + ```vue + + ``` + +### Actions +To enable [actions](https://statamic.dev/backend-apis/actions), you'll need to make another controller. It should extend Statamic's `ActionController`. + +The `$key` should match what's returned by [action's `visibleTo` method](https://statamic.dev/backend-apis/actions#filtering-actions). Inside `getSelectedItems`, you should lookup items via their IDs: + +```php +map(function ($item) { + return TeamMember::find($item); + }); + } +} +``` + +Next, add two routes pointing to the controller: + +```php +Route::get('team-members/actions', [TeamMemberActionController::class, 'run']); +Route::get('team-members/actions/list', [TeamMemberActionController::class, 'bulkActions']); +``` + +Finally, pass the URL for the first route to the `actionUrl` prop: + +```vue ++``` + +### Filtering +To enable filters on your listing, you'll need to call `Scope::filters()` in your controller and pass it to the `Listing` component on your page. + +The `Scope::filters()` method expects a key (which should match what's returned by the [filter's `visibleTo` method](https://statamic.dev/backend-apis/query-scopes-and-filters#filters) along with any additional context you wish to provide. + +```php +return Interia::render('team-members/Index', [ + 'filters' => Scope::filters('team-members', [ + 'company' => 'Statamic', + ]), + // ... +]); +``` + +```vue + + + +``` ## Arguments