Skip to content

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>

Released under the MIT License.