Skip to content

Commit 54fbf54

Browse files
committed
feat: add feature flag
1 parent 1b42c5a commit 54fbf54

14 files changed

Lines changed: 383 additions & 29 deletions

File tree

FEATURE_FLAG_DEBUG.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Debugging Feature Flags
2+
3+
## Steps to Enable the Feature Flag
4+
5+
### Option 1: Environment Variable (Highest Priority)
6+
```bash
7+
export MEDUSA_FF_VIEW_CONFIGURATIONS=true
8+
npm run dev
9+
```
10+
11+
### Option 2: medusa-config.js
12+
```javascript
13+
module.exports = {
14+
projectConfig: {
15+
// ... other config
16+
},
17+
featureFlags: {
18+
view_configurations: true
19+
}
20+
}
21+
```
22+
23+
**Note**: Make sure `featureFlags` is at the root level, not inside `projectConfig`.
24+
25+
## Debugging Steps
26+
27+
1. **Test the feature flag directly**:
28+
```bash
29+
curl http://localhost:9000/admin/test-feature-flag
30+
```
31+
This will show:
32+
- Current flag value
33+
- Environment variable value
34+
- All loaded flags
35+
36+
2. **Check feature flags API**:
37+
```bash
38+
curl http://localhost:9000/admin/feature-flags \
39+
-H "Authorization: Bearer YOUR_TOKEN"
40+
```
41+
42+
3. **Browser Console**:
43+
- Open browser dev tools
44+
- Navigate to Orders page
45+
- Check console for:
46+
- "Feature flags loaded: ..."
47+
- "view_configurations flag: ..."
48+
- "Checking feature flag view_configurations: ..."
49+
50+
## Common Issues
51+
52+
1. **Config not loading**: Make sure to restart the server after changing medusa-config.js
53+
2. **Cache issues**: Clear browser cache or open in incognito mode
54+
3. **Wrong config format**: Ensure featureFlags is at root level, not nested
55+
56+
## Verification
57+
58+
When the flag is enabled:
59+
- `/admin/view-configurations` endpoints should return data (not 404)
60+
- Orders page should show view selector dropdown
61+
- Console should show "view_configurations flag: true"
62+
63+
When the flag is disabled:
64+
- `/admin/view-configurations` endpoints should return 404
65+
- Orders page should show legacy table
66+
- Console should show "view_configurations flag: false"

packages/admin/dashboard/src/components/data-table/data-table.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { useQueryParams } from "../../hooks/use-query-params"
2525
import { ActionMenu } from "../common/action-menu"
2626
import { ViewConfiguration } from "../../providers/view-configuration-provider"
2727
import { ViewSelector } from "../table/view-selector"
28+
import { useFeatureFlag } from "../../providers/feature-flag-provider"
2829

2930
type DataTableActionProps = {
3031
label: string
@@ -130,6 +131,11 @@ export const DataTable = <TData,>({
130131
currentColumns,
131132
}: DataTableProps<TData>) => {
132133
const { t } = useTranslation()
134+
const isViewConfigEnabled = useFeatureFlag("view_configurations")
135+
136+
// If view config is disabled, don't use column visibility features
137+
const effectiveEnableColumnVisibility = isViewConfigEnabled && enableColumnVisibility
138+
const effectiveEnableViewSelector = isViewConfigEnabled && enableViewSelector
133139

134140
const enableFiltering = filters && filters.length > 0
135141
const enableCommands = commands && commands.length > 0
@@ -314,13 +320,13 @@ export const DataTable = <TData,>({
314320
: undefined,
315321
rowSelection,
316322
isLoading,
317-
columnVisibility: enableColumnVisibility
323+
columnVisibility: effectiveEnableColumnVisibility
318324
? {
319325
state: columnVisibility,
320326
onColumnVisibilityChange: handleColumnVisibilityChange,
321327
}
322328
: undefined,
323-
columnOrder: columnOrder && onColumnOrderChange
329+
columnOrder: effectiveEnableColumnVisibility && columnOrder && onColumnOrderChange
324330
? {
325331
state: columnOrder,
326332
onColumnOrderChange: onColumnOrderChange,
@@ -359,8 +365,8 @@ export const DataTable = <TData,>({
359365
{enableSorting && (
360366
<Primitive.SortingMenu tooltip={t("filters.sortLabel")} />
361367
)}
362-
{enableColumnVisibility && <Primitive.ColumnVisibilityMenu />}
363-
{enableViewSelector && entity && (
368+
{effectiveEnableColumnVisibility && <Primitive.ColumnVisibilityMenu />}
369+
{effectiveEnableViewSelector && entity && (
364370
<ViewSelector
365371
entity={entity}
366372
onViewChange={onViewChange}
@@ -387,8 +393,8 @@ export const DataTable = <TData,>({
387393
{enableSorting && (
388394
<Primitive.SortingMenu tooltip={t("filters.sortLabel")} />
389395
)}
390-
{enableColumnVisibility && <Primitive.ColumnVisibilityMenu />}
391-
{enableViewSelector && entity && (
396+
{effectiveEnableColumnVisibility && <Primitive.ColumnVisibilityMenu />}
397+
{effectiveEnableViewSelector && entity && (
392398
<ViewSelector
393399
entity={entity}
394400
onViewChange={onViewChange}

packages/admin/dashboard/src/components/table/data-table/data-table.tsx

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ interface DataTableProps<TData>
1212
pageSize: number
1313
queryObject?: Record<string, any>
1414
noRecords?: Pick<NoResultsProps, "title" | "message">
15-
enableColumnVisibility?: boolean
1615
}
1716

1817
// Maybe we should use the memoized version of DataTableRoot
@@ -39,11 +38,6 @@ export const _DataTable = <TData,>({
3938
noHeader = false,
4039
layout = "fit",
4140
noRecords: noRecordsProps = {},
42-
enableColumnVisibility = false,
43-
enableViewSelector = false,
44-
entity,
45-
onViewChange,
46-
currentColumns,
4741
}: DataTableProps<TData>) => {
4842
if (isLoading) {
4943
return (
@@ -85,12 +79,6 @@ export const _DataTable = <TData,>({
8579
orderBy={orderBy}
8680
filters={filters}
8781
prefix={prefix}
88-
table={table}
89-
enableColumnVisibility={enableColumnVisibility}
90-
enableViewSelector={enableViewSelector}
91-
entity={entity}
92-
onViewChange={onViewChange}
93-
currentColumns={currentColumns}
9482
/>
9583
<DataTableRoot
9684
table={table}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { useQuery } from "@tanstack/react-query"
2+
import { sdk } from "../../lib/client"
3+
4+
export type FeatureFlags = {
5+
view_configurations?: boolean
6+
[key: string]: boolean | undefined
7+
}
8+
9+
export const useFeatureFlags = () => {
10+
return useQuery<FeatureFlags>({
11+
queryKey: ["admin", "feature-flags"],
12+
queryFn: async () => {
13+
const response = await sdk.client.fetch<{ feature_flags: FeatureFlags }>("/admin/feature-flags", {
14+
method: "GET",
15+
})
16+
17+
return response.feature_flags
18+
},
19+
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
20+
cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
21+
})
22+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React, { createContext, useContext } from "react"
2+
import { useFeatureFlags, FeatureFlags } from "../../hooks/api/feature-flags"
3+
4+
interface FeatureFlagContextValue {
5+
flags: FeatureFlags
6+
isLoading: boolean
7+
isFeatureEnabled: (flag: keyof FeatureFlags) => boolean
8+
}
9+
10+
const FeatureFlagContext = createContext<FeatureFlagContextValue | null>(null)
11+
12+
export const useFeatureFlag = (flag: keyof FeatureFlags): boolean => {
13+
const context = useContext(FeatureFlagContext)
14+
if (!context) {
15+
// If no context, assume feature is disabled
16+
return false
17+
}
18+
return context.isFeatureEnabled(flag)
19+
}
20+
21+
export const useFeatureFlagContext = () => {
22+
const context = useContext(FeatureFlagContext)
23+
if (!context) {
24+
throw new Error("useFeatureFlagContext must be used within FeatureFlagProvider")
25+
}
26+
return context
27+
}
28+
29+
interface FeatureFlagProviderProps {
30+
children: React.ReactNode
31+
}
32+
33+
export const FeatureFlagProvider: React.FC<FeatureFlagProviderProps> = ({ children }) => {
34+
const { data: flags = {}, isLoading, error } = useFeatureFlags()
35+
36+
// Debug logging
37+
React.useEffect(() => {
38+
if (!isLoading) {
39+
console.log("Feature flags loaded:", flags)
40+
console.log("view_configurations flag:", flags.view_configurations)
41+
}
42+
if (error) {
43+
console.error("Error loading feature flags:", error)
44+
}
45+
}, [flags, isLoading, error])
46+
47+
const isFeatureEnabled = (flag: keyof FeatureFlags): boolean => {
48+
const enabled = flags[flag] === true
49+
console.log(`Checking feature flag ${flag}:`, enabled)
50+
return enabled
51+
}
52+
53+
return (
54+
<FeatureFlagContext.Provider value={{ flags, isLoading, isFeatureEnabled }}>
55+
{children}
56+
</FeatureFlagContext.Provider>
57+
)
58+
}

packages/admin/dashboard/src/providers/providers.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ExtensionProvider } from "./extension-provider"
99
import { I18nProvider } from "./i18n-provider"
1010
import { ThemeProvider } from "./theme-provider"
1111
import { ViewConfigurationProvider } from "./view-configuration-provider"
12+
import { FeatureFlagProvider } from "./feature-flag-provider"
1213

1314
type ProvidersProps = PropsWithChildren<{
1415
api: DashboardApp["api"]
@@ -21,11 +22,13 @@ export const Providers = ({ api, children }: ProvidersProps) => {
2122
<HelmetProvider>
2223
<QueryClientProvider client={queryClient}>
2324
<ThemeProvider>
24-
<ViewConfigurationProvider>
25-
<I18n />
26-
<I18nProvider>{children}</I18nProvider>
27-
<Toaster />
28-
</ViewConfigurationProvider>
25+
<FeatureFlagProvider>
26+
<ViewConfigurationProvider>
27+
<I18n />
28+
<I18nProvider>{children}</I18nProvider>
29+
<Toaster />
30+
</ViewConfigurationProvider>
31+
</FeatureFlagProvider>
2932
</ThemeProvider>
3033
</QueryClientProvider>
3134
</HelmetProvider>

packages/admin/dashboard/src/providers/view-configuration-provider/view-configuration-provider.tsx

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@ import { PropsWithChildren, useCallback, useRef, useState } from "react"
22
import { ViewConfigurationContext, ViewConfiguration } from "./view-configuration-context"
33
import { sdk } from "../../lib/client"
44
import { toast } from "@medusajs/ui"
5+
import { useFeatureFlag } from "../feature-flag-provider"
56

67
export const ViewConfigurationProvider = ({ children }: PropsWithChildren) => {
78
const [viewConfigurations] = useState<Map<string, ViewConfiguration[]>>(new Map())
89
const [activeViews] = useState<Map<string, ViewConfiguration>>(new Map())
910
const [isLoading] = useState<Map<string, boolean>>(new Map())
11+
const isViewConfigEnabled = useFeatureFlag("view_configurations")
1012

1113
// Use ref to track ongoing requests to prevent duplicate fetches
1214
const fetchingRef = useRef<Map<string, Promise<ViewConfiguration[]>>>(new Map())
1315

1416
const getViewConfigurations = useCallback(async (entity: string): Promise<ViewConfiguration[]> => {
17+
// Return empty array if feature is disabled
18+
if (!isViewConfigEnabled) {
19+
return []
20+
}
21+
1522
// Check cache first
1623
if (viewConfigurations.has(entity)) {
1724
return viewConfigurations.get(entity)!
@@ -44,9 +51,14 @@ export const ViewConfigurationProvider = ({ children }: PropsWithChildren) => {
4451

4552
fetchingRef.current.set(entity, fetchPromise)
4653
return fetchPromise
47-
}, [viewConfigurations, isLoading])
54+
}, [viewConfigurations, isLoading, isViewConfigEnabled])
4855

4956
const getActiveView = useCallback(async (entity: string): Promise<ViewConfiguration | null> => {
57+
// Return null if feature is disabled
58+
if (!isViewConfigEnabled) {
59+
return null
60+
}
61+
5062
// Check cache first
5163
if (activeViews.has(entity)) {
5264
return activeViews.get(entity)
@@ -63,9 +75,14 @@ export const ViewConfigurationProvider = ({ children }: PropsWithChildren) => {
6375
console.error("Failed to fetch active view configuration:", error)
6476
return null
6577
}
66-
}, [activeViews])
78+
}, [activeViews, isViewConfigEnabled])
6779

6880
const setActiveView = useCallback(async (entity: string, viewConfigurationId: string) => {
81+
// Do nothing if feature is disabled
82+
if (!isViewConfigEnabled) {
83+
return
84+
}
85+
6986
try {
7087
await sdk.admin.viewConfiguration.setActive({
7188
entity,
@@ -82,11 +99,16 @@ export const ViewConfigurationProvider = ({ children }: PropsWithChildren) => {
8299
console.error("Failed to set active view configuration:", error)
83100
toast.error("Failed to set active view")
84101
}
85-
}, [activeViews, getViewConfigurations])
102+
}, [activeViews, getViewConfigurations, isViewConfigEnabled])
86103

87104
const createViewConfiguration = useCallback(async (
88105
config: Omit<ViewConfiguration, "id" | "created_at" | "updated_at">
89106
): Promise<ViewConfiguration> => {
107+
// Throw error if feature is disabled
108+
if (!isViewConfigEnabled) {
109+
throw new Error("View configurations feature is not enabled")
110+
}
111+
90112
try {
91113
const response = await sdk.admin.viewConfiguration.create(config)
92114
const newConfig = response.view_configuration
@@ -101,12 +123,17 @@ export const ViewConfigurationProvider = ({ children }: PropsWithChildren) => {
101123
toast.error(errorMessage)
102124
throw error
103125
}
104-
}, [viewConfigurations])
126+
}, [viewConfigurations, isViewConfigEnabled])
105127

106128
const updateViewConfiguration = useCallback(async (
107129
id: string,
108130
config: Partial<ViewConfiguration>
109131
): Promise<ViewConfiguration> => {
132+
// Throw error if feature is disabled
133+
if (!isViewConfigEnabled) {
134+
throw new Error("View configurations feature is not enabled")
135+
}
136+
110137
try {
111138
const response = await sdk.admin.viewConfiguration.update(id, config)
112139
const updatedConfig = response.view_configuration
@@ -128,9 +155,14 @@ export const ViewConfigurationProvider = ({ children }: PropsWithChildren) => {
128155
toast.error(errorMessage)
129156
throw error
130157
}
131-
}, [viewConfigurations, activeViews])
158+
}, [viewConfigurations, activeViews, isViewConfigEnabled])
132159

133160
const deleteViewConfiguration = useCallback(async (id: string) => {
161+
// Throw error if feature is disabled
162+
if (!isViewConfigEnabled) {
163+
throw new Error("View configurations feature is not enabled")
164+
}
165+
134166
try {
135167
// First get the config to know which entity to invalidate
136168
const configs = Array.from(viewConfigurations.values()).flat()
@@ -152,7 +184,7 @@ export const ViewConfigurationProvider = ({ children }: PropsWithChildren) => {
152184
toast.error("Failed to delete view")
153185
throw error
154186
}
155-
}, [viewConfigurations, activeViews])
187+
}, [viewConfigurations, activeViews, isViewConfigEnabled])
156188

157189
const invalidateCache = useCallback((entity: string) => {
158190
viewConfigurations.delete(entity)

0 commit comments

Comments
 (0)