diff --git a/integration-tests/http/__tests__/feature-flags/admin/feature-flags.spec.ts b/integration-tests/http/__tests__/feature-flags/admin/feature-flags.spec.ts new file mode 100644 index 0000000000..8ac06dfb1a --- /dev/null +++ b/integration-tests/http/__tests__/feature-flags/admin/feature-flags.spec.ts @@ -0,0 +1,40 @@ +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { + adminHeaders, + createAdminUser, +} from "../../../../helpers/create-admin-user" + +jest.setTimeout(30000) + +medusaIntegrationTestRunner({ + testSuite: ({ dbConnection, getContainer, api }) => { + describe("GET /admin/feature-flags", () => { + it("should return feature flags when unauthenticated", async () => { + const response = await api.get("/admin/feature-flags") + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("feature_flags") + expect(response.data.feature_flags).toEqual( + expect.objectContaining({ + view_configurations: false, + }) + ) + }) + + it("should return feature flags when authenticated as admin", async () => { + const container = getContainer() + await createAdminUser(dbConnection, adminHeaders, container) + + const response = await api.get("/admin/feature-flags", adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("feature_flags") + expect(response.data.feature_flags).toEqual( + expect.objectContaining({ + view_configurations: false, + }) + ) + }) + }) + }, +}) diff --git a/packages/admin/dashboard/src/hooks/api/feature-flags.tsx b/packages/admin/dashboard/src/hooks/api/feature-flags.tsx new file mode 100644 index 0000000000..8b81583719 --- /dev/null +++ b/packages/admin/dashboard/src/hooks/api/feature-flags.tsx @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../../lib/client" + +export type FeatureFlags = { + view_configurations?: boolean + [key: string]: boolean | undefined +} + +export const useFeatureFlags = () => { + return useQuery({ + queryKey: ["admin", "feature-flags"], + queryFn: async () => { + const response = await sdk.client.fetch<{ feature_flags: FeatureFlags }>("/admin/feature-flags", { + method: "GET", + }) + + return response.feature_flags + }, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes + }) +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/providers/feature-flag-provider/index.tsx b/packages/admin/dashboard/src/providers/feature-flag-provider/index.tsx new file mode 100644 index 0000000000..86e8194cf9 --- /dev/null +++ b/packages/admin/dashboard/src/providers/feature-flag-provider/index.tsx @@ -0,0 +1,46 @@ +import React, { createContext, useContext } from "react" +import { useFeatureFlags, FeatureFlags } from "../../hooks/api/feature-flags" + +interface FeatureFlagContextValue { + flags: FeatureFlags + isLoading: boolean + isFeatureEnabled: (flag: keyof FeatureFlags) => boolean +} + +const FeatureFlagContext = createContext(null) + +export const useFeatureFlag = (flag: keyof FeatureFlags): boolean => { + const context = useContext(FeatureFlagContext) + if (!context) { + // If no context, assume feature is disabled + return false + } + return context.isFeatureEnabled(flag) +} + +export const useFeatureFlagContext = () => { + const context = useContext(FeatureFlagContext) + if (!context) { + throw new Error("useFeatureFlagContext must be used within FeatureFlagProvider") + } + return context +} + +interface FeatureFlagProviderProps { + children: React.ReactNode +} + +export const FeatureFlagProvider: React.FC = ({ children }) => { + const { data: flags = {}, isLoading, error } = useFeatureFlags() + + const isFeatureEnabled = (flag: keyof FeatureFlags): boolean => { + const enabled = flags[flag] === true + return enabled + } + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/providers/providers.tsx b/packages/admin/dashboard/src/providers/providers.tsx index 9386b4abcc..027eb97182 100644 --- a/packages/admin/dashboard/src/providers/providers.tsx +++ b/packages/admin/dashboard/src/providers/providers.tsx @@ -8,6 +8,7 @@ import { queryClient } from "../lib/query-client" import { ExtensionProvider } from "./extension-provider" import { I18nProvider } from "./i18n-provider" import { ThemeProvider } from "./theme-provider" +import { FeatureFlagProvider } from "./feature-flag-provider" type ProvidersProps = PropsWithChildren<{ api: DashboardApp["api"] @@ -20,9 +21,11 @@ export const Providers = ({ api, children }: ProvidersProps) => { - - {children} - + + + {children} + + diff --git a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/configurable-order-list-table.tsx b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/configurable-order-list-table.tsx new file mode 100644 index 0000000000..8d1fb07cb0 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/configurable-order-list-table.tsx @@ -0,0 +1,19 @@ +import { Container, Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +export const ConfigurableOrderListTable = () => { + const { t } = useTranslation() + + return ( + +
+ {t("orders.domain")} +
+
+

+ View configurations feature is enabled. Full implementation coming soon. +

+
+
+ ) +} \ No newline at end of file diff --git a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx index 99641a2fed..16be9f4ecb 100644 --- a/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx @@ -8,6 +8,8 @@ import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-ord import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters" import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query" import { useDataTable } from "../../../../../hooks/use-data-table" +import { useFeatureFlag } from "../../../../../providers/feature-flag-provider" +import { ConfigurableOrderListTable } from "./configurable-order-list-table" import { DEFAULT_FIELDS } from "../../const" @@ -15,6 +17,13 @@ const PAGE_SIZE = 20 export const OrderListTable = () => { const { t } = useTranslation() + const isViewConfigEnabled = useFeatureFlag("view_configurations") + + // If feature flag is enabled, use the new configurable table + if (isViewConfigEnabled) { + return + } + const { searchParams, raw } = useOrderTableQuery({ pageSize: PAGE_SIZE, }) diff --git a/packages/medusa/src/api/admin/feature-flags/route.ts b/packages/medusa/src/api/admin/feature-flags/route.ts new file mode 100644 index 0000000000..c8e75270a5 --- /dev/null +++ b/packages/medusa/src/api/admin/feature-flags/route.ts @@ -0,0 +1,26 @@ +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export const AUTHENTICATE = false + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse<{ feature_flags: Record }> +) => { + const featureFlagRouter = req.scope.resolve( + ContainerRegistrationKeys.FEATURE_FLAG_ROUTER + ) as any + + const flags = featureFlagRouter.listFlags() + + // Convert array of flags to a simple key-value object + const featureFlags: Record = {} + flags.forEach(flag => { + featureFlags[flag.key] = flag.value + }) + + res.json({ feature_flags: featureFlags }) +} \ No newline at end of file diff --git a/packages/medusa/src/loaders/feature-flags/view-configurations.ts b/packages/medusa/src/loaders/feature-flags/view-configurations.ts new file mode 100644 index 0000000000..3231ab12ef --- /dev/null +++ b/packages/medusa/src/loaders/feature-flags/view-configurations.ts @@ -0,0 +1,10 @@ +import { FlagSettings } from "@medusajs/framework/feature-flags" + +const ViewConfigurationsFeatureFlag: FlagSettings = { + key: "view_configurations", + default_val: false, + env_key: "MEDUSA_FF_VIEW_CONFIGURATIONS", + description: "[WIP] Enable view configurations for data tables", +} + +export default ViewConfigurationsFeatureFlag \ No newline at end of file