mirror of
https://github.com/xtr-dev/payload-feature-flags.git
synced 2025-12-09 18:33:25 +00:00
v0.0.12: Type consistency and configuration improvements
Type System Enhancements: - Introduced PayloadID helper type (string | number) for flexible ID handling - Created shared types module (src/types/index.ts) for better type consistency - Exported PayloadID and FeatureFlag types from main index for user access - Fixed runtime issues with different Payload ID configurations Configuration Improvements: - Made API request limits configurable via maxFlags prop (default 100, max 1000) - Added configurable collection slug support for custom collection names - Enhanced URL construction to use config.routes.admin for proper path handling - Improved server-side pagination with query parameter support Code Quality: - Centralized type definitions for better maintainability - Enhanced type safety across client and server components - Improved prop interfaces with better documentation - Fixed potential number parsing edge cases with parseFloat Developer Experience: - Users can now configure collection slug, API limits, and admin paths - Better TypeScript support with exported shared types - Consistent handling of both string and numeric IDs - More flexible plugin configuration options 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xtr-dev/payload-feature-flags",
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.12",
|
||||
"description": "Feature flags plugin for Payload CMS - manage feature toggles, A/B tests, and gradual rollouts",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -6,6 +6,9 @@ export type CollectionOverrides = Partial<
|
||||
fields?: (args: { defaultFields: Field[] }) => Field[]
|
||||
}
|
||||
|
||||
// Export shared types for users of the plugin
|
||||
export type { PayloadID, FeatureFlag } from './types/index.js'
|
||||
|
||||
export type PayloadFeatureFlagsConfig = {
|
||||
/**
|
||||
* Enable/disable the plugin
|
||||
|
||||
24
src/types/index.ts
Normal file
24
src/types/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Shared types for the feature flags plugin
|
||||
|
||||
// Helper type for flexible ID handling - supports both string and number IDs
|
||||
// This allows the plugin to work with different Payload ID configurations
|
||||
export type PayloadID = string | number
|
||||
|
||||
// Common interface for feature flags used across the plugin
|
||||
export interface FeatureFlag {
|
||||
id: PayloadID
|
||||
name: string
|
||||
description?: string
|
||||
enabled: boolean
|
||||
rolloutPercentage?: number
|
||||
variants?: Array<{
|
||||
name: string
|
||||
weight: number
|
||||
metadata?: any
|
||||
}>
|
||||
environment?: 'development' | 'staging' | 'production'
|
||||
tags?: Array<{ tag: string }>
|
||||
metadata?: any
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
useConfig,
|
||||
useTheme
|
||||
} from '@payloadcms/ui'
|
||||
import type { PayloadID, FeatureFlag } from '../types/index.js'
|
||||
|
||||
// Simple debounce hook
|
||||
function useDebounce<T>(value: T, delay: number): T {
|
||||
@@ -22,30 +23,19 @@ function useDebounce<T>(value: T, delay: number): T {
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
interface FeatureFlag {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
enabled: boolean
|
||||
rolloutPercentage?: number
|
||||
variants?: Array<{
|
||||
name: string
|
||||
weight: number
|
||||
metadata?: any
|
||||
}>
|
||||
environment?: 'development' | 'staging' | 'production'
|
||||
tags?: Array<{ tag: string }>
|
||||
metadata?: any
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
interface FeatureFlagsClientProps {
|
||||
initialFlags?: FeatureFlag[]
|
||||
canUpdate?: boolean
|
||||
maxFlags?: number // Configurable limit for API requests
|
||||
collectionSlug?: string // Configurable collection slug for URLs
|
||||
}
|
||||
|
||||
const FeatureFlagsClientComponent = ({ initialFlags = [], canUpdate = true }: FeatureFlagsClientProps) => {
|
||||
const FeatureFlagsClientComponent = ({
|
||||
initialFlags = [],
|
||||
canUpdate = true,
|
||||
maxFlags = 100,
|
||||
collectionSlug = 'feature-flags'
|
||||
}: FeatureFlagsClientProps) => {
|
||||
const { config } = useConfig()
|
||||
const { theme } = useTheme()
|
||||
const [flags, setFlags] = useState<FeatureFlag[]>(initialFlags)
|
||||
@@ -54,7 +44,7 @@ const FeatureFlagsClientComponent = ({ initialFlags = [], canUpdate = true }: Fe
|
||||
const [search, setSearch] = useState('')
|
||||
const [sortField, setSortField] = useState<'name' | 'enabled' | 'rolloutPercentage' | 'updatedAt'>('name')
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
|
||||
const [saving, setSaving] = useState<string | null>(null)
|
||||
const [saving, setSaving] = useState<PayloadID | null>(null)
|
||||
const [successMessage, setSuccessMessage] = useState('')
|
||||
|
||||
// Debounce search to reduce re-renders
|
||||
@@ -65,9 +55,9 @@ const FeatureFlagsClientComponent = ({ initialFlags = [], canUpdate = true }: Fe
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
// Use a reasonable limit to prevent performance issues
|
||||
const limit = Math.min(1000, 100)
|
||||
const response = await fetch(`${config.serverURL}${config.routes.api}/feature-flags?limit=${limit}`, {
|
||||
// Use configurable limit, capped at 1000 for performance
|
||||
const limit = Math.min(1000, maxFlags)
|
||||
const response = await fetch(`${config.serverURL}${config.routes.api}/${collectionSlug}?limit=${limit}`, {
|
||||
credentials: 'include',
|
||||
signal,
|
||||
})
|
||||
@@ -101,7 +91,7 @@ const FeatureFlagsClientComponent = ({ initialFlags = [], canUpdate = true }: Fe
|
||||
}
|
||||
}
|
||||
|
||||
const updateFlag = useCallback(async (flagId: string, updates: Partial<FeatureFlag>) => {
|
||||
const updateFlag = useCallback(async (flagId: PayloadID, updates: Partial<FeatureFlag>) => {
|
||||
// Security check: Don't allow updates if user doesn't have permission
|
||||
if (!canUpdate) {
|
||||
setError('You do not have permission to update feature flags')
|
||||
@@ -114,7 +104,7 @@ const FeatureFlagsClientComponent = ({ initialFlags = [], canUpdate = true }: Fe
|
||||
setSuccessMessage('')
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.serverURL}${config.routes.api}/feature-flags/${flagId}`, {
|
||||
const response = await fetch(`${config.serverURL}${config.routes.api}/${collectionSlug}/${flagId}`, {
|
||||
method: 'PATCH',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
@@ -149,7 +139,7 @@ const FeatureFlagsClientComponent = ({ initialFlags = [], canUpdate = true }: Fe
|
||||
} finally {
|
||||
setSaving(null)
|
||||
}
|
||||
}, [config.serverURL, config.routes.api, canUpdate])
|
||||
}, [config.serverURL, config.routes.api, canUpdate, collectionSlug])
|
||||
|
||||
const handleSort = useCallback((field: typeof sortField) => {
|
||||
if (sortField === field) {
|
||||
@@ -512,7 +502,7 @@ const FeatureFlagsClientComponent = ({ initialFlags = [], canUpdate = true }: Fe
|
||||
color: styles.text
|
||||
}}>
|
||||
<a
|
||||
href={`${config.routes.admin}/collections/feature-flags/${flag.id}`}
|
||||
href={`${config.routes.admin}/collections/${collectionSlug}/${flag.id}`}
|
||||
style={{
|
||||
color: styles.info,
|
||||
textDecoration: 'none',
|
||||
|
||||
@@ -2,32 +2,16 @@ import type { AdminViewServerProps } from 'payload'
|
||||
import { DefaultTemplate } from '@payloadcms/next/templates'
|
||||
import { Gutter } from '@payloadcms/ui'
|
||||
import FeatureFlagsClient from './FeatureFlagsClient.js'
|
||||
|
||||
interface FeatureFlag {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
enabled: boolean
|
||||
rolloutPercentage?: number
|
||||
variants?: Array<{
|
||||
name: string
|
||||
weight: number
|
||||
metadata?: any
|
||||
}>
|
||||
environment?: 'development' | 'staging' | 'production'
|
||||
tags?: Array<{ tag: string }>
|
||||
metadata?: any
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
import type { FeatureFlag } from '../types/index.js'
|
||||
|
||||
async function fetchInitialFlags(payload: any, searchParams?: Record<string, any>): Promise<FeatureFlag[]> {
|
||||
try {
|
||||
const limit = Math.min(1000, parseInt(searchParams?.limit as string) || 100)
|
||||
const page = Math.max(1, parseInt(searchParams?.page as string) || 1)
|
||||
const collectionSlug = searchParams?.collectionSlug as string || 'feature-flags'
|
||||
|
||||
const result = await payload.find({
|
||||
collection: 'feature-flags',
|
||||
collection: collectionSlug,
|
||||
limit,
|
||||
page,
|
||||
sort: 'name',
|
||||
@@ -100,7 +84,8 @@ export default async function FeatureFlagsView({
|
||||
}
|
||||
|
||||
// Security check: User must have permissions to access feature-flags collection
|
||||
const canReadFeatureFlags = permissions?.collections?.['feature-flags']?.read
|
||||
const collectionSlug = searchParams?.collectionSlug as string || 'feature-flags'
|
||||
const canReadFeatureFlags = permissions?.collections?.[collectionSlug]?.read
|
||||
if (!canReadFeatureFlags) {
|
||||
return (
|
||||
<DefaultTemplate
|
||||
@@ -141,8 +126,8 @@ export default async function FeatureFlagsView({
|
||||
// Fetch initial data server-side (only if user has access)
|
||||
const initialFlags = await fetchInitialFlags(initPageResult.req.payload, searchParams)
|
||||
|
||||
// Check if user can update feature flags
|
||||
const canUpdateFeatureFlags = permissions?.collections?.['feature-flags']?.update || false
|
||||
// Check if user can update feature flags (use already defined collection slug)
|
||||
const canUpdateFeatureFlags = permissions?.collections?.[collectionSlug]?.update || false
|
||||
|
||||
// Use DefaultTemplate with proper props structure from initPageResult
|
||||
return (
|
||||
@@ -160,6 +145,8 @@ export default async function FeatureFlagsView({
|
||||
<FeatureFlagsClient
|
||||
initialFlags={initialFlags}
|
||||
canUpdate={canUpdateFeatureFlags}
|
||||
maxFlags={parseInt(searchParams?.maxFlags as string) || 100}
|
||||
collectionSlug={collectionSlug}
|
||||
/>
|
||||
</Gutter>
|
||||
</DefaultTemplate>
|
||||
|
||||
Reference in New Issue
Block a user