Data Management Table
A feature-rich interface for managing data records, featuring search, status filtering, and row-level actions.
Demo
Implementation
vue
<script setup lang="ts">
import { ref, computed } from 'vue';
// 1. Component Imports (Default)
import CLTable from '@codeandfunction/callaloo/CLTable';
import CLInput from '@codeandfunction/callaloo/CLInput';
import CLSelect from '@codeandfunction/callaloo/CLSelect';
import CLPill from '@codeandfunction/callaloo/CLPill';
import CLButton from '@codeandfunction/callaloo/CLButton';
import CLHeading from '@codeandfunction/callaloo/CLHeading';
import CLText from '@codeandfunction/callaloo/CLText';
import CLCard from '@codeandfunction/callaloo/CLCard';
import { useToast } from '@codeandfunction/callaloo/composables/useToast';
import type { CLOption } from '@codeandfunction/callaloo';
// 2. Enum & Type Imports (Named)
import {
CLButtonTypes,
CLBorderRadius,
CLColorVariants,
CLColors,
CLHeadingLevels,
CLHeadingTypes,
CLIconNames,
CLTextTypes,
CLTableTypes
} from '@codeandfunction/callaloo';
type StatusFilter = UserStatus | 'all';
type UserStatus = 'active' | 'inactive' | 'pending';
interface UserRow {
email: string;
id: number;
lastLogin: string;
name: string;
role: string;
status: UserStatus;
}
// 3. State & Mock Data
const searchQuery = ref('');
const statusFilter = ref<StatusFilter>('all');
const toast = useToast();
const statusOptions: CLOption[] = [
{ label: 'All Statuses', value: 'all' },
{ label: 'Active', value: 'active' },
{ label: 'Pending', value: 'pending' },
{ label: 'Inactive', value: 'inactive' },
];
const users = ref<UserRow[]>([
{ email: '[email protected]', id: 1, lastLogin: '2 hours ago', name: 'Alice Freeman', role: 'Administrator', status: 'active' },
{ email: '[email protected]', id: 2, lastLogin: '5 hours ago', name: 'Bob Cordell', role: 'Editor', status: 'active' },
{ email: '[email protected]', id: 3, lastLogin: 'Never', name: 'Charlie Dean', role: 'Viewer', status: 'pending' },
{ email: '[email protected]', id: 4, lastLogin: '2 days ago', name: 'Diana Prince', role: 'Editor', status: 'inactive' },
{ email: '[email protected]', id: 5, lastLogin: '1 hour ago', name: 'Edward Norton', role: 'Administrator', status: 'active' },
]);
const filteredUsers = computed((): UserRow[] => {
return users.value.filter(user => {
const matchesSearch = user.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.value.toLowerCase());
const matchesStatus = statusFilter.value === 'all' || user.status === statusFilter.value;
return matchesSearch && matchesStatus;
});
});
const getStatusColor = (status: UserStatus): CLColors => {
switch (status) {
case 'active': return CLColors.Success;
case 'pending': return CLColors.Warning;
case 'inactive': return CLColors.Danger;
default: return CLColors.Neutral;
}
};
const handleAddUser = (): void => {
toast.showToast({
color: CLColors.Primary,
message: 'Opening new user form...',
title: 'Add User',
});
};
const handleEdit = (user: UserRow): void => {
toast.showToast({
color: CLColors.Primary,
message: `Opening editor for ${user.name}`,
title: 'Edit User',
});
};
</script>
<template>
<div class="pattern-preview">
<div class="header-section">
<CLHeading :level="CLHeadingLevels.H2" :type="CLHeadingTypes.Large">User Management</CLHeading>
<CLText :color="CLColors.Neutral" :type="CLTextTypes.Small">Manage system users, their roles, and account statuses.</CLText>
</div>
<div class="table-pattern">
<div class="toolbar">
<div class="filters">
<div class="search-input">
<CLInput
id="user-search"
name="user-search"
v-model="searchQuery"
placeholder="Search by name or email..."
:prefix="CLIconNames.Search"
fluid
/>
</div>
<div class="status-select">
<CLSelect
id="status-filter"
name="status-filter"
v-model="statusFilter"
:options="statusOptions"
fluid
/>
</div>
</div>
<CLButton
:color="CLColors.Primary"
:icon-before="CLIconNames.Plus"
:type="CLButtonTypes.Button"
@click="handleAddUser"
>
Add User
</CLButton>
</div>
<CLTable :col-widths="['180px', 'auto', 'auto', 'auto', 'auto']" :type="CLTableTypes.Default" striped>
<CLTable.Header>
<CLTable.Row>
<CLTable.Cell is-header>User</CLTable.Cell>
<CLTable.Cell is-header>Role</CLTable.Cell>
<CLTable.Cell is-header>Status</CLTable.Cell>
<CLTable.Cell is-header>Last Login</CLTable.Cell>
<CLTable.Cell is-header></CLTable.Cell>
</CLTable.Row>
</CLTable.Header>
<CLTable.Body>
<CLTable.Row v-for="user in filteredUsers" :key="user.id">
<CLTable.Cell>
{{ user.name }}
<CLTable.NestedCell>{{ user.email }}</CLTable.NestedCell>
</CLTable.Cell>
<CLTable.Cell>
<CLText :type="CLTextTypes.Small" truncate>{{ user.role }}</CLText>
</CLTable.Cell>
<CLTable.Cell>
<CLPill
:color="getStatusColor(user.status)"
:label="user.status"
:variant="CLColorVariants.Soft"
/>
</CLTable.Cell>
<CLTable.Cell>
<CLText :type="CLTextTypes.Small" truncate>{{ user.lastLogin }}</CLText>
</CLTable.Cell>
<CLTable.Cell>
<div class="actions-cell">
<CLButton
:variant="CLColorVariants.Ghost"
:border-radius="CLBorderRadius.Full"
:color="CLColors.Neutral"
:icon-before="CLIconNames.EditPencil"
:type="CLButtonTypes.Button"
:aria-label="`Edit ${user.name}`"
@click="handleEdit(user)"
/>
</div>
</CLTable.Cell>
</CLTable.Row>
<CLTable.Row v-if="filteredUsers.length === 0">
<CLTable.Cell colspan="5">
<div class="empty-row">
<CLText :color="CLColors.Neutral">No users found matching your criteria.</CLText>
</div>
</CLTable.Cell>
</CLTable.Row>
</CLTable.Body>
</CLTable>
</div>
</div>
</template>
<style scoped>
.pattern-preview {
display: flex;
flex-direction: column;
gap: 2rem;
}
.table-pattern {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.toolbar {
display: flex;
flex-direction: column;
gap: 1rem;
}
@media (min-width: 640px) {
.toolbar {
flex-direction: row;
align-items: flex-end;
justify-content: space-between;
}
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 1rem;
flex: 1;
}
.search-input {
min-width: 250px;
flex: 1;
}
.status-select {
min-width: 180px;
}
.actions-cell {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.empty-row {
padding: 2rem;
text-align: center;
}
</style>