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:
@@ -22,16 +22,18 @@ export const adminHeaders = {
|
|||||||
export const createAdminUser = async (
|
export const createAdminUser = async (
|
||||||
dbConnection,
|
dbConnection,
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
container?
|
container?,
|
||||||
|
options?: { email?: string }
|
||||||
) => {
|
) => {
|
||||||
const appContainer = container ?? getContainer()!
|
const appContainer = container ?? getContainer()!
|
||||||
|
const email = options?.email ?? "admin@medusa.js"
|
||||||
|
|
||||||
const userModule: IUserModuleService = appContainer.resolve(Modules.USER)
|
const userModule: IUserModuleService = appContainer.resolve(Modules.USER)
|
||||||
const authModule: IAuthModuleService = appContainer.resolve(Modules.AUTH)
|
const authModule: IAuthModuleService = appContainer.resolve(Modules.AUTH)
|
||||||
const user = await userModule.createUsers({
|
const user = await userModule.createUsers({
|
||||||
first_name: "Admin",
|
first_name: "Admin",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
email: "admin@medusa.js",
|
email,
|
||||||
})
|
})
|
||||||
|
|
||||||
const hashConfig = { logN: 15, r: 8, p: 1 }
|
const hashConfig = { logN: 15, r: 8, p: 1 }
|
||||||
@@ -41,7 +43,7 @@ export const createAdminUser = async (
|
|||||||
provider_identities: [
|
provider_identities: [
|
||||||
{
|
{
|
||||||
provider: "emailpass",
|
provider: "emailpass",
|
||||||
entity_id: "admin@medusa.js",
|
entity_id: email,
|
||||||
provider_metadata: {
|
provider_metadata: {
|
||||||
password: passwordHash.toString("base64"),
|
password: passwordHash.toString("base64"),
|
||||||
},
|
},
|
||||||
|
|||||||
949
integration-tests/http/__tests__/view-configurations.spec.ts
Normal file
949
integration-tests/http/__tests__/view-configurations.spec.ts
Normal file
@@ -0,0 +1,949 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
|
import { adminHeaders, createAdminUser } from "../../helpers/create-admin-user"
|
||||||
|
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||||
|
|
||||||
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
const env = { MEDUSA_FF_MEDUSA_V2: true, MEDUSA_FF_VIEW_CONFIGURATIONS: true }
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
env,
|
||||||
|
testSuite: ({ dbConnection, api, getContainer }) => {
|
||||||
|
describe("View Configurations API", () => {
|
||||||
|
let adminHeader
|
||||||
|
let secondAdminHeader
|
||||||
|
let secondAdminUserId
|
||||||
|
let adminUserId
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
const { user: adminUser } = await createAdminUser(
|
||||||
|
dbConnection,
|
||||||
|
adminHeaders,
|
||||||
|
container
|
||||||
|
)
|
||||||
|
adminHeader = adminHeaders.headers
|
||||||
|
adminUserId = adminUser.id
|
||||||
|
|
||||||
|
// Create a second admin user
|
||||||
|
const secondAdminHeaders = { headers: {} }
|
||||||
|
const { user: secondAdminUser } = await createAdminUser(
|
||||||
|
dbConnection,
|
||||||
|
secondAdminHeaders,
|
||||||
|
container,
|
||||||
|
{ email: "admin2@test.com" }
|
||||||
|
)
|
||||||
|
secondAdminUserId = secondAdminUser.id
|
||||||
|
secondAdminHeader = secondAdminHeaders.headers
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/views/{entity}/configurations", () => {
|
||||||
|
it("should create a personal view configuration", async () => {
|
||||||
|
const payload = {
|
||||||
|
name: "My Order View",
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "display_id", "created_at"],
|
||||||
|
column_order: ["display_id", "id", "created_at"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
"/admin/views/orders/configurations",
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.view_configuration).toMatchObject({
|
||||||
|
entity: "orders",
|
||||||
|
name: "My Order View",
|
||||||
|
user_id: secondAdminUserId,
|
||||||
|
configuration: payload.configuration,
|
||||||
|
})
|
||||||
|
expect(response.data.view_configuration.is_system_default).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create a system default view as admin", async () => {
|
||||||
|
const payload = {
|
||||||
|
name: "Default Order View",
|
||||||
|
is_system_default: true,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "display_id", "created_at", "total"],
|
||||||
|
column_order: ["display_id", "created_at", "total", "id"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
"/admin/views/orders/configurations",
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
headers: adminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.view_configuration).toMatchObject({
|
||||||
|
entity: "orders",
|
||||||
|
name: "Default Order View",
|
||||||
|
user_id: null,
|
||||||
|
is_system_default: true,
|
||||||
|
configuration: payload.configuration,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /admin/views/{entity}/configurations", () => {
|
||||||
|
let systemView
|
||||||
|
let personalView
|
||||||
|
let otherUserView
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
const settingsService = container.resolve("settings")
|
||||||
|
|
||||||
|
// Create system default view
|
||||||
|
systemView = await settingsService.createViewConfigurations({
|
||||||
|
entity: "orders",
|
||||||
|
name: "System Default",
|
||||||
|
is_system_default: true,
|
||||||
|
user_id: null,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "display_id"],
|
||||||
|
column_order: ["display_id", "id"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create personal view for non-admin user
|
||||||
|
personalView = await settingsService.createViewConfigurations({
|
||||||
|
entity: "orders",
|
||||||
|
name: "My Personal View",
|
||||||
|
is_system_default: false,
|
||||||
|
user_id: secondAdminUserId,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "total"],
|
||||||
|
column_order: ["total", "id"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create view for another user
|
||||||
|
otherUserView = await settingsService.createViewConfigurations({
|
||||||
|
entity: "orders",
|
||||||
|
name: "Other User View",
|
||||||
|
is_system_default: false,
|
||||||
|
user_id: "other-user-id",
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id"],
|
||||||
|
column_order: ["id"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should list system defaults and personal views", async () => {
|
||||||
|
const response = await api.get("/admin/views/orders/configurations", {
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.view_configurations).toHaveLength(2)
|
||||||
|
expect(response.data.view_configurations).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: systemView.id }),
|
||||||
|
expect.objectContaining({ id: personalView.id }),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
// Should not include other user's view
|
||||||
|
expect(response.data.view_configurations).not.toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: otherUserView.id }),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should filter by entity", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
"/admin/views/products/configurations",
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.view_configurations).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /admin/views/{entity}/configurations/:id", () => {
|
||||||
|
let viewConfig
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
const settingsService = container.resolve("settings")
|
||||||
|
|
||||||
|
viewConfig = await settingsService.createViewConfigurations({
|
||||||
|
entity: "orders",
|
||||||
|
name: "Test View",
|
||||||
|
is_system_default: false,
|
||||||
|
user_id: secondAdminUserId,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id"],
|
||||||
|
column_order: ["id"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should retrieve own view configuration", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
`/admin/views/orders/configurations/${viewConfig.id}`,
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.view_configuration).toMatchObject({
|
||||||
|
id: viewConfig.id,
|
||||||
|
entity: "orders",
|
||||||
|
name: "Test View",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should prevent access to other user's view", async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
const settingsService = container.resolve("settings")
|
||||||
|
|
||||||
|
const otherView = await settingsService.createViewConfigurations({
|
||||||
|
entity: "orders",
|
||||||
|
name: "Other View",
|
||||||
|
is_system_default: false,
|
||||||
|
user_id: "other-user-id",
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id"],
|
||||||
|
column_order: ["id"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.get(`/admin/views/orders/configurations/${otherView.id}`, {
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
})
|
||||||
|
.catch((e) => e.response)
|
||||||
|
|
||||||
|
expect(response.status).toBe(400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/views/{entity}/configurations/:id", () => {
|
||||||
|
let viewConfig
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
const settingsService = container.resolve("settings")
|
||||||
|
|
||||||
|
viewConfig = await settingsService.createViewConfigurations({
|
||||||
|
entity: "orders",
|
||||||
|
name: "Test View",
|
||||||
|
is_system_default: false,
|
||||||
|
user_id: secondAdminUserId,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id"],
|
||||||
|
column_order: ["id"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should update own view configuration", async () => {
|
||||||
|
const payload = {
|
||||||
|
name: "Updated View",
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "total"],
|
||||||
|
column_order: ["total", "id"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/views/orders/configurations/${viewConfig.id}`,
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.view_configuration).toMatchObject({
|
||||||
|
id: viewConfig.id,
|
||||||
|
name: "Updated View",
|
||||||
|
configuration: payload.configuration,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("DELETE /admin/views/{entity}/configurations/:id", () => {
|
||||||
|
let viewConfig
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
const settingsService = container.resolve("settings")
|
||||||
|
|
||||||
|
viewConfig = await settingsService.createViewConfigurations({
|
||||||
|
entity: "orders",
|
||||||
|
name: "Test View",
|
||||||
|
is_system_default: false,
|
||||||
|
user_id: secondAdminUserId,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id"],
|
||||||
|
column_order: ["id"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should delete own view configuration", async () => {
|
||||||
|
const response = await api.delete(
|
||||||
|
`/admin/views/orders/configurations/${viewConfig.id}`,
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toMatchObject({
|
||||||
|
id: viewConfig.id,
|
||||||
|
object: "view_configuration",
|
||||||
|
deleted: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify it's deleted
|
||||||
|
const getResponse = await api
|
||||||
|
.get(`/admin/views/orders/configurations/${viewConfig.id}`, {
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
})
|
||||||
|
.catch((e) => e.response)
|
||||||
|
|
||||||
|
expect(getResponse.status).toBe(404)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /admin/views/{entity}/configurations/active", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
const settingsService = container.resolve("settings")
|
||||||
|
|
||||||
|
// Create and set active view
|
||||||
|
const viewConfig = await settingsService.createViewConfigurations({
|
||||||
|
entity: "orders",
|
||||||
|
name: "Active View",
|
||||||
|
is_system_default: false,
|
||||||
|
user_id: secondAdminUserId,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "total"],
|
||||||
|
column_order: ["total", "id"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await settingsService.setActiveViewConfiguration(
|
||||||
|
"orders",
|
||||||
|
secondAdminUserId,
|
||||||
|
viewConfig.id
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should retrieve active view configuration", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
"/admin/views/orders/configurations/active",
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.view_configuration).toMatchObject({
|
||||||
|
entity: "orders",
|
||||||
|
name: "Active View",
|
||||||
|
user_id: secondAdminUserId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return null when no active view", async () => {
|
||||||
|
const response = await api.get(
|
||||||
|
"/admin/views/products/configurations/active",
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.view_configuration).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/views/{entity}/configurations/active", () => {
|
||||||
|
let viewConfig
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
const settingsService = container.resolve("settings")
|
||||||
|
|
||||||
|
viewConfig = await settingsService.createViewConfigurations({
|
||||||
|
entity: "orders",
|
||||||
|
name: "Test View",
|
||||||
|
is_system_default: false,
|
||||||
|
user_id: secondAdminUserId,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id"],
|
||||||
|
column_order: ["id"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should set active view configuration", async () => {
|
||||||
|
const response = await api.post(
|
||||||
|
"/admin/views/orders/configurations/active",
|
||||||
|
{
|
||||||
|
view_configuration_id: viewConfig.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.success).toBe(true)
|
||||||
|
|
||||||
|
// Verify it's active
|
||||||
|
const activeResponse = await api.get(
|
||||||
|
"/admin/views/orders/configurations/active",
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(activeResponse.data.view_configuration.id).toBe(viewConfig.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should clear active view and return to default when setting view_configuration_id to null", async () => {
|
||||||
|
// First set an active view
|
||||||
|
await api.post(
|
||||||
|
"/admin/views/orders/configurations/active",
|
||||||
|
{
|
||||||
|
view_configuration_id: viewConfig.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify it's active
|
||||||
|
let activeResponse = await api.get(
|
||||||
|
"/admin/views/orders/configurations/active",
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(activeResponse.data.view_configuration.id).toBe(viewConfig.id)
|
||||||
|
|
||||||
|
// Now clear the active view
|
||||||
|
const clearResponse = await api.post(
|
||||||
|
"/admin/views/orders/configurations/active",
|
||||||
|
{
|
||||||
|
view_configuration_id: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(clearResponse.status).toBe(200)
|
||||||
|
expect(clearResponse.data.success).toBe(true)
|
||||||
|
|
||||||
|
// Verify the active view is cleared
|
||||||
|
activeResponse = await api.get(
|
||||||
|
"/admin/views/orders/configurations/active",
|
||||||
|
{
|
||||||
|
headers: secondAdminHeader,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Debug output
|
||||||
|
if (activeResponse.data.view_configuration) {
|
||||||
|
console.log("Active view after clearing:", {
|
||||||
|
id: activeResponse.data.view_configuration.id,
|
||||||
|
name: activeResponse.data.view_configuration.name,
|
||||||
|
is_system_default:
|
||||||
|
activeResponse.data.view_configuration.is_system_default,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should either return null or a system default if one exists
|
||||||
|
if (activeResponse.data.view_configuration) {
|
||||||
|
expect(
|
||||||
|
activeResponse.data.view_configuration.is_system_default
|
||||||
|
).toBe(true)
|
||||||
|
} else {
|
||||||
|
expect(activeResponse.data.view_configuration).toBeNull()
|
||||||
|
}
|
||||||
|
expect(activeResponse.data.is_default_active).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("System Default Views", () => {
|
||||||
|
it("should make system default views available to all users", async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
|
||||||
|
// Create a third admin user
|
||||||
|
const thirdAdminHeaders = { headers: {} }
|
||||||
|
const { user: thirdAdminUser } = await createAdminUser(
|
||||||
|
dbConnection,
|
||||||
|
thirdAdminHeaders,
|
||||||
|
container,
|
||||||
|
{ email: "admin3@test.com" }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Admin 1 creates a system default view
|
||||||
|
const systemDefaultView = await api.post(
|
||||||
|
"/admin/views/orders/configurations",
|
||||||
|
{
|
||||||
|
name: "System Default View",
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "display_id", "created_at"],
|
||||||
|
column_order: ["display_id", "id", "created_at"],
|
||||||
|
},
|
||||||
|
is_system_default: true,
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(systemDefaultView.status).toEqual(200)
|
||||||
|
expect(systemDefaultView.data.view_configuration.user_id).toBeNull()
|
||||||
|
|
||||||
|
// Admin 3 should be able to see this view
|
||||||
|
const viewsForAdmin3 = await api.get(
|
||||||
|
"/admin/views/orders/configurations",
|
||||||
|
thirdAdminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(viewsForAdmin3.status).toEqual(200)
|
||||||
|
const systemDefaults = viewsForAdmin3.data.view_configurations.filter(
|
||||||
|
(v: any) => v.is_system_default
|
||||||
|
)
|
||||||
|
expect(systemDefaults).toHaveLength(1)
|
||||||
|
expect(systemDefaults[0].name).toEqual("System Default View")
|
||||||
|
|
||||||
|
// Admin 3 should also be able to retrieve it directly
|
||||||
|
const directRetrieve = await api.get(
|
||||||
|
`/admin/views/orders/configurations/${systemDefaultView.data.view_configuration.id}`,
|
||||||
|
thirdAdminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(directRetrieve.status).toEqual(200)
|
||||||
|
expect(directRetrieve.data.view_configuration.name).toEqual(
|
||||||
|
"System Default View"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow creating system default without name", async () => {
|
||||||
|
// Create a system default view without providing a name
|
||||||
|
const systemDefaultView = await api.post(
|
||||||
|
"/admin/views/customers/configurations",
|
||||||
|
{
|
||||||
|
is_system_default: true,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "email", "first_name", "last_name"],
|
||||||
|
column_order: ["email", "first_name", "last_name", "id"],
|
||||||
|
},
|
||||||
|
// Note: no name field
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(systemDefaultView.status).toEqual(200)
|
||||||
|
expect(systemDefaultView.data.view_configuration.user_id).toBeNull()
|
||||||
|
expect(
|
||||||
|
systemDefaultView.data.view_configuration.is_system_default
|
||||||
|
).toBe(true)
|
||||||
|
// Name should be undefined/null when not provided
|
||||||
|
expect(systemDefaultView.data.view_configuration.name).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should set view as active when created with set_active flag", async () => {
|
||||||
|
// Create a view with set_active = true
|
||||||
|
const viewConfig = await api.post(
|
||||||
|
"/admin/views/orders/configurations",
|
||||||
|
{
|
||||||
|
name: "Auto-Active View",
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "display_id", "status"],
|
||||||
|
column_order: ["display_id", "status", "id"],
|
||||||
|
},
|
||||||
|
set_active: true,
|
||||||
|
},
|
||||||
|
{ headers: secondAdminHeader }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(viewConfig.status).toEqual(200)
|
||||||
|
|
||||||
|
// Verify the view is now active
|
||||||
|
const activeView = await api.get(
|
||||||
|
"/admin/views/orders/configurations/active",
|
||||||
|
{ headers: secondAdminHeader }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(activeView.status).toEqual(200)
|
||||||
|
expect(activeView.data.view_configuration).toBeTruthy()
|
||||||
|
expect(activeView.data.view_configuration.id).toEqual(
|
||||||
|
viewConfig.data.view_configuration.id
|
||||||
|
)
|
||||||
|
expect(activeView.data.view_configuration.name).toEqual(
|
||||||
|
"Auto-Active View"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should set view as active when updated with set_active flag", async () => {
|
||||||
|
const container = getContainer()
|
||||||
|
const settingsService = container.resolve("settings")
|
||||||
|
|
||||||
|
// Create two views
|
||||||
|
const view1 = await settingsService.createViewConfigurations({
|
||||||
|
entity: "orders",
|
||||||
|
name: "View 1",
|
||||||
|
is_system_default: false,
|
||||||
|
user_id: secondAdminUserId,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id"],
|
||||||
|
column_order: ["id"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const view2 = await settingsService.createViewConfigurations({
|
||||||
|
entity: "orders",
|
||||||
|
name: "View 2",
|
||||||
|
is_system_default: false,
|
||||||
|
user_id: secondAdminUserId,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "total"],
|
||||||
|
column_order: ["total", "id"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set view1 as active initially
|
||||||
|
await settingsService.setActiveViewConfiguration(
|
||||||
|
"orders",
|
||||||
|
secondAdminUserId,
|
||||||
|
view1.id
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update view2 with set_active flag
|
||||||
|
const updateResponse = await api.post(
|
||||||
|
`/admin/views/orders/configurations/${view2.id}`,
|
||||||
|
{
|
||||||
|
name: "Updated View 2",
|
||||||
|
set_active: true,
|
||||||
|
},
|
||||||
|
{ headers: secondAdminHeader }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updateResponse.status).toEqual(200)
|
||||||
|
|
||||||
|
// Verify view2 is now the active view
|
||||||
|
const activeView = await api.get(
|
||||||
|
"/admin/views/orders/configurations/active",
|
||||||
|
{ headers: secondAdminHeader }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(activeView.status).toEqual(200)
|
||||||
|
expect(activeView.data.view_configuration.id).toEqual(view2.id)
|
||||||
|
expect(activeView.data.view_configuration.name).toEqual(
|
||||||
|
"Updated View 2"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow resetting system default to code-level defaults", async () => {
|
||||||
|
// Create a system default view
|
||||||
|
const systemDefaultView = await api.post(
|
||||||
|
"/admin/views/orders/configurations",
|
||||||
|
{
|
||||||
|
name: "Custom System Default",
|
||||||
|
is_system_default: true,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "status", "total"],
|
||||||
|
column_order: ["status", "total", "id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(systemDefaultView.status).toEqual(200)
|
||||||
|
const viewId = systemDefaultView.data.view_configuration.id
|
||||||
|
|
||||||
|
// Verify it exists
|
||||||
|
let viewsList = await api.get(
|
||||||
|
"/admin/views/orders/configurations",
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
viewsList.data.view_configurations.some((v: any) => v.id === viewId)
|
||||||
|
).toBe(true)
|
||||||
|
|
||||||
|
// Delete the system default view (reset to code defaults)
|
||||||
|
const deleteResponse = await api.delete(
|
||||||
|
`/admin/views/orders/configurations/${viewId}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(deleteResponse.status).toEqual(200)
|
||||||
|
expect(deleteResponse.data.deleted).toBe(true)
|
||||||
|
|
||||||
|
// Verify it's gone
|
||||||
|
viewsList = await api.get(
|
||||||
|
"/admin/views/orders/configurations",
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
viewsList.data.view_configurations.some((v: any) => v.id === viewId)
|
||||||
|
).toBe(false)
|
||||||
|
|
||||||
|
// Getting active view should return null (falls back to code defaults)
|
||||||
|
const activeView = await api.get(
|
||||||
|
"/admin/views/orders/configurations/active",
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
expect(activeView.data.view_configuration).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return system default view when created and no user view is active", async () => {
|
||||||
|
// Step 1: Create a system default view
|
||||||
|
const systemDefaultView = await api.post(
|
||||||
|
"/admin/views/orders/configurations",
|
||||||
|
{
|
||||||
|
name: "System Default Orders",
|
||||||
|
is_system_default: true,
|
||||||
|
configuration: {
|
||||||
|
visible_columns: [
|
||||||
|
"id",
|
||||||
|
"display_id",
|
||||||
|
"created_at",
|
||||||
|
"customer",
|
||||||
|
"total",
|
||||||
|
],
|
||||||
|
column_order: [
|
||||||
|
"display_id",
|
||||||
|
"customer",
|
||||||
|
"total",
|
||||||
|
"created_at",
|
||||||
|
"id",
|
||||||
|
],
|
||||||
|
filters: {},
|
||||||
|
sorting: { id: "created_at", desc: true },
|
||||||
|
search: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(systemDefaultView.status).toEqual(200)
|
||||||
|
expect(
|
||||||
|
systemDefaultView.data.view_configuration.is_system_default
|
||||||
|
).toBe(true)
|
||||||
|
|
||||||
|
// Step 2: Retrieve active view - should return the system default
|
||||||
|
const activeView = await api.get(
|
||||||
|
"/admin/views/orders/configurations/active",
|
||||||
|
{ headers: secondAdminHeader }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(activeView.status).toEqual(200)
|
||||||
|
expect(activeView.data.view_configuration).toBeTruthy()
|
||||||
|
expect(activeView.data.view_configuration.id).toEqual(
|
||||||
|
systemDefaultView.data.view_configuration.id
|
||||||
|
)
|
||||||
|
expect(activeView.data.view_configuration.name).toEqual(
|
||||||
|
"System Default Orders"
|
||||||
|
)
|
||||||
|
expect(activeView.data.view_configuration.is_system_default).toBe(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
expect(activeView.data.is_default_active).toBe(true)
|
||||||
|
expect(activeView.data.default_type).toEqual("system")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Filter, Sorting, and Search Persistence", () => {
|
||||||
|
it("should save and restore filters, sorting, and search configuration", async () => {
|
||||||
|
// Create a view with filters, sorting, and search
|
||||||
|
const viewConfig = await api.post(
|
||||||
|
"/admin/views/orders/configurations",
|
||||||
|
{
|
||||||
|
name: "Filtered View",
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "status", "total", "created_at"],
|
||||||
|
column_order: ["status", "total", "created_at", "id"],
|
||||||
|
filters: {
|
||||||
|
status: ["pending", "completed"],
|
||||||
|
total: { gte: 100 },
|
||||||
|
},
|
||||||
|
sorting: { id: "created_at", desc: true },
|
||||||
|
search: "test search",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ headers: secondAdminHeader }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(viewConfig.status).toEqual(200)
|
||||||
|
expect(
|
||||||
|
viewConfig.data.view_configuration.configuration.filters
|
||||||
|
).toEqual({
|
||||||
|
status: ["pending", "completed"],
|
||||||
|
total: { gte: 100 },
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
viewConfig.data.view_configuration.configuration.sorting
|
||||||
|
).toEqual({
|
||||||
|
id: "created_at",
|
||||||
|
desc: true,
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
viewConfig.data.view_configuration.configuration.search
|
||||||
|
).toEqual("test search")
|
||||||
|
|
||||||
|
// Retrieve the view and verify filters are preserved
|
||||||
|
const getResponse = await api.get(
|
||||||
|
`/admin/views/orders/configurations/${viewConfig.data.view_configuration.id}`,
|
||||||
|
{ headers: secondAdminHeader }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(getResponse.status).toEqual(200)
|
||||||
|
expect(
|
||||||
|
getResponse.data.view_configuration.configuration.filters
|
||||||
|
).toEqual({
|
||||||
|
status: ["pending", "completed"],
|
||||||
|
total: { gte: 100 },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should remove filters when updating a view without filters", async () => {
|
||||||
|
// Create a view with filters
|
||||||
|
const viewConfig = await api.post(
|
||||||
|
"/admin/views/orders/configurations",
|
||||||
|
{
|
||||||
|
name: "View with Filters",
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "status", "total"],
|
||||||
|
column_order: ["status", "total", "id"],
|
||||||
|
filters: {
|
||||||
|
status: ["pending", "completed"],
|
||||||
|
total: { gte: 100 },
|
||||||
|
},
|
||||||
|
sorting: { id: "total", desc: true },
|
||||||
|
search: "initial search",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ headers: secondAdminHeader }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(viewConfig.status).toEqual(200)
|
||||||
|
const viewId = viewConfig.data.view_configuration.id
|
||||||
|
|
||||||
|
// Update the view to remove filters
|
||||||
|
const updateResponse = await api.post(
|
||||||
|
`/admin/views/orders/configurations/${viewId}`,
|
||||||
|
{
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "status", "total"],
|
||||||
|
column_order: ["status", "total", "id"],
|
||||||
|
filters: {}, // Empty filters object
|
||||||
|
sorting: null, // Remove sorting
|
||||||
|
search: "", // Clear search
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ headers: secondAdminHeader }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updateResponse.status).toEqual(200)
|
||||||
|
|
||||||
|
// Verify filters were removed
|
||||||
|
expect(
|
||||||
|
updateResponse.data.view_configuration.configuration.filters
|
||||||
|
).toEqual({})
|
||||||
|
expect(
|
||||||
|
updateResponse.data.view_configuration.configuration.sorting
|
||||||
|
).toBeNull()
|
||||||
|
expect(
|
||||||
|
updateResponse.data.view_configuration.configuration.search
|
||||||
|
).toEqual("")
|
||||||
|
|
||||||
|
// Retrieve again to double-check persistence
|
||||||
|
const getResponse = await api.get(
|
||||||
|
`/admin/views/orders/configurations/${viewId}`,
|
||||||
|
{ headers: secondAdminHeader }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(getResponse.status).toEqual(200)
|
||||||
|
expect(
|
||||||
|
getResponse.data.view_configuration.configuration.filters
|
||||||
|
).toEqual({})
|
||||||
|
expect(
|
||||||
|
getResponse.data.view_configuration.configuration.sorting
|
||||||
|
).toBeNull()
|
||||||
|
expect(
|
||||||
|
getResponse.data.view_configuration.configuration.search
|
||||||
|
).toEqual("")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should update only specific filters while keeping others", async () => {
|
||||||
|
// Create a view with multiple filters
|
||||||
|
const viewConfig = await api.post(
|
||||||
|
"/admin/views/orders/configurations",
|
||||||
|
{
|
||||||
|
name: "Multi-Filter View",
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "status", "total", "created_at"],
|
||||||
|
column_order: ["status", "total", "created_at", "id"],
|
||||||
|
filters: {
|
||||||
|
status: ["pending", "completed"],
|
||||||
|
total: { gte: 100, lte: 1000 },
|
||||||
|
created_at: { gte: "2024-01-01" },
|
||||||
|
},
|
||||||
|
sorting: { id: "created_at", desc: true },
|
||||||
|
search: "customer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ headers: secondAdminHeader }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(viewConfig.status).toEqual(200)
|
||||||
|
const viewId = viewConfig.data.view_configuration.id
|
||||||
|
|
||||||
|
// Update to remove only the 'total' filter
|
||||||
|
const updateResponse = await api.post(
|
||||||
|
`/admin/views/orders/configurations/${viewId}`,
|
||||||
|
{
|
||||||
|
configuration: {
|
||||||
|
visible_columns: ["id", "status", "total", "created_at"],
|
||||||
|
column_order: ["status", "total", "created_at", "id"],
|
||||||
|
filters: {
|
||||||
|
status: ["pending", "completed"],
|
||||||
|
created_at: { gte: "2024-01-01" },
|
||||||
|
// 'total' filter removed
|
||||||
|
},
|
||||||
|
sorting: { id: "created_at", desc: true },
|
||||||
|
search: "customer",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ headers: secondAdminHeader }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(updateResponse.status).toEqual(200)
|
||||||
|
expect(
|
||||||
|
updateResponse.data.view_configuration.configuration.filters
|
||||||
|
).toEqual({
|
||||||
|
status: ["pending", "completed"],
|
||||||
|
created_at: { gte: "2024-01-01" },
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
updateResponse.data.view_configuration.configuration.filters.total
|
||||||
|
).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -25,6 +25,7 @@ export * from "./region"
|
|||||||
export * from "./reservation"
|
export * from "./reservation"
|
||||||
export * from "./return-reason"
|
export * from "./return-reason"
|
||||||
export * from "./sales-channel"
|
export * from "./sales-channel"
|
||||||
|
export * from "./settings"
|
||||||
export * from "./shipping-options"
|
export * from "./shipping-options"
|
||||||
export * from "./shipping-profile"
|
export * from "./shipping-profile"
|
||||||
export * from "./stock-location"
|
export * from "./stock-location"
|
||||||
|
|||||||
2
packages/core/core-flows/src/settings/index.ts
Normal file
2
packages/core/core-flows/src/settings/index.ts
Normal file
@@ -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])
|
||||||
|
}
|
||||||
|
)
|
||||||
3
packages/core/core-flows/src/settings/steps/index.ts
Normal file
3
packages/core/core-flows/src/settings/steps/index.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
2
packages/core/core-flows/src/settings/workflows/index.ts
Normal file
2
packages/core/core-flows/src/settings/workflows/index.ts
Normal file
@@ -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,
|
IPromotionModuleService,
|
||||||
IRegionModuleService,
|
IRegionModuleService,
|
||||||
ISalesChannelModuleService,
|
ISalesChannelModuleService,
|
||||||
|
ISettingsModuleService,
|
||||||
IStockLocationService,
|
IStockLocationService,
|
||||||
IStoreModuleService,
|
IStoreModuleService,
|
||||||
ITaxModuleService,
|
ITaxModuleService,
|
||||||
@@ -74,6 +75,7 @@ declare module "@medusajs/types" {
|
|||||||
[Modules.FILE]: IFileModuleService
|
[Modules.FILE]: IFileModuleService
|
||||||
[Modules.NOTIFICATION]: INotificationModuleService
|
[Modules.NOTIFICATION]: INotificationModuleService
|
||||||
[Modules.LOCKING]: ILockingModule
|
[Modules.LOCKING]: ILockingModule
|
||||||
|
[Modules.SETTINGS]: ISettingsModuleService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,4 +43,5 @@ export * from "./tax-provider"
|
|||||||
export * from "./tax-rate"
|
export * from "./tax-rate"
|
||||||
export * from "./tax-region"
|
export * from "./tax-region"
|
||||||
export * from "./user"
|
export * from "./user"
|
||||||
|
export * from "./view-configuration"
|
||||||
export * from "./workflow-execution"
|
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">
|
||||||
1
packages/core/types/src/http/view-configuration/index.ts
Normal file
1
packages/core/types/src/http/view-configuration/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./admin"
|
||||||
@@ -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.
|
* The IDs to filter by.
|
||||||
*/
|
*/
|
||||||
@@ -140,6 +140,21 @@ export interface FilterableViewConfigurationProps extends BaseFilterable<ViewCon
|
|||||||
name?: string | string[]
|
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.
|
* The filters to apply on the retrieved user preferences.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ export interface CreateViewConfigurationDTO {
|
|||||||
entity: string
|
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.
|
* Whether this is a system default configuration.
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "@medusajs/framework/http"
|
||||||
|
import { AdminUpdateViewConfigurationType } from "../validators"
|
||||||
|
import { HttpTypes } from "@medusajs/framework/types"
|
||||||
|
import { MedusaError, Modules } from "@medusajs/framework/utils"
|
||||||
|
import { updateViewConfigurationWorkflow } from "@medusajs/core-flows"
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
req: AuthenticatedMedusaRequest<HttpTypes.AdminGetViewConfigurationParams>,
|
||||||
|
res: MedusaResponse<HttpTypes.AdminViewConfigurationResponse>
|
||||||
|
) => {
|
||||||
|
const settingsService = req.scope.resolve(Modules.SETTINGS)
|
||||||
|
|
||||||
|
const viewConfiguration = await settingsService.retrieveViewConfiguration(
|
||||||
|
req.params.id,
|
||||||
|
req.queryConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
viewConfiguration.user_id &&
|
||||||
|
viewConfiguration.user_id !== req.auth_context.actor_id &&
|
||||||
|
!req.auth_context.app_metadata?.admin
|
||||||
|
) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
"You don't have access to this view configuration"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ view_configuration: viewConfiguration })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: AuthenticatedMedusaRequest<AdminUpdateViewConfigurationType>,
|
||||||
|
res: MedusaResponse<HttpTypes.AdminViewConfigurationResponse>
|
||||||
|
) => {
|
||||||
|
const settingsService = req.scope.resolve(Modules.SETTINGS)
|
||||||
|
|
||||||
|
// Single retrieval for permission check
|
||||||
|
const existing = await settingsService.retrieveViewConfiguration(
|
||||||
|
req.params.id,
|
||||||
|
{ select: ["id", "user_id", "is_system_default"] }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existing.user_id && existing.user_id !== req.auth_context.actor_id) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
"You can only update your own view configurations"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
id: req.params.id,
|
||||||
|
...req.validatedBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { result } = await updateViewConfigurationWorkflow(req.scope).run({
|
||||||
|
input,
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({ view_configuration: result })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DELETE = async (
|
||||||
|
req: AuthenticatedMedusaRequest,
|
||||||
|
res: MedusaResponse<HttpTypes.AdminViewConfigurationDeleteResponse>
|
||||||
|
) => {
|
||||||
|
const settingsService = req.scope.resolve(Modules.SETTINGS)
|
||||||
|
|
||||||
|
// Retrieve existing to check permissions
|
||||||
|
const existing = await settingsService.retrieveViewConfiguration(
|
||||||
|
req.params.id,
|
||||||
|
{ select: ["id", "user_id", "is_system_default", "entity", "name"] }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existing.user_id && existing.user_id !== req.auth_context.actor_id) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
"You can only delete your own view configurations"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await settingsService.deleteViewConfigurations(req.params.id)
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
id: req.params.id,
|
||||||
|
object: "view_configuration",
|
||||||
|
deleted: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "@medusajs/framework/http"
|
||||||
|
import {
|
||||||
|
AdminSetActiveViewConfigurationType,
|
||||||
|
AdminGetActiveViewConfigurationParamsType,
|
||||||
|
} from "../validators"
|
||||||
|
import { HttpTypes } from "@medusajs/framework/types"
|
||||||
|
import { Modules } from "@medusajs/framework/utils"
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
req: AuthenticatedMedusaRequest<AdminGetActiveViewConfigurationParamsType>,
|
||||||
|
res: MedusaResponse<
|
||||||
|
HttpTypes.AdminViewConfigurationResponse & {
|
||||||
|
is_default_active?: boolean
|
||||||
|
default_type?: "system" | "code"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const settingsService = req.scope.resolve(Modules.SETTINGS)
|
||||||
|
|
||||||
|
const viewConfiguration = await settingsService.getActiveViewConfiguration(
|
||||||
|
req.params.entity,
|
||||||
|
req.auth_context.actor_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!viewConfiguration) {
|
||||||
|
// No active view set or explicitly cleared - return null
|
||||||
|
res.json({
|
||||||
|
view_configuration: null,
|
||||||
|
is_default_active: true,
|
||||||
|
default_type: "code",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Check if the user has an explicit preference
|
||||||
|
const activeViewPref = await settingsService.getUserPreference(
|
||||||
|
req.auth_context.actor_id,
|
||||||
|
`active_view.${req.params.entity}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// If there's no preference and the view is a system default, it means we're falling back to system default
|
||||||
|
const isDefaultActive =
|
||||||
|
!activeViewPref && viewConfiguration.is_system_default
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
view_configuration: viewConfiguration,
|
||||||
|
is_default_active: isDefaultActive,
|
||||||
|
default_type:
|
||||||
|
isDefaultActive && viewConfiguration.is_system_default
|
||||||
|
? "system"
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: AuthenticatedMedusaRequest<AdminSetActiveViewConfigurationType>,
|
||||||
|
res: MedusaResponse<{ success: boolean }>
|
||||||
|
) => {
|
||||||
|
const settingsService = req.scope.resolve(Modules.SETTINGS)
|
||||||
|
|
||||||
|
if (req.body.view_configuration_id === null) {
|
||||||
|
// Clear the active view configuration
|
||||||
|
await settingsService.clearActiveViewConfiguration(
|
||||||
|
req.params.entity,
|
||||||
|
req.auth_context.actor_id
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Set a specific view as active
|
||||||
|
await settingsService.setActiveViewConfiguration(
|
||||||
|
req.params.entity,
|
||||||
|
req.auth_context.actor_id,
|
||||||
|
req.body.view_configuration_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true })
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
MedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
MedusaNextFunction
|
||||||
|
} from "@medusajs/framework/http"
|
||||||
|
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||||
|
import ViewConfigurationsFeatureFlag from "../../../../../loaders/feature-flags/view-configurations"
|
||||||
|
|
||||||
|
export const ensureViewConfigurationsEnabled = async (
|
||||||
|
req: MedusaRequest,
|
||||||
|
res: MedusaResponse,
|
||||||
|
next: MedusaNextFunction
|
||||||
|
) => {
|
||||||
|
const flagRouter = req.scope.resolve(
|
||||||
|
ContainerRegistrationKeys.FEATURE_FLAG_ROUTER
|
||||||
|
) as any
|
||||||
|
|
||||||
|
if (!flagRouter.isFeatureEnabled(ViewConfigurationsFeatureFlag.key)) {
|
||||||
|
res.status(404).json({
|
||||||
|
type: "not_found",
|
||||||
|
message: "Route not found"
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { validateAndTransformBody, validateAndTransformQuery } from "@medusajs/framework"
|
||||||
|
import { MiddlewareRoute } from "@medusajs/framework/http"
|
||||||
|
import * as QueryConfig from "./query-config"
|
||||||
|
import {
|
||||||
|
AdminCreateViewConfiguration,
|
||||||
|
AdminUpdateViewConfiguration,
|
||||||
|
AdminSetActiveViewConfiguration,
|
||||||
|
AdminGetViewConfigurationParams,
|
||||||
|
AdminGetActiveViewConfigurationParams,
|
||||||
|
AdminGetViewConfigurationsParams,
|
||||||
|
} from "./validators"
|
||||||
|
import { ensureViewConfigurationsEnabled } from "./middleware"
|
||||||
|
|
||||||
|
export const viewConfigurationRoutesMiddlewares: MiddlewareRoute[] = [
|
||||||
|
// Apply feature flag check to all view configuration routes
|
||||||
|
{
|
||||||
|
method: ["GET", "POST", "DELETE"],
|
||||||
|
matcher: "/admin/views/*/configurations*",
|
||||||
|
middlewares: [ensureViewConfigurationsEnabled],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["GET"],
|
||||||
|
matcher: "/admin/views/:entity/configurations",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminGetViewConfigurationsParams,
|
||||||
|
QueryConfig.retrieveViewConfigurationList
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/admin/views/:entity/configurations",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformBody(AdminCreateViewConfiguration),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["GET"],
|
||||||
|
matcher: "/admin/views/:entity/configurations/:id",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminGetViewConfigurationParams,
|
||||||
|
QueryConfig.retrieveViewConfiguration
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/admin/views/:entity/configurations/:id",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformBody(AdminUpdateViewConfiguration),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["GET"],
|
||||||
|
matcher: "/admin/views/:entity/configurations/active",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformQuery(
|
||||||
|
AdminGetActiveViewConfigurationParams,
|
||||||
|
QueryConfig.retrieveViewConfiguration
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/admin/views/:entity/configurations/active",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformBody(AdminSetActiveViewConfiguration),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export const defaultViewConfigurationFields = [
|
||||||
|
"id",
|
||||||
|
"entity",
|
||||||
|
"name",
|
||||||
|
"user_id",
|
||||||
|
"is_system_default",
|
||||||
|
"configuration",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
export const retrieveViewConfigurationList = {
|
||||||
|
defaults: defaultViewConfigurationFields,
|
||||||
|
isList: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const retrieveViewConfiguration = {
|
||||||
|
defaults: defaultViewConfigurationFields,
|
||||||
|
isList: false,
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "@medusajs/framework/http"
|
||||||
|
import { AdminCreateViewConfigurationType } from "./validators"
|
||||||
|
import { HttpTypes } from "@medusajs/framework/types"
|
||||||
|
import { MedusaError, Modules } from "@medusajs/framework/utils"
|
||||||
|
import { createViewConfigurationWorkflow } from "@medusajs/core-flows"
|
||||||
|
|
||||||
|
export const GET = async (
|
||||||
|
req: AuthenticatedMedusaRequest<HttpTypes.AdminGetViewConfigurationsParams>,
|
||||||
|
res: MedusaResponse<HttpTypes.AdminViewConfigurationListResponse>
|
||||||
|
) => {
|
||||||
|
const settingsService = req.scope.resolve(Modules.SETTINGS)
|
||||||
|
|
||||||
|
const filters = {
|
||||||
|
...req.filterableFields,
|
||||||
|
entity: req.params.entity,
|
||||||
|
$or: [{ user_id: req.auth_context.actor_id }, { is_system_default: true }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const [viewConfigurations, count] =
|
||||||
|
await settingsService.listAndCountViewConfigurations(
|
||||||
|
filters,
|
||||||
|
req.queryConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
view_configurations: viewConfigurations,
|
||||||
|
count,
|
||||||
|
offset: req.queryConfig.pagination?.skip || 0,
|
||||||
|
limit: req.queryConfig.pagination?.take || 20,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: AuthenticatedMedusaRequest<AdminCreateViewConfigurationType>,
|
||||||
|
res: MedusaResponse<HttpTypes.AdminViewConfigurationResponse>
|
||||||
|
) => {
|
||||||
|
// Validate: name is required unless creating a system default
|
||||||
|
if (!req.body.is_system_default && !req.body.name) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"Name is required unless creating a system default view"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
...req.body,
|
||||||
|
entity: req.params.entity,
|
||||||
|
user_id: req.body.is_system_default ? null : req.auth_context.actor_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { result } = await createViewConfigurationWorkflow(req.scope).run({
|
||||||
|
input,
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.json({ view_configuration: result })
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import {
|
||||||
|
createFindParams,
|
||||||
|
createOperatorMap,
|
||||||
|
createSelectParams,
|
||||||
|
} from "../../../../utils/validators"
|
||||||
|
import { applyAndAndOrOperators } from "../../../../utils/common-validators"
|
||||||
|
|
||||||
|
export const AdminGetViewConfigurationParams = createSelectParams()
|
||||||
|
|
||||||
|
export type AdminGetActiveViewConfigurationParamsType = z.infer<typeof AdminGetActiveViewConfigurationParams>
|
||||||
|
export const AdminGetActiveViewConfigurationParams = createSelectParams()
|
||||||
|
|
||||||
|
export const AdminGetViewConfigurationsParamsFields = z.object({
|
||||||
|
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
entity: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
name: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
|
user_id: z.union([z.string(), z.array(z.string()), z.null()]).optional(),
|
||||||
|
is_system_default: z.boolean().optional(),
|
||||||
|
created_at: createOperatorMap().optional(),
|
||||||
|
updated_at: createOperatorMap().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AdminGetViewConfigurationsParamsType = z.infer<typeof AdminGetViewConfigurationsParams>
|
||||||
|
export const AdminGetViewConfigurationsParams = createFindParams({
|
||||||
|
offset: 0,
|
||||||
|
limit: 20,
|
||||||
|
})
|
||||||
|
.merge(AdminGetViewConfigurationsParamsFields)
|
||||||
|
.merge(applyAndAndOrOperators(AdminGetViewConfigurationsParamsFields))
|
||||||
|
|
||||||
|
export type AdminCreateViewConfigurationType = z.infer<typeof AdminCreateViewConfiguration>
|
||||||
|
export const AdminCreateViewConfiguration = z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
is_system_default: z.boolean().optional().default(false),
|
||||||
|
set_active: z.boolean().optional().default(false),
|
||||||
|
configuration: z.object({
|
||||||
|
visible_columns: z.array(z.string()),
|
||||||
|
column_order: z.array(z.string()),
|
||||||
|
column_widths: z.record(z.string(), z.number()).optional(),
|
||||||
|
filters: z.record(z.string(), z.any()).optional(),
|
||||||
|
sorting: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
desc: z.boolean(),
|
||||||
|
}).nullable().optional(),
|
||||||
|
search: z.string().optional(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AdminUpdateViewConfigurationType = z.infer<typeof AdminUpdateViewConfiguration>
|
||||||
|
export const AdminUpdateViewConfiguration = z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
is_system_default: z.boolean().optional(),
|
||||||
|
set_active: z.boolean().optional().default(false),
|
||||||
|
configuration: z.object({
|
||||||
|
visible_columns: z.array(z.string()).optional(),
|
||||||
|
column_order: z.array(z.string()).optional(),
|
||||||
|
column_widths: z.record(z.string(), z.number()).optional(),
|
||||||
|
filters: z.record(z.string(), z.any()).optional(),
|
||||||
|
sorting: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
desc: z.boolean(),
|
||||||
|
}).nullable().optional(),
|
||||||
|
search: z.string().optional(),
|
||||||
|
}).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AdminSetActiveViewConfigurationType = z.infer<typeof AdminSetActiveViewConfiguration>
|
||||||
|
export const AdminSetActiveViewConfiguration = z.object({
|
||||||
|
view_configuration_id: z.union([z.string(), z.null()]),
|
||||||
|
})
|
||||||
@@ -41,6 +41,7 @@ import { adminTaxRegionRoutesMiddlewares } from "./admin/tax-regions/middlewares
|
|||||||
import { adminTaxProviderRoutesMiddlewares } from "./admin/tax-providers/middlewares"
|
import { adminTaxProviderRoutesMiddlewares } from "./admin/tax-providers/middlewares"
|
||||||
import { adminUploadRoutesMiddlewares } from "./admin/uploads/middlewares"
|
import { adminUploadRoutesMiddlewares } from "./admin/uploads/middlewares"
|
||||||
import { adminUserRoutesMiddlewares } from "./admin/users/middlewares"
|
import { adminUserRoutesMiddlewares } from "./admin/users/middlewares"
|
||||||
|
import { viewConfigurationRoutesMiddlewares } from "./admin/views/[entity]/configurations/middlewares"
|
||||||
import { adminWorkflowsExecutionsMiddlewares } from "./admin/workflows-executions/middlewares"
|
import { adminWorkflowsExecutionsMiddlewares } from "./admin/workflows-executions/middlewares"
|
||||||
import { authRoutesMiddlewares } from "./auth/middlewares"
|
import { authRoutesMiddlewares } from "./auth/middlewares"
|
||||||
|
|
||||||
@@ -126,4 +127,5 @@ export default defineMiddlewares([
|
|||||||
...adminTaxProviderRoutesMiddlewares,
|
...adminTaxProviderRoutesMiddlewares,
|
||||||
...adminOrderEditRoutesMiddlewares,
|
...adminOrderEditRoutesMiddlewares,
|
||||||
...adminPaymentCollectionsMiddlewares,
|
...adminPaymentCollectionsMiddlewares,
|
||||||
|
...viewConfigurationRoutesMiddlewares,
|
||||||
])
|
])
|
||||||
|
|||||||
Reference in New Issue
Block a user