From 0c7c8642487e97ed51e4fe13892977ce7904fcd2 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Fri, 3 Oct 2025 17:36:53 +0200 Subject: [PATCH] v0.0.12: Type consistency and configuration improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- package.json | 2 +- src/index.ts | 3 +++ src/types/index.ts | 24 +++++++++++++++++ src/views/FeatureFlagsClient.tsx | 44 ++++++++++++-------------------- src/views/FeatureFlagsView.tsx | 31 +++++++--------------- 5 files changed, 54 insertions(+), 50 deletions(-) create mode 100644 src/types/index.ts diff --git a/package.json b/package.json index 76cf062..0fdad0e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.ts b/src/index.ts index 3d83577..3b03e47 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..85c5f11 --- /dev/null +++ b/src/types/index.ts @@ -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 +} \ No newline at end of file diff --git a/src/views/FeatureFlagsClient.tsx b/src/views/FeatureFlagsClient.tsx index 37d53a5..ed0919c 100644 --- a/src/views/FeatureFlagsClient.tsx +++ b/src/views/FeatureFlagsClient.tsx @@ -4,6 +4,7 @@ import { useConfig, useTheme } from '@payloadcms/ui' +import type { PayloadID, FeatureFlag } from '../types/index.js' // Simple debounce hook function useDebounce(value: T, delay: number): T { @@ -22,30 +23,19 @@ function useDebounce(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(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(null) + const [saving, setSaving] = useState(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) => { + const updateFlag = useCallback(async (flagId: PayloadID, updates: Partial) => { // 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 }}> - 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): Promise { 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 (