@shifl-inc/ui
v0.9.17
Published
Reusable Vue 3 UI components for Shifl.
Maintainers
Readme
Shifl Grid (Vue 3)
JSON-driven data grid for Shifl UI, built with Vue 3 + TypeScript. Library-first output via Vite (library mode) with Tailwind + CSS variables for theming.
Status
- ✅ Minimal grid (column visibility toggle, client-side sorting, global filter)
- ✅ API integration with server-side data fetching
- ✅ Pagination, search, filters, and sorting via API
- 🚧 Everything else scaffolded/stubbed for incremental delivery (editing, virtual scroll, tours, views)
Requirements
- Node
>=20.11.0 - Peer deps:
vue@^3.4.0
Install
npm install @shifl-inc/uiUsage
Setup
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { ShiflGridPlugin } from '@shifl-inc/ui';
import '@shifl-inc/ui/style.css';
const app = createApp(App);
app.use(ShiflGridPlugin);
app.mount('#app');Client-Side Mode (Static Data)
For static data that doesn't require API calls:
<script setup lang="ts">
import { ShiflGrid } from '@shifl-inc/ui';
import type { GridConfig } from '@shifl-inc/ui';
const config: GridConfig = {
name: 'My Preferred Shipments',
columns: [
{ key: 'shifl_ref', label: 'Shifl Ref #', sortable: true, freeze: true },
{ key: 'company_name', label: 'Customer', sortable: true }
],
search: ['shifl_ref', 'company_name'],
sort: { key: 'shifl_ref', order: 'asc' },
rows: [
{ shifl_ref: 'SF-1001', company_name: 'GMA Inc' },
{ shifl_ref: 'SF-1002', company_name: 'Walads Autowash' }
],
paginationMeta: {
show: true,
currentPage: 1,
perPage: 20,
total: 2
}
};
</script>
<template>
<ShiflGrid :config="config" />
</template>Server-Side Mode (API Integration)
For dynamic data fetched from an API:
<script setup lang="ts">
import { ShiflGrid } from '@shifl-inc/ui';
import type { GridConfig } from '@shifl-inc/ui';
const config: GridConfig = {
name: 'My Preferred Shipments',
apiConfig: {
apiUrl: 'https://api.example.com/shipments',
method: 'GET', // Optional: 'GET' | 'POST' | 'PUT' (default: 'GET')
getAuthToken: () => localStorage.getItem('token'), // Optional: function to get auth token
headers: {
// Optional: additional headers
'X-Custom-Header': 'value'
},
additionalParams: {
// Optional: extra params to send with requests
module: 'shipments',
customerId: 123
},
debounceMs: 300, // Optional: debounce search queries (default: 300ms)
responseTransformer: <T,>(response: unknown) => {
// Optional: transform API response to expected format
const res = response as {
data?: T[];
meta?: {
current_page?: number;
per_page?: number;
total?: number;
};
};
return {
data: res.data || [],
meta: res.meta
? {
currentPage: res.meta.current_page,
perPage: res.meta.per_page,
total: res.meta.total,
show: true
}
: undefined
};
}
},
columns: [
{ key: 'shifl_ref', label: 'Shifl Ref #', sortable: true, freeze: true },
{ key: 'company_name', label: 'Customer', sortable: true }
],
search: ['shifl_ref', 'company_name'],
paginationMeta: {
show: true,
currentPage: 1,
perPage: 20,
total: 0 // Will be updated from API response
}
};
</script>
<template>
<ShiflGrid :config="config" />
</template>Configuration Options
GridConfig
interface GridConfig<T = Record<string, unknown>> {
id?: string;
name?: string;
customerId?: number;
module?: string;
resourceId?: string | number | null;
creatorId?: number;
pageLocation?: string;
view?: 'regular' | 'compact' | 'comfortable';
// API Configuration (for server-side mode)
apiConfig?: {
apiUrl: string; // Required: API endpoint URL
method?: 'GET' | 'POST' | 'PUT'; // Optional: HTTP method (default: 'GET')
getAuthToken?: () => string | undefined; // Optional: function to get auth token
headers?: Record<string, string>; // Optional: additional headers
additionalParams?: Record<string, unknown> | ((context) => Record<string, unknown>); // Optional: extra params
paramsBuilder?: (context) => URLSearchParams | Record<string, unknown>; // Optional: custom query params
bodyBuilder?: (context) => unknown; // Optional: custom request body
responseTransformer?: <T>(response: unknown) => ApiResponse<T>; // Optional: transform API response
debounceMs?: number; // Optional: search debounce delay (default: 300ms)
};
columns: GridColumn[];
search?: string[]; // Searchable column keys (for client-side filtering)
searchMode?: 'client' | 'server'; // Auto-set to 'server' if apiConfig is provided
filters?: GridFilter[];
sort?: GridSort;
paginationMeta?: PaginationMeta;
rows?: T[]; // Static data (for client-side mode)
}Client-Side Mode Features
- Sorting: Client-side only; click column headers to toggle asc/desc
- Global filter: Simple case-insensitive substring search across listed
searchfields - Visibility: Toggle columns via pills above the grid
- Pagination: Client-side pagination of static data
Server-Side Mode Features
- Sorting: Server-side; click column headers to sort via API
- Search: Debounced search queries sent to API (default: 300ms delay)
- Filters: Filter values sent as query parameters or in request body
- Pagination: Handled by server; pagination metadata from API response
- Authentication: Optional bearer token via
getAuthTokenfunction - Custom params: Use
additionalParamsto send extra parameters - Pagination state: User's selected page is the source of truth; API's
currentPageis informational only
API Response Formats
Default Response Format (Recommended)
The grid works out-of-the-box with this format:
{
"data": [
{ "id": 1, "name": "Item 1", "status": "active" },
{ "id": 2, "name": "Item 2", "status": "inactive" }
],
"meta": {
"currentPage": 1,
"perPage": 20,
"total": 100,
"lastPage": 5,
"from": 1,
"to": 20,
"show": true
}
}Alternative Default Formats
The default transformer also supports these formats:
Format 1: Using rows instead of data
{
"rows": [
{ "id": 1, "name": "Item 1" },
{ "id": 2, "name": "Item 2" }
],
"meta": {
"currentPage": 1,
"perPage": 20,
"total": 100
}
}Format 2: Data only (no pagination)
{
"data": [
{ "id": 1, "name": "Item 1" },
{ "id": 2, "name": "Item 2" }
]
}Required Fields
The grid requires these fields in meta for proper pagination:
Required for pagination to work:
perPage(orper_pagein Laravel format) - number of items per pagetotal(total number of records) - used to calculate total pages
Optional but recommended:
currentPage(orcurrent_pagein Laravel format) - informational only, does not override user's selected pagelastPage(orlast_page) - for better UXfrom- starting record numberto- ending record numbershow- whether to show pagination (default:true)
Important: Pagination State Behavior
The grid uses the user's pagination state as the source of truth. When a user clicks page 2, the grid will:
- Display page 2 as active in the UI
- Send
page=2in the API request - Show the correct active page even if the API response returns
currentPage: 1
This behavior ensures the pagination UI remains correct even when using mock/dummy APIs that always return currentPage: 1. The API's currentPage value is only used for informational purposes and does not override the user's selection.
Custom Response Transformers
If your API uses a different response format, use the responseTransformer option:
Laravel-Style Pagination Format
const config: GridConfig = {
apiConfig: {
apiUrl: 'https://api.example.com/data',
getAuthToken: () => localStorage.getItem('token'),
responseTransformer: <T>(response: unknown) => {
const res = response as {
data?: T[];
meta?: {
current_page?: number;
per_page?: number;
total?: number;
last_page?: number;
from?: number;
to?: number;
};
};
return {
data: res.data || [],
meta: res.meta
? {
currentPage: res.meta.current_page,
perPage: res.meta.per_page,
total: res.meta.total,
lastPage: res.meta.last_page,
from: res.meta.from,
to: res.meta.to,
show: true
}
: undefined
};
}
}
// ... rest of config
};Laravel Response Example:
{
"data": [{ "id": 1, "name": "Item 1" }],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 100,
"last_page": 5,
"from": 1,
"to": 20
}
}REST API Standard Format
const config: GridConfig = {
apiConfig: {
apiUrl: 'https://api.example.com/data',
responseTransformer: <T>(response: unknown) => {
const res = response as {
results?: T[];
items?: T[];
count?: number;
page?: number;
pageSize?: number;
};
return {
data: res.results || res.items || [],
meta: {
currentPage: res.page || 1,
perPage: res.pageSize || 20,
total: res.count || 0,
show: true
}
};
}
}
// ... rest of config
};REST Response Example:
{
"results": [{ "id": 1, "name": "Item 1" }],
"count": 100,
"page": 1,
"pageSize": 20
}GraphQL-Style Format
const config: GridConfig = {
apiConfig: {
apiUrl: 'https://api.example.com/graphql',
responseTransformer: <T>(response: unknown) => {
const res = response as {
data?: {
items?: T[];
pagination?: {
page: number;
pageSize: number;
totalCount: number;
};
};
};
return {
data: res.data?.items || [],
meta: res.data?.pagination
? {
currentPage: res.data.pagination.page,
perPage: res.data.pagination.pageSize,
total: res.data.pagination.totalCount,
show: true
}
: undefined
};
}
}
// ... rest of config
};GraphQL Response Example:
{
"data": {
"items": [{ "id": 1, "name": "Item 1" }],
"pagination": {
"page": 1,
"pageSize": 20,
"totalCount": 100
}
}
}Request Parameters Sent by Grid
The grid automatically sends these parameters:
GET Request (Query Parameters)
GET /api/data?search=query&page=1&per_page=20&sort=name&order=asc&filters[status]=active&filters[type][]=type1&filters[type][]=type2POST/PUT Request (Body)
{
"search": "query",
"page": 1,
"per_page": 20,
"sort": "name",
"order": "asc",
"filters": {
"status": "active",
"type": ["type1", "type2"]
},
"module": "shipments",
"customerId": 123
}Error Handling
The grid handles errors gracefully. Your API can return standard HTTP error responses:
{
"error": "Unauthorized",
"message": "Invalid authentication token"
}Or with status codes:
400- Bad Request401- Unauthorized403- Forbidden404- Not Found500- Internal Server Error
The grid will display the error and stop loading.
Scripts
npm run dev— Vite dev servernpm run build— Library build + d.ts viavite-plugin-dtsnpm run lint— ESLint (Vue + TS) flat confignpm run test— Vitest (jsdom)npm run storybook— Storybook devnpm run storybook:build— Storybook static build
Project structure (high level)
src/components/grid—ShiflGrid.vue(minimal implementation)src/composables— Core behaviors (sort/filter/columns implemented, others stubbed/TODO)src/types— Grid + theme contractssrc/theme— CSS tokenssrc/styles— Tailwind entry + component stylessrc/utils— Helpers (config normalization)src/plugins— Vue install pluginstories/— Storybook stories.storybook/— Storybook configuration
API Integration
The grid supports server-side data fetching with automatic API calls for search, filters, sorting, and pagination. When apiConfig is provided, the grid automatically:
- Fetches data from your API endpoint
- Sends search, filter, sort, and pagination parameters
- Handles loading states and errors
- Updates the grid when parameters change
- Supports bearer token authentication
- Debounces search queries to reduce API calls
- Uses user's pagination state as source of truth - the active page displayed is based on user selection, not the API's
currentPageresponse
See API Response Formats for detailed information about:
- Expected API response formats
- Custom response transformers
- Request parameters sent by the grid
- Error handling
- Pagination behavior and requirements
Notes
- Virtual scroll, editing, tours, saved/recommended views, telemetry are intentionally stubbed for later milestones.
