uiXpress
Api

Dashboard Cards API

Create custom dashboard cards using Vue.js or React components with the extensible dashboard card system

Table of Contents

Overview

The dashboard uses a 12-column CSS Grid layout where cards can span 1-12 columns. Cards are self-contained components (Vue or React) that receive data and handle their own refresh logic. The dashboard supports both Vue.js 3 and React components, allowing developers to choose their preferred framework.

Key Features

  • Flexible Grid System: 12-column grid with dynamic column spans
  • Self-Contained Cards: Each card manages its own data and refresh logic
  • Capability-Based Access: Control card visibility based on user permissions
  • Recursive Groups: Organize cards into nested groups
  • Date Range Integration: Cards automatically receive date range changes
  • Extensible API: Easy integration for external plugins

Card Structure

Each card consists of three main files. The component file extension depends on the framework you choose:

Vue Card Structure:

your-card/
├── metadata.js      # Card configuration
├── component.vue    # Vue component
└── index.js         # Export file

React Card Structure:

your-card/
├── metadata.js      # Card configuration
├── component.jsx    # React component
└── index.js         # Export file

File Structure

metadata.js

export default {
  id: 'unique-card-id',
  title: 'Card Title',
  description: 'Card description',
  width: 4, // 1-12 columns
  language: 'vue', // 'vue' or 'react'
  requires_capabilities: ['manage_options'], // Optional
};

component.vue (Vue Component)

<script setup>
import { ref, computed, onMounted, watch } from 'vue';

const props = defineProps({
  dateRange: {
    type: Array,
    required: true,
  },
  appData: {
    type: Object,
    required: true,
  },
  id: {
    type: String,
    required: true,
  },
});

// Your component logic here
</script>

<template>
  <!-- Your card template -->
</template>

component.jsx (React Component)

import React, { useState, useEffect } from 'react';

/**
 * Your React Card Component
 * 
 * @param {Object} props - Component props
 * @param {Array} props.dateRange - Array of two dates [startDate, endDate]
 * @param {Object} props.appData - Vue app store instance
 * @param {string} props.id - Card ID from metadata
 */
const YourCard = ({ dateRange, appData, id }) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // Load data when component mounts
    loadData();
  }, []);

  useEffect(() => {
    // Reload data when dateRange changes
    loadData();
  }, [dateRange]);

  const loadData = async () => {
    setLoading(true);
    try {
      // Your API call here
      // const response = await fetch(...);
      // setData(response);
    } catch (error) {
      console.error('Error loading data:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="bg-zinc-50 dark:bg-zinc-800/20 border border-zinc-200/40 dark:border-zinc-800/60 rounded-3xl p-6">
      {/* Your card content */}
    </div>
  );
};

export default YourCard;

index.js

For Vue Components:

import metadata from './metadata.js';
import component from './component.vue';

export default {
  metadata,
  component,
};

For React Components:

import metadata from './metadata.js';
import component from './component.jsx';

export default {
  metadata,
  component,
};

Metadata Configuration

Required Properties

PropertyTypeDescriptionDefault
idstringUnique identifier for the cardRequired
titlestringDisplay title for the cardRequired
descriptionstringBrief description of the cardRequired
widthnumberNumber of columns (1-12) the card spansRequired
categorystringDashboard category (e.g., 'site', 'analytics')Required
languagestringComponent language ('vue' or 'react')'vue'
mobileWidthnumberNumber of columns on mobile (1-12)12

Optional Properties

PropertyTypeDescription
requires_capabilitiesarrayWordPress capabilities required to view the card

Example Metadata

Vue Card Example:

export default {
  id: 'user-analytics',
  title: 'User Analytics',
  description: 'Track user engagement and activity',
  width: 6,
  mobileWidth: 12,
  language: 'vue',
  category: 'site',
  requires_capabilities: ['manage_options', 'edit_users'],
};

React Card Example:

export default {
  id: 'react-analytics',
  title: 'React Analytics',
  description: 'Analytics built with React',
  width: 6,
  mobileWidth: 12,
  language: 'react', // Important: Set to 'react' for React components
  category: 'analytics',
  requires_capabilities: ['manage_options'],
};

Component Development

Props

Your component receives three props:

dateRange (Array)

An array containing two Date objects:

[
  Date,  // Start date of selected range
  Date   // End date of selected range
]

Vue Example:

const startDate = props.dateRange[0].toISOString();
const endDate = props.dateRange[1].toISOString();

React Example:

const startDate = dateRange[0].toISOString();
const endDate = dateRange[1].toISOString();

appData (Object)

The Vue app store instance containing:

{
  state: {
    adminUrl: string,           // WordPress admin URL
    siteURL: string,            // Site URL
    currentUser: object,       // Current user data
    uixpress_settings: object, // Plugin settings
    // ... other app state
  }
}

id (String)

The card ID from metadata, useful for unique identifiers or debugging.

Note for React Components: Veaury automatically converts Vue's kebab-case props (date-range, app-data) to React's camelCase (dateRange, appData). However, it's recommended to handle both formats for maximum compatibility:

const dateRange = props.dateRange || props['date-range'];
const appData = props.appData || props['app-data'];

Component Lifecycle

<script setup>
import { ref, computed, onMounted, watch } from 'vue';

const props = defineProps({
  dateRange: { type: Object, required: true },
  appData: { type: Object, required: true },
});

// Component state
const data = ref([]);
const loading = ref(false);
const error = ref(null);

// Load data function
const loadData = async () => {
  loading.value = true;
  try {
    // Your API call here
    const response = await fetch('/wp-json/wp/v2/posts');
    data.value = await response.json();
  } catch (err) {
    error.value = err.message;
  } finally {
    loading.value = false;
  }
};

// Watch for date range changes
watch(
  () => props.dateRange,
  () => {
    loadData();
  },
  { deep: true }
);

// Watch for app data changes
watch(
  () => props.appData,
  () => {
    // React to app data changes
  },
  { deep: true }
);

// Load data on mount
onMounted(() => {
  loadData();
});
</script>

Registration Methods

Internal Cards (Plugin Development)

For cards within the UIXpress plugin:

// In app/src/pages/dashboard/src/cards/index.js
import { addFilter } from '@/assets/js/functions/HooksSystem.js';
import myCard from './my-card/index.js';

addFilter('uixpress/dashboard/cards/register', (widgets) => {
  return [...widgets, myCard];
});

External Cards (Other Plugins)

For external plugins, listen for the uixpress/dashboard/ready event and use the global window.uixpress API:

// In your external plugin
document.addEventListener("uixpress/dashboard/ready", () => {
  const { addFilter } = window.uixpress;

  // Add a new card
  addFilter("uixpress/dashboard/cards/register", (widgets) => {
    return [...widgets, myExternalCard];
  });
});

Filtering/Removing Cards

You can filter out existing cards by returning a filtered array:

document.addEventListener("uixpress/dashboard/ready", () => {
  const { addFilter } = window.uixpress;

  // Remove a specific card by ID
  addFilter("uixpress/dashboard/cards/register", (widgets) => {
    return widgets.filter((widget) => {
      return !(widget.metadata && widget.metadata.id === "server-health");
    });
  });
});

Categories

Dashboard cards are organized into categories. You can manipulate the categories using the uixpress/dashboard/categories/register filter.

Modifying Categories

document.addEventListener("uixpress/dashboard/ready", () => {
  const { addFilter } = window.uixpress;

  // Sort categories (e.g., put Analytics first)
  addFilter("uixpress/dashboard/categories/register", (categories) => {
    return categories.sort((a, b) =>
      a.value === "analytics" ? -1 : b.value === "analytics" ? 1 : 0
    );
  });
});

Adding Custom Categories

document.addEventListener("uixpress/dashboard/ready", () => {
  const { addFilter } = window.uixpress;

  addFilter("uixpress/dashboard/categories/register", (categories) => {
    return [
      ...categories,
      {
        label: "My Custom Category",
        value: "my-category",
      },
    ];
  });
});

Removing Categories

document.addEventListener("uixpress/dashboard/ready", () => {
  const { addFilter } = window.uixpress;

  addFilter("uixpress/dashboard/categories/register", (categories) => {
    return categories.filter((category) => category.value !== "analytics");
  });
});

Complete Example

Here's a complete example showing how to manipulate both categories and cards:

document.addEventListener("uixpress/dashboard/ready", () => {
  const { addFilter } = window.uixpress;

  // Reorder categories - put Analytics first
  addFilter("uixpress/dashboard/categories/register", (categories) => {
    return categories.sort((a, b) =>
      a.value === "analytics" ? -1 : b.value === "analytics" ? 1 : 0
    );
  });

  // Filter out specific cards
  addFilter("uixpress/dashboard/cards/register", (widgets) => {
    return widgets.filter((widget) => {
      return !(widget.metadata && widget.metadata.id === "server-health");
    });
  });
});

Groups and Nesting

Creating Groups

Groups allow you to organize related cards:

const analyticsGroup = {
  metadata: {
    id: 'analytics-group',
    title: 'Analytics',
    width: 6,
    columns: 2, // Number of columns in the group
  },
  isGroup: true,
  children: [card1, card2, card3],
};

// Register the group
addFilter('uixpress/dashboard/cards/register', (widgets) => {
  return [...widgets, analyticsGroup];
});

Nested Groups

Groups can contain other groups:

const mainGroup = {
  metadata: {
    id: 'main-group',
    title: 'Main Dashboard',
    width: 8,
    columns: 4,
  },
  isGroup: true,
  children: [analyticsGroup, anotherGroup, standaloneCard],
};

Capability Requirements

Control card visibility based on user permissions:

export default {
  id: 'admin-only-card',
  title: 'Admin Only',
  description: 'Only visible to administrators',
  width: 4,
  language: 'vue',
  requires_capabilities: ['manage_options'],
};

Multiple Capabilities

export default {
  id: 'editor-card',
  title: 'Editor Card',
  description: 'Visible to editors and admins',
  width: 4,
  language: 'vue',
  requires_capabilities: ['edit_posts', 'manage_options'],
};

React Component Development

React Component Example

Here's a complete example of a React dashboard card:

metadata.js:

export default {
  id: 'react-test-card',
  title: 'React Test Card',
  description: 'A test card built with React',
  width: 4,
  mobileWidth: 12,
  language: 'react', // Important: Set to 'react'
  category: 'site',
};

component.jsx:

import React, { useState, useEffect, useMemo } from 'react';

/**
 * React Test Card Component
 * 
 * @param {Object} props - Component props
 * @param {Array} props.dateRange - Array of two dates [startDate, endDate]
 * @param {Object} props.appData - Vue app store instance
 * @param {string} props.id - Card ID from metadata
 */
const ReactTestCard = (props) => {
  // Handle both camelCase and kebab-case prop names (veaury compatibility)
  const dateRange = props.dateRange || props['date-range'];
  const appData = props.appData || props['app-data'];
  const id = props.id;

  const [count, setCount] = useState(0);
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  /**
   * Normalizes a date value to a Date object
   */
  const normalizeDate = (date) => {
    if (!date) return null;
    if (date instanceof Date) return date;
    if (typeof date === 'string' || typeof date === 'number') {
      const d = new Date(date);
      return isNaN(d.getTime()) ? null : d;
    }
    return null;
  };

  /**
   * Calculates days between two dates
   */
  const getDaysBetween = (start, end) => {
    const startDate = normalizeDate(start);
    const endDate = normalizeDate(end);
    if (!startDate || !endDate) return 0;
    const diffTime = Math.abs(endDate.getTime() - startDate.getTime());
    return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
  };

  // Normalize dateRange and calculate days
  const normalizedDateRange = useMemo(() => {
    if (!dateRange || !Array.isArray(dateRange) || dateRange.length !== 2) {
      return null;
    }
    const start = normalizeDate(dateRange[0]);
    const end = normalizeDate(dateRange[1]);
    if (!start || !end) return null;
    return [start, end];
  }, [dateRange]);

  const daysInRange = normalizedDateRange
    ? getDaysBetween(normalizedDateRange[0], normalizedDateRange[1])
    : 0;

  useEffect(() => {
    // Load data when component mounts or dateRange changes
    loadData();
  }, [dateRange]);

  const loadData = async () => {
    setLoading(true);
    try {
      // Your API call here
      // Example:
      // const response = await fetch('/wp-json/wp/v2/posts');
      // setData(await response.json());
    } catch (error) {
      console.error('Error loading data:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="bg-zinc-50 dark:bg-zinc-800/20 border border-zinc-200/40 dark:border-zinc-800/60 rounded-3xl p-6 flex flex-col">
      <div className="flex items-center justify-between mb-6">
        <div>
          <h3 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
            React Test Card
          </h3>
          <p className="text-sm text-zinc-500 dark:text-zinc-400">
            Built with React
          </p>
        </div>
      </div>

      {loading ? (
        <div className="text-center py-4">Loading...</div>
      ) : (
        <div className="space-y-4">
          <div className="text-center">
            <div className="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
              {daysInRange}
            </div>
            <div className="text-xs text-zinc-500 dark:text-zinc-400">
              Days Selected
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

export default ReactTestCard;

index.js:

import metadata from './metadata.js';
import component from './component.jsx';

export default {
  metadata,
  component,
};

React Component Best Practices

  1. Prop Handling: Always handle both camelCase and kebab-case prop names for veaury compatibility
  2. Date Normalization: Normalize date values since they may come as Date objects, strings, or numbers
  3. useMemo for Computed Values: Use useMemo for expensive calculations based on props
  4. useEffect for Side Effects: Use useEffect to watch for prop changes and reload data
  5. Tailwind Classes: Use Tailwind CSS classes (same as Vue components) for consistent styling
  6. Error Handling: Always include error handling for API calls and data processing

Vue vs React: When to Use Which?

  • Use Vue if you're already familiar with Vue.js or want consistency with the dashboard's primary framework
  • Use React if you have existing React components or prefer React's ecosystem
  • Both frameworks work seamlessly together in the same dashboard
  • Performance is equivalent - choose based on your preference or existing codebase

Examples

Basic Vue Card

// metadata.js
export default {
  id: 'simple-stats',
  title: 'Simple Stats',
  description: 'Display basic statistics',
  width: 3,
  language: 'vue',
};
<!-- component.vue -->
<script setup>
import { ref, onMounted } from 'vue';

const props = defineProps({
  dateRange: { type: Object, required: true },
  appData: { type: Object, required: true },
});

const stats = ref({
  total: 0,
  active: 0,
  inactive: 0,
});

const loadStats = async () => {
  // Your API call here
  stats.value = {
    total: 100,
    active: 85,
    inactive: 15,
  };
};

onMounted(() => {
  loadStats();
});
</script>

<template>
  <div class="p-4">
    <h3 class="text-lg font-semibold mb-4">Simple Stats</h3>
    <div class="grid grid-cols-3 gap-4">
      <div class="text-center">
        <div class="text-2xl font-bold">{{ stats.total }}</div>
        <div class="text-sm text-gray-500">Total</div>
      </div>
      <div class="text-center">
        <div class="text-2xl font-bold text-green-600">{{ stats.active }}</div>
        <div class="text-sm text-gray-500">Active</div>
      </div>
      <div class="text-center">
        <div class="text-2xl font-bold text-red-600">{{ stats.inactive }}</div>
        <div class="text-sm text-gray-500">Inactive</div>
      </div>
    </div>
  </div>
</template>

Card with WordPress REST API

<script setup>
import { ref, onMounted, watch } from 'vue';
import { lmnFetch } from '@/assets/js/functions/lmnFetch.js';

const props = defineProps({
  dateRange: { type: Object, required: true },
  appData: { type: Object, required: true },
});

const posts = ref([]);
const loading = ref(false);

const loadPosts = async () => {
  loading.value = true;
  try {
    const response = await lmnFetch('/wp/v2/posts', {
      method: 'GET',
      params: {
        per_page: 5,
        after: props.dateRange.startDate.toISOString(),
        before: props.dateRange.endDate.toISOString(),
      },
    });
    posts.value = response;
  } catch (error) {
    console.error('Failed to load posts:', error);
  } finally {
    loading.value = false;
  }
};

watch(() => props.dateRange, loadPosts, { deep: true });
onMounted(loadPosts);
</script>

<template>
  <div class="p-4">
    <h3 class="text-lg font-semibold mb-4">Recent Posts</h3>

    <div v-if="loading" class="text-center py-4">Loading...</div>

    <div v-else-if="posts.length === 0" class="text-center py-4 text-gray-500">
      No posts found
    </div>

    <div v-else class="space-y-3">
      <div
        v-for="post in posts"
        :key="post.id"
        class="p-3 bg-gray-50 rounded-lg"
      >
        <h4 class="font-medium">{{ post.title.rendered }}</h4>
        <p class="text-sm text-gray-500">{{ post.date }}</p>
      </div>
    </div>
  </div>
</template>

External Plugin Integration

Vue Component Example:

// In your external plugin
(function () {
  'use strict';

  // Wait for UIXpress dashboard to be ready
  document.addEventListener('uixpress/dashboard/ready', function () {
    const { addFilter } = window.uixpress;

    // Define your Vue card
    const myExternalCard = {
      metadata: {
        id: 'my-plugin-card',
        title: 'My Plugin Data',
        description: 'Data from my plugin',
        width: 4,
        language: 'vue',
        category: 'site',
        requires_capabilities: ['manage_options'],
      },
      component: {
        // Your Vue component definition
        template: `
          <div class="p-4">
            <h3>My Plugin Card</h3>
            <p>This card is from an external plugin!</p>
          </div>
        `,
        props: ['dateRange', 'appData', 'id'],
      },
    };

    // Register the card
    addFilter('uixpress/dashboard/cards/register', function (widgets) {
      return [...widgets, myExternalCard];
    });
  });
})();

React Component Example:

// In your external plugin
(function () {
  'use strict';

  // Wait for UIXpress dashboard to be ready
  document.addEventListener('uixpress/dashboard/ready', function () {
    const { addFilter } = window.uixpress;

    // Define your React card component
    const MyReactCard = ({ dateRange, appData, id }) => {
      return (
        <div className="p-4">
          <h3>My React Plugin Card</h3>
          <p>This React card is from an external plugin!</p>
        </div>
      );
    };

    // Define your React card
    const myExternalReactCard = {
      metadata: {
        id: 'my-react-plugin-card',
        title: 'My React Plugin Data',
        description: 'React component from my plugin',
        width: 4,
        language: 'react', // Important: Set to 'react'
        category: 'site',
        requires_capabilities: ['manage_options'],
      },
      component: MyReactCard,
    };

    // Register the card
    addFilter('uixpress/dashboard/cards/register', function (widgets) {
      return [...widgets, myExternalReactCard];
    });
  });
})();

Best Practices

1. Card Design

  • Keep cards focused on a single purpose
  • Use consistent spacing and typography
  • Follow the established design system
  • Ensure responsive behavior

2. Performance

  • Implement proper loading states
  • Use efficient data fetching
  • Avoid unnecessary re-renders
  • Cache data when appropriate

3. Error Handling

  • Always handle API errors gracefully
  • Provide meaningful error messages
  • Implement retry mechanisms when appropriate

4. Accessibility

  • Use semantic HTML
  • Provide proper ARIA labels
  • Ensure keyboard navigation works
  • Test with screen readers

5. Security

  • Validate all user inputs
  • Use WordPress nonces for API calls
  • Respect capability requirements
  • Sanitize output data

6. Testing

  • Test with different user roles
  • Verify date range functionality
  • Test responsive layouts
  • Validate API integrations

Troubleshooting

Common Issues

  1. Card not appearing: Check if user has required capabilities
  2. API calls failing: Verify nonce and permissions
  3. Styling issues: Ensure Tailwind classes are available
  4. Date range not updating: Check watch implementation

Debug Tips

// Add debugging to your component
console.log('Date range changed:', props.dateRange);
console.log('App data:', props.appData);

Getting Help

  • Check the browser console for errors
  • Verify WordPress REST API endpoints
  • Test with different user roles
  • Review the UIXpress documentation

For more information, visit the UIXpress Documentation or contact support.