feat(api): add view configuration API routes (#13177)

* feat: add view_configurations feature flag

  - Add feature flag provider and hooks to admin dashboard
  - Add backend API endpoint for feature flags
  - Create view_configurations feature flag (disabled by default)
  - Update order list table to use legacy version when flag is disabled
  - Can be enabled with MEDUSA_FF_VIEW_CONFIGURATIONS=true env var

* fix: naming

* fix: feature flags unauthenticated

* fix: add test

* feat: add settings module

* fix: deps

* fix: cleanup

* fix: add more tetsts

* fix: rm changelog

* fix: deps

* fix: add settings module to default modules list

* feat(api): add view configuration API routes

- Add CRUD endpoints for view configurations
- Add active view configuration management endpoints
- Add feature flag middleware for view config routes
- Add comprehensive integration tests
- Add HTTP types for view configuration payloads and responses
- Support system defaults and user-specific configurations
- Enable setting views as active during create/update operations

* fix: test

* fix: test

* fix: test

* fix: change view configuration path

* fix: tests

* fix: remove manual settings module config from integration tests

* fix: container typing

* fix: workflows
This commit is contained in:
Sebastian Rindom
2025-08-15 13:17:52 +02:00
committed by GitHub
parent f7fc05307f
commit 12a38bcd2b
28 changed files with 1858 additions and 10 deletions
+1
View File
@@ -25,6 +25,7 @@ export * from "./region"
export * from "./reservation"
export * from "./return-reason"
export * from "./sales-channel"
export * from "./settings"
export * from "./shipping-options"
export * from "./shipping-profile"
export * from "./stock-location"
@@ -0,0 +1,2 @@
export * from "./steps"
export * from "./workflows"
@@ -0,0 +1,27 @@
import {
CreateViewConfigurationDTO,
} from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
export type CreateViewConfigurationStepInput = CreateViewConfigurationDTO
export const createViewConfigurationStepId = "create-view-configuration"
export const createViewConfigurationStep = createStep(
createViewConfigurationStepId,
async (data: CreateViewConfigurationStepInput, { container }) => {
const service = container.resolve(Modules.SETTINGS)
const created = await service.createViewConfigurations(data)
return new StepResponse(created, { id: created.id })
},
async (compensateInput, { container }) => {
if (!compensateInput?.id) {
return
}
const service = container.resolve(Modules.SETTINGS)
await service.deleteViewConfigurations([compensateInput.id])
}
)
@@ -0,0 +1,3 @@
export * from "./create-view-configuration"
export * from "./update-view-configuration"
export * from "./set-active-view-configuration"
@@ -0,0 +1,59 @@
import { ISettingsModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
export type SetActiveViewConfigurationStepInput = {
id: string
entity: string
user_id: string
}
export const setActiveViewConfigurationStepId = "set-active-view-configuration"
export const setActiveViewConfigurationStep = createStep(
setActiveViewConfigurationStepId,
async (input: SetActiveViewConfigurationStepInput, { container }) => {
const service = container.resolve<ISettingsModuleService>(Modules.SETTINGS)
// Get the currently active view configuration for rollback
const currentActiveView = await service.getActiveViewConfiguration(
input.entity,
input.user_id
)
// Set the new view as active
await service.setActiveViewConfiguration(
input.entity,
input.user_id,
input.id
)
return new StepResponse(input.id, {
entity: input.entity,
user_id: input.user_id,
previousActiveViewId: currentActiveView?.id || null,
})
},
async (compensateInput, { container }) => {
if (!compensateInput) {
return
}
const service = container.resolve<ISettingsModuleService>(Modules.SETTINGS)
if (compensateInput.previousActiveViewId) {
// Restore the previous active view
await service.setActiveViewConfiguration(
compensateInput.entity,
compensateInput.user_id,
compensateInput.previousActiveViewId
)
} else {
// If there was no previous active view, clear the active view
await service.clearActiveViewConfiguration(
compensateInput.entity,
compensateInput.user_id
)
}
}
)
@@ -0,0 +1,41 @@
import {
UpdateViewConfigurationDTO,
ISettingsModuleService,
ViewConfigurationDTO,
} from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
export type UpdateViewConfigurationStepInput = {
id: string
data: UpdateViewConfigurationDTO
}
export const updateViewConfigurationStepId = "update-view-configuration"
export const updateViewConfigurationStep = createStep(
updateViewConfigurationStepId,
async (input: UpdateViewConfigurationStepInput, { container }) => {
const service = container.resolve<ISettingsModuleService>(Modules.SETTINGS)
const currentState = await service.retrieveViewConfiguration(input.id)
const updated = await service.updateViewConfigurations(input.id, input.data)
return new StepResponse(updated, {
id: input.id,
previousState: currentState,
})
},
async (compensateInput, { container }) => {
if (!compensateInput?.id || !compensateInput?.previousState) {
return
}
const service = container.resolve<ISettingsModuleService>(Modules.SETTINGS)
const { id, created_at, updated_at, ...restoreData } =
compensateInput.previousState as ViewConfigurationDTO
await service.updateViewConfigurations(compensateInput.id, restoreData)
}
)
@@ -0,0 +1,42 @@
import {
CreateViewConfigurationDTO,
ViewConfigurationDTO,
} from "@medusajs/framework/types"
import {
WorkflowData,
WorkflowResponse,
createWorkflow,
when,
} from "@medusajs/framework/workflows-sdk"
import {
createViewConfigurationStep,
setActiveViewConfigurationStep,
} from "../steps"
export type CreateViewConfigurationWorkflowInput =
CreateViewConfigurationDTO & {
set_active?: boolean
}
export const createViewConfigurationWorkflowId = "create-view-configuration"
export const createViewConfigurationWorkflow = createWorkflow(
createViewConfigurationWorkflowId,
(
input: WorkflowData<CreateViewConfigurationWorkflowInput>
): WorkflowResponse<ViewConfigurationDTO> => {
const viewConfig = createViewConfigurationStep(input)
when({ input, viewConfig }, ({ input }) => {
return !!input.set_active && !!input.user_id
}).then(() => {
setActiveViewConfigurationStep({
id: viewConfig.id,
entity: viewConfig.entity,
user_id: input.user_id as string,
})
})
return new WorkflowResponse(viewConfig)
}
)
@@ -0,0 +1,2 @@
export * from "./create-view-configuration"
export * from "./update-view-configuration"
@@ -0,0 +1,51 @@
import {
UpdateViewConfigurationDTO,
ViewConfigurationDTO,
} from "@medusajs/framework/types"
import {
WorkflowData,
WorkflowResponse,
createWorkflow,
when,
transform,
} from "@medusajs/framework/workflows-sdk"
import {
updateViewConfigurationStep,
setActiveViewConfigurationStep,
} from "../steps"
export type UpdateViewConfigurationWorkflowInput = {
id: string
set_active?: boolean
} & UpdateViewConfigurationDTO
export const updateViewConfigurationWorkflowId = "update-view-configuration"
export const updateViewConfigurationWorkflow = createWorkflow(
updateViewConfigurationWorkflowId,
(
input: WorkflowData<UpdateViewConfigurationWorkflowInput>
): WorkflowResponse<ViewConfigurationDTO> => {
const updateData = transform({ input }, ({ input }) => {
const { id, set_active, ...data } = input
return data
})
const viewConfig = updateViewConfigurationStep({
id: input.id,
data: updateData,
})
when({ input, viewConfig }, ({ input, viewConfig }) => {
return !!input.set_active && !!viewConfig.user_id
}).then(() => {
setActiveViewConfigurationStep({
id: viewConfig.id,
entity: viewConfig.entity,
user_id: viewConfig.user_id as string,
})
})
return new WorkflowResponse(viewConfig)
}
)
@@ -21,6 +21,7 @@ import {
IPromotionModuleService,
IRegionModuleService,
ISalesChannelModuleService,
ISettingsModuleService,
IStockLocationService,
IStoreModuleService,
ITaxModuleService,
@@ -74,6 +75,7 @@ declare module "@medusajs/types" {
[Modules.FILE]: IFileModuleService
[Modules.NOTIFICATION]: INotificationModuleService
[Modules.LOCKING]: ILockingModule
[Modules.SETTINGS]: ISettingsModuleService
}
}
+1
View File
@@ -43,4 +43,5 @@ export * from "./tax-provider"
export * from "./tax-rate"
export * from "./tax-region"
export * from "./user"
export * from "./view-configuration"
export * from "./workflow-execution"
@@ -0,0 +1,3 @@
export * from "./responses"
export * from "./queries"
export * from "./payloads"
@@ -0,0 +1,108 @@
export interface AdminCreateViewConfiguration {
/**
* The entity this configuration is for (e.g., "order", "product").
*/
entity: string
/**
* The name of the view configuration.
*/
name?: string
/**
* Whether this is a system default configuration.
*/
is_system_default?: boolean
/**
* Whether to set this view as the active view after creation.
*/
set_active?: boolean
/**
* The view configuration settings.
*/
configuration: {
/**
* The list of visible column IDs.
*/
visible_columns: string[]
/**
* The order of columns.
*/
column_order: string[]
/**
* Custom column widths.
*/
column_widths?: Record<string, number>
/**
* Active filters for the view.
*/
filters?: Record<string, any>
/**
* Sorting configuration.
*/
sorting?: {
id: string
desc: boolean
} | null
/**
* Search query for the view.
*/
search?: string
}
}
export interface AdminUpdateViewConfiguration {
/**
* The name of the view configuration.
*/
name?: string
/**
* Whether this is a system default configuration.
*/
is_system_default?: boolean
/**
* Whether to set this view as the active view after update.
*/
set_active?: boolean
/**
* The view configuration settings.
*/
configuration?: {
/**
* The list of visible column IDs.
*/
visible_columns?: string[]
/**
* The order of columns.
*/
column_order?: string[]
/**
* Custom column widths.
*/
column_widths?: Record<string, number>
/**
* Active filters for the view.
*/
filters?: Record<string, any>
/**
* Sorting configuration.
*/
sorting?: {
id: string
desc: boolean
} | null
/**
* Search query for the view.
*/
search?: string
}
}
export interface AdminSetActiveViewConfiguration {
/**
* The entity to set the active view for.
*/
entity: string
/**
* The ID of the view configuration to set as active, or null to clear the active view.
*/
view_configuration_id: string | null
}
@@ -0,0 +1,37 @@
import { BaseFilterable, OperatorMap } from "../../../dal"
import { FindParams, SelectParams } from "../../common"
export interface AdminGetViewConfigurationParams extends SelectParams {}
export interface AdminGetViewConfigurationsParams
extends FindParams,
BaseFilterable<AdminGetViewConfigurationsParams> {
/**
* IDs to filter view configurations by.
*/
id?: string | string[]
/**
* Entity to filter by.
*/
entity?: string | string[]
/**
* Name to filter by.
*/
name?: string | string[]
/**
* User ID to filter by.
*/
user_id?: string | string[] | null
/**
* Filter by system default status.
*/
is_system_default?: boolean
/**
* Date filters for when the view configuration was created.
*/
created_at?: OperatorMap<string>
/**
* Date filters for when the view configuration was updated.
*/
updated_at?: OperatorMap<string>
}
@@ -0,0 +1,80 @@
import { DeleteResponse, PaginatedResponse } from "../../common"
interface AdminViewConfiguration {
/**
* The view configuration's ID.
*/
id: string
/**
* The entity this configuration is for (e.g., "order", "product").
*/
entity: string
/**
* The name of the view configuration.
*/
name: string | null
/**
* The ID of the user who owns this configuration, or null for system defaults.
*/
user_id: string | null
/**
* Whether this is a system default configuration.
*/
is_system_default: boolean
/**
* The view configuration settings.
*/
configuration: {
/**
* The list of visible column IDs.
*/
visible_columns: string[]
/**
* The order of columns.
*/
column_order: string[]
/**
* Custom column widths.
*/
column_widths?: Record<string, number>
/**
* Active filters for the view.
*/
filters?: Record<string, any>
/**
* Sorting configuration.
*/
sorting?: {
id: string
desc: boolean
} | null
/**
* Search query for the view.
*/
search?: string
}
/**
* The date the view configuration was created.
*/
created_at: Date
/**
* The date the view configuration was updated.
*/
updated_at: Date
}
export interface AdminViewConfigurationResponse {
/**
* The view configuration's details.
*/
view_configuration: AdminViewConfiguration | null
}
export type AdminViewConfigurationListResponse = PaginatedResponse<{
/**
* The list of view configurations.
*/
view_configurations: AdminViewConfiguration[]
}>
export type AdminViewConfigurationDeleteResponse = DeleteResponse<"view_configuration">
@@ -0,0 +1 @@
export * from "./admin"
+17 -2
View File
@@ -111,9 +111,9 @@ export interface UserPreferenceDTO {
}
/**
* The filters to apply on the retrieved view configurations.
* Partial filters for view configuration fields.
*/
export interface FilterableViewConfigurationProps extends BaseFilterable<ViewConfigurationDTO> {
export interface ViewConfigurationFilterableFields {
/**
* The IDs to filter by.
*/
@@ -140,6 +140,21 @@ export interface FilterableViewConfigurationProps extends BaseFilterable<ViewCon
name?: string | string[]
}
/**
* The filters to apply on the retrieved view configurations.
*/
export interface FilterableViewConfigurationProps extends ViewConfigurationFilterableFields {
/**
* An array of filters to apply on the entity, where each item in the array is joined with an "and" condition.
*/
$and?: (ViewConfigurationFilterableFields | FilterableViewConfigurationProps)[]
/**
* An array of filters to apply on the entity, where each item in the array is joined with an "or" condition.
*/
$or?: (ViewConfigurationFilterableFields | FilterableViewConfigurationProps)[]
}
/**
* The filters to apply on the retrieved user preferences.
*/
@@ -8,14 +8,14 @@ export interface CreateViewConfigurationDTO {
entity: string
/**
* The name of the configuration.
* The name of the configuration. Required unless creating a system default.
*/
name: string
name?: string
/**
* The user ID this configuration belongs to.
* The user ID this configuration belongs to. Can be null for system defaults.
*/
user_id?: string
user_id?: string | null
/**
* Whether this is a system default configuration.
@@ -131,4 +131,4 @@ export interface UpdateUserPreferenceDTO {
* The preference value.
*/
value: any
}
}