Integrate Payload CMS theme system and add clickable flag names

Theme Integration:
- Added useTheme hook from @payloadcms/ui
- Replaced all hardcoded colors with CSS custom properties
- Created getThemeStyles() function for consistent theming
- Updated all UI elements to respect dark/light theme settings
- Added proper contrast for text, backgrounds, and borders

Navigation Enhancement:
- Made feature flag names clickable links
- Links navigate to /admin/collections/feature-flags/{id} for editing
- Added hover effects with underline on flag name links
- Used theme-aware link color (info blue)

The interface now properly adapts to Payload's admin panel theme,
supporting both dark and light modes seamlessly.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-03 15:39:10 +02:00
parent b364fb9e8f
commit 477f7f96eb

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { useState, useEffect, useCallback, useMemo, memo } from 'react' import { useState, useEffect, useCallback, useMemo, memo } from 'react'
import { useConfig } from '@payloadcms/ui' import { useConfig, useTheme } from '@payloadcms/ui'
interface FeatureFlag { interface FeatureFlag {
id: string id: string
@@ -22,6 +22,7 @@ interface FeatureFlag {
const FeatureFlagsViewComponent = () => { const FeatureFlagsViewComponent = () => {
const { config } = useConfig() const { config } = useConfig()
const { theme } = useTheme()
const [flags, setFlags] = useState<FeatureFlag[]>([]) const [flags, setFlags] = useState<FeatureFlag[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState('') const [error, setError] = useState('')
@@ -168,27 +169,47 @@ const FeatureFlagsViewComponent = () => {
return <span>{sortDirection === 'asc' ? '↑' : '↓'}</span> return <span>{sortDirection === 'asc' ? '↑' : '↓'}</span>
} }
// Theme-aware styles
const getThemeStyles = () => ({
background: 'var(--theme-bg)',
surface: 'var(--theme-elevation-50)',
surfaceHover: 'var(--theme-elevation-100)',
border: 'var(--theme-elevation-150)',
text: 'var(--theme-text)',
textMuted: 'var(--theme-text-400)',
textSubdued: 'var(--theme-text-600)',
primary: 'var(--theme-success-500)',
warning: 'var(--theme-warning-500)',
error: 'var(--theme-error-500)',
info: 'var(--theme-info-500)',
inputBg: 'var(--theme-elevation-0)',
inputBorder: 'var(--theme-elevation-250)',
headerBg: 'var(--theme-elevation-100)',
})
const styles = getThemeStyles()
if (loading) { if (loading) {
return ( return (
<div style={{ padding: '2rem', textAlign: 'center' }}> <div style={{ padding: '2rem', textAlign: 'center', backgroundColor: styles.background, color: styles.text }}>
<div style={{ fontSize: '1.125rem', color: '#6b7280' }}>Loading feature flags...</div> <div style={{ fontSize: '1.125rem', color: styles.textMuted }}>Loading feature flags...</div>
</div> </div>
) )
} }
return ( return (
<div style={{ padding: '2rem', maxWidth: '100%' }}> <div style={{ padding: '2rem', maxWidth: '100%', backgroundColor: styles.background, color: styles.text, minHeight: '100vh' }}>
{/* Header */} {/* Header */}
<div style={{ marginBottom: '2rem' }}> <div style={{ marginBottom: '2rem' }}>
<h1 style={{ <h1 style={{
fontSize: '2rem', fontSize: '2rem',
fontWeight: '700', fontWeight: '700',
color: '#111827', color: styles.text,
marginBottom: '0.5rem' marginBottom: '0.5rem'
}}> }}>
Feature Flags Dashboard Feature Flags Dashboard
</h1> </h1>
<p style={{ color: '#6b7280', fontSize: '1rem' }}> <p style={{ color: styles.textMuted, fontSize: '1rem' }}>
Manage all feature flags in a spreadsheet view Manage all feature flags in a spreadsheet view
</p> </p>
</div> </div>
@@ -199,7 +220,7 @@ const FeatureFlagsViewComponent = () => {
position: 'fixed', position: 'fixed',
top: '20px', top: '20px',
right: '20px', right: '20px',
backgroundColor: '#10b981', backgroundColor: styles.primary,
color: 'white', color: 'white',
padding: '0.75rem 1.5rem', padding: '0.75rem 1.5rem',
borderRadius: '0.5rem', borderRadius: '0.5rem',
@@ -213,11 +234,11 @@ const FeatureFlagsViewComponent = () => {
{error && ( {error && (
<div style={{ <div style={{
marginBottom: '1rem', marginBottom: '1rem',
backgroundColor: '#fef2f2', backgroundColor: styles.error + '20',
border: '1px solid #fecaca', border: `1px solid ${styles.error}`,
borderRadius: '0.5rem', borderRadius: '0.5rem',
padding: '1rem', padding: '1rem',
color: '#dc2626' color: styles.error
}}> }}>
<strong>Error:</strong> {error} <strong>Error:</strong> {error}
</div> </div>
@@ -238,25 +259,27 @@ const FeatureFlagsViewComponent = () => {
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
style={{ style={{
padding: '0.5rem 1rem', padding: '0.5rem 1rem',
border: '1px solid #d1d5db', border: `1px solid ${styles.inputBorder}`,
borderRadius: '0.5rem', borderRadius: '0.5rem',
fontSize: '0.875rem', fontSize: '0.875rem',
width: '300px' width: '300px',
backgroundColor: styles.inputBg,
color: styles.text
}} }}
/> />
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}> <div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<div style={{ fontSize: '0.875rem', color: '#6b7280' }}> <div style={{ fontSize: '0.875rem', color: styles.textMuted }}>
{filteredAndSortedFlags.length} of {flags.filter(f => f && f.name).length} flags {filteredAndSortedFlags.length} of {flags.filter(f => f && f.name).length} flags
</div> </div>
<button <button
onClick={() => fetchFlags()} onClick={() => fetchFlags()}
style={{ style={{
padding: '0.5rem 1rem', padding: '0.5rem 1rem',
border: '1px solid #d1d5db', border: `1px solid ${styles.inputBorder}`,
borderRadius: '0.5rem', borderRadius: '0.5rem',
backgroundColor: 'white', backgroundColor: styles.surface,
color: '#374151', color: styles.text,
fontSize: '0.875rem', fontSize: '0.875rem',
cursor: 'pointer' cursor: 'pointer'
}} }}
@@ -268,8 +291,8 @@ const FeatureFlagsViewComponent = () => {
{/* Spreadsheet Table */} {/* Spreadsheet Table */}
<div style={{ <div style={{
backgroundColor: 'white', backgroundColor: styles.surface,
border: '1px solid #e5e7eb', border: `1px solid ${styles.border}`,
borderRadius: '0.75rem', borderRadius: '0.75rem',
overflow: 'hidden' overflow: 'hidden'
}}> }}>
@@ -280,16 +303,16 @@ const FeatureFlagsViewComponent = () => {
fontSize: '0.875rem' fontSize: '0.875rem'
}}> }}>
<thead> <thead>
<tr style={{ backgroundColor: '#f9fafb' }}> <tr style={{ backgroundColor: styles.headerBg }}>
<th style={{ <th style={{
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
textAlign: 'left', textAlign: 'left',
fontWeight: '600', fontWeight: '600',
color: '#374151', color: styles.text,
borderBottom: '1px solid #e5e7eb', borderBottom: `1px solid ${styles.border}`,
position: 'sticky', position: 'sticky',
left: 0, left: 0,
backgroundColor: '#f9fafb', backgroundColor: styles.headerBg,
minWidth: '50px' minWidth: '50px'
}}> }}>
Status Status
@@ -300,8 +323,8 @@ const FeatureFlagsViewComponent = () => {
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
textAlign: 'left', textAlign: 'left',
fontWeight: '600', fontWeight: '600',
color: '#374151', color: styles.text,
borderBottom: '1px solid #e5e7eb', borderBottom: `1px solid ${styles.border}`,
cursor: 'pointer', cursor: 'pointer',
minWidth: '200px', minWidth: '200px',
userSelect: 'none' userSelect: 'none'
@@ -313,8 +336,8 @@ const FeatureFlagsViewComponent = () => {
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
textAlign: 'left', textAlign: 'left',
fontWeight: '600', fontWeight: '600',
color: '#374151', color: styles.text,
borderBottom: '1px solid #e5e7eb', borderBottom: `1px solid ${styles.border}`,
minWidth: '300px' minWidth: '300px'
}}> }}>
Description Description
@@ -325,8 +348,8 @@ const FeatureFlagsViewComponent = () => {
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
textAlign: 'center', textAlign: 'center',
fontWeight: '600', fontWeight: '600',
color: '#374151', color: styles.text,
borderBottom: '1px solid #e5e7eb', borderBottom: `1px solid ${styles.border}`,
cursor: 'pointer', cursor: 'pointer',
minWidth: '80px', minWidth: '80px',
userSelect: 'none' userSelect: 'none'
@@ -340,8 +363,8 @@ const FeatureFlagsViewComponent = () => {
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
textAlign: 'center', textAlign: 'center',
fontWeight: '600', fontWeight: '600',
color: '#374151', color: styles.text,
borderBottom: '1px solid #e5e7eb', borderBottom: `1px solid ${styles.border}`,
cursor: 'pointer', cursor: 'pointer',
minWidth: '120px', minWidth: '120px',
userSelect: 'none' userSelect: 'none'
@@ -353,8 +376,8 @@ const FeatureFlagsViewComponent = () => {
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
textAlign: 'center', textAlign: 'center',
fontWeight: '600', fontWeight: '600',
color: '#374151', color: styles.text,
borderBottom: '1px solid #e5e7eb', borderBottom: `1px solid ${styles.border}`,
minWidth: '100px' minWidth: '100px'
}}> }}>
Variants Variants
@@ -363,8 +386,8 @@ const FeatureFlagsViewComponent = () => {
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
textAlign: 'left', textAlign: 'left',
fontWeight: '600', fontWeight: '600',
color: '#374151', color: styles.text,
borderBottom: '1px solid #e5e7eb', borderBottom: `1px solid ${styles.border}`,
minWidth: '150px' minWidth: '150px'
}}> }}>
Tags Tags
@@ -375,8 +398,8 @@ const FeatureFlagsViewComponent = () => {
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
textAlign: 'left', textAlign: 'left',
fontWeight: '600', fontWeight: '600',
color: '#374151', color: styles.text,
borderBottom: '1px solid #e5e7eb', borderBottom: `1px solid ${styles.border}`,
cursor: 'pointer', cursor: 'pointer',
minWidth: '150px', minWidth: '150px',
userSelect: 'none' userSelect: 'none'
@@ -392,7 +415,7 @@ const FeatureFlagsViewComponent = () => {
<td colSpan={8} style={{ <td colSpan={8} style={{
padding: '2rem', padding: '2rem',
textAlign: 'center', textAlign: 'center',
color: '#6b7280' color: styles.textMuted
}}> }}>
{search ? 'No flags match your search' : 'No feature flags yet'} {search ? 'No flags match your search' : 'No feature flags yet'}
</td> </td>
@@ -400,10 +423,10 @@ const FeatureFlagsViewComponent = () => {
) : ( ) : (
filteredAndSortedFlags.map(flag => ( filteredAndSortedFlags.map(flag => (
<tr key={flag.id} style={{ <tr key={flag.id} style={{
borderBottom: '1px solid #e5e7eb', borderBottom: `1px solid ${styles.border}`,
transition: 'background-color 0.15s', transition: 'background-color 0.15s',
}} }}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f9fafb'} onMouseEnter={(e) => e.currentTarget.style.backgroundColor = styles.surfaceHover}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = ''} onMouseLeave={(e) => e.currentTarget.style.backgroundColor = ''}
> >
<td style={{ <td style={{
@@ -417,20 +440,31 @@ const FeatureFlagsViewComponent = () => {
height: '8px', height: '8px',
borderRadius: '50%', borderRadius: '50%',
backgroundColor: flag.enabled ? backgroundColor: flag.enabled ?
(flag.rolloutPercentage && flag.rolloutPercentage < 100 ? '#f59e0b' : '#10b981') (flag.rolloutPercentage && flag.rolloutPercentage < 100 ? styles.warning : styles.primary)
: '#ef4444' : styles.error
}} /> }} />
</td> </td>
<td style={{ <td style={{
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
fontWeight: '500', fontWeight: '500',
color: '#111827' color: styles.text
}}> }}>
{flag.name} <a
href={`/admin/collections/feature-flags/${flag.id}`}
style={{
color: styles.info,
textDecoration: 'none',
cursor: 'pointer'
}}
onMouseEnter={(e) => e.currentTarget.style.textDecoration = 'underline'}
onMouseLeave={(e) => e.currentTarget.style.textDecoration = 'none'}
>
{flag.name}
</a>
</td> </td>
<td style={{ <td style={{
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
color: '#6b7280' color: styles.textMuted
}}> }}>
{flag.description || '-'} {flag.description || '-'}
</td> </td>
@@ -447,7 +481,7 @@ const FeatureFlagsViewComponent = () => {
width: '18px', width: '18px',
height: '18px', height: '18px',
cursor: saving === flag.id ? 'wait' : 'pointer', cursor: saving === flag.id ? 'wait' : 'pointer',
accentColor: '#10b981' accentColor: styles.primary
}} }}
/> />
</td> </td>
@@ -469,25 +503,27 @@ const FeatureFlagsViewComponent = () => {
style={{ style={{
width: '60px', width: '60px',
padding: '0.25rem 0.5rem', padding: '0.25rem 0.5rem',
border: '1px solid #d1d5db', border: `1px solid ${styles.inputBorder}`,
borderRadius: '0.25rem', borderRadius: '0.25rem',
fontSize: '0.875rem', fontSize: '0.875rem',
textAlign: 'center', textAlign: 'center',
cursor: saving === flag.id ? 'wait' : 'text', cursor: saving === flag.id ? 'wait' : 'text',
opacity: flag.enabled ? 1 : 0.5 opacity: flag.enabled ? 1 : 0.5,
backgroundColor: styles.inputBg,
color: styles.text
}} }}
/> />
<span style={{ color: '#6b7280' }}>%</span> <span style={{ color: styles.textMuted }}>%</span>
</div> </div>
</td> </td>
<td style={{ <td style={{
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
textAlign: 'center', textAlign: 'center',
color: '#6b7280' color: styles.textMuted
}}> }}>
{flag.variants && flag.variants.length > 0 ? ( {flag.variants && flag.variants.length > 0 ? (
<span style={{ <span style={{
backgroundColor: '#f3f4f6', backgroundColor: styles.surface,
padding: '0.25rem 0.5rem', padding: '0.25rem 0.5rem',
borderRadius: '0.25rem', borderRadius: '0.25rem',
fontSize: '0.75rem' fontSize: '0.75rem'
@@ -503,8 +539,8 @@ const FeatureFlagsViewComponent = () => {
<div style={{ display: 'flex', gap: '0.25rem', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '0.25rem', flexWrap: 'wrap' }}>
{flag.tags.map((t, i) => ( {flag.tags.map((t, i) => (
<span key={i} style={{ <span key={i} style={{
backgroundColor: '#e0e7ff', backgroundColor: styles.info + '20',
color: '#3730a3', color: styles.info,
padding: '0.125rem 0.5rem', padding: '0.125rem 0.5rem',
borderRadius: '0.25rem', borderRadius: '0.25rem',
fontSize: '0.75rem' fontSize: '0.75rem'
@@ -517,7 +553,7 @@ const FeatureFlagsViewComponent = () => {
</td> </td>
<td style={{ <td style={{
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
color: '#6b7280', color: styles.textMuted,
fontSize: '0.75rem' fontSize: '0.75rem'
}}> }}>
{new Date(flag.updatedAt).toLocaleDateString()} {new Date(flag.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {new Date(flag.updatedAt).toLocaleDateString()} {new Date(flag.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
@@ -536,7 +572,7 @@ const FeatureFlagsViewComponent = () => {
display: 'flex', display: 'flex',
gap: '2rem', gap: '2rem',
fontSize: '0.875rem', fontSize: '0.875rem',
color: '#6b7280' color: styles.textMuted
}}> }}>
<div> <div>
<span style={{ fontWeight: '600' }}>Total:</span> {flags.filter(f => f && f.name).length} flags <span style={{ fontWeight: '600' }}>Total:</span> {flags.filter(f => f && f.name).length} flags