mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-07 06:39:52 +02:00
add save default view
This commit is contained in:
@@ -512,6 +512,18 @@ authenticated.put(
|
||||
launcher.updateLauncherView
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/launcher/default-view",
|
||||
verifyOrgAccess,
|
||||
launcher.upsertLauncherDefaultView
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/org/:orgId/launcher/default-view",
|
||||
verifyOrgAccess,
|
||||
launcher.deleteLauncherDefaultView
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/org/:orgId/launcher/views/:viewId",
|
||||
verifyOrgAccess,
|
||||
|
||||
@@ -7,6 +7,7 @@ import moment from "moment";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import { isLauncherDefaultOverrideViewName } from "./launcherDefaultView";
|
||||
import { launcherViewConfigSchema } from "./types";
|
||||
|
||||
const createLauncherViewBodySchema = z.strictObject({
|
||||
@@ -40,6 +41,15 @@ export async function createLauncherView(
|
||||
);
|
||||
}
|
||||
|
||||
if (isLauncherDefaultOverrideViewName(parsed.data.name)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"This view name is reserved"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed.data.orgWide) {
|
||||
const canCreateOrgWide = await checkUserActionPermission(
|
||||
ActionsEnum.createOrgWideLauncherView,
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import {
|
||||
deleteAllDefaultViewOverrides,
|
||||
deleteDefaultViewOverride
|
||||
} from "./launcherDefaultView";
|
||||
|
||||
const deleteLauncherDefaultViewBodySchema = z.strictObject({
|
||||
orgWide: z.boolean().optional().default(false),
|
||||
all: z.boolean().optional().default(false)
|
||||
});
|
||||
|
||||
export async function deleteLauncherDefaultView(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = deleteLauncherDefaultViewBodySchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed.data.all) {
|
||||
const canManageOrgWide = await checkUserActionPermission(
|
||||
ActionsEnum.createOrgWideLauncherView,
|
||||
req
|
||||
);
|
||||
if (!canManageOrgWide) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission perform this action"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await deleteAllDefaultViewOverrides(orgId, userId);
|
||||
} else if (parsed.data.orgWide) {
|
||||
const canManageOrgWide = await checkUserActionPermission(
|
||||
ActionsEnum.createOrgWideLauncherView,
|
||||
req
|
||||
);
|
||||
if (!canManageOrgWide) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission perform this action"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await deleteDefaultViewOverride({
|
||||
orgId,
|
||||
userId,
|
||||
orgWide: true
|
||||
});
|
||||
} else {
|
||||
await deleteDefaultViewOverride({
|
||||
orgId,
|
||||
userId,
|
||||
orgWide: false
|
||||
});
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher default view reset successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error resetting launcher default view:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { and, eq } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import { isLauncherDefaultOverrideViewName } from "./launcherDefaultView";
|
||||
|
||||
export async function deleteLauncherView(
|
||||
req: Request,
|
||||
@@ -46,6 +47,15 @@ export async function deleteLauncherView(
|
||||
);
|
||||
}
|
||||
|
||||
if (isLauncherDefaultOverrideViewName(existing.name)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"The default view cannot be deleted from here"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const isPersonalView = existing.userId === userId;
|
||||
const isOrgWideView = existing.userId == null;
|
||||
const canManageOrgWide = await checkUserActionPermission(
|
||||
|
||||
@@ -8,3 +8,5 @@ export { listLauncherViews } from "./listLauncherViews";
|
||||
export { createLauncherView } from "./createLauncherView";
|
||||
export { updateLauncherView } from "./updateLauncherView";
|
||||
export { deleteLauncherView } from "./deleteLauncherView";
|
||||
export { upsertLauncherDefaultView } from "./upsertLauncherDefaultView";
|
||||
export { deleteLauncherDefaultView } from "./deleteLauncherDefaultView";
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { db, launcherViews } from "@server/db";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import moment from "moment";
|
||||
import {
|
||||
LAUNCHER_DEFAULT_OVERRIDE_VIEW_NAME,
|
||||
launcherViewConfigSchema,
|
||||
type LauncherDefaultViewOverrides,
|
||||
type LauncherViewConfig,
|
||||
type LauncherViewRecord
|
||||
} from "./types";
|
||||
|
||||
export function isLauncherDefaultOverrideViewName(name: string) {
|
||||
return name === LAUNCHER_DEFAULT_OVERRIDE_VIEW_NAME;
|
||||
}
|
||||
|
||||
export function mapViewRow(
|
||||
row: typeof launcherViews.$inferSelect
|
||||
): LauncherViewRecord {
|
||||
return {
|
||||
viewId: row.viewId,
|
||||
orgId: row.orgId,
|
||||
userId: row.userId,
|
||||
name: row.name,
|
||||
config: launcherViewConfigSchema.parse(JSON.parse(row.config)),
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
isOrgWide: row.userId == null
|
||||
};
|
||||
}
|
||||
|
||||
export function extractDefaultViewOverrides(
|
||||
rows: Array<typeof launcherViews.$inferSelect>
|
||||
): LauncherDefaultViewOverrides {
|
||||
const overrideRows = rows.filter((row) =>
|
||||
isLauncherDefaultOverrideViewName(row.name)
|
||||
);
|
||||
|
||||
const personalRow = overrideRows.find((row) => row.userId !== null);
|
||||
const orgWideRow = overrideRows.find((row) => row.userId === null);
|
||||
|
||||
return {
|
||||
personal: personalRow ? mapViewRow(personalRow) : null,
|
||||
orgWide: orgWideRow ? mapViewRow(orgWideRow) : null
|
||||
};
|
||||
}
|
||||
|
||||
export function listVisibleLauncherViews(
|
||||
rows: Array<typeof launcherViews.$inferSelect>
|
||||
): LauncherViewRecord[] {
|
||||
return rows
|
||||
.filter((row) => !isLauncherDefaultOverrideViewName(row.name))
|
||||
.map(mapViewRow);
|
||||
}
|
||||
|
||||
export async function findDefaultViewOverride(
|
||||
orgId: string,
|
||||
orgWide: boolean,
|
||||
userId: string
|
||||
) {
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(launcherViews)
|
||||
.where(
|
||||
and(
|
||||
eq(launcherViews.orgId, orgId),
|
||||
eq(launcherViews.name, LAUNCHER_DEFAULT_OVERRIDE_VIEW_NAME),
|
||||
orgWide
|
||||
? isNull(launcherViews.userId)
|
||||
: eq(launcherViews.userId, userId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return existing ?? null;
|
||||
}
|
||||
|
||||
export async function upsertDefaultViewOverride({
|
||||
orgId,
|
||||
userId,
|
||||
orgWide,
|
||||
config
|
||||
}: {
|
||||
orgId: string;
|
||||
userId: string;
|
||||
orgWide: boolean;
|
||||
config: LauncherViewConfig;
|
||||
}) {
|
||||
const now = moment().toISOString();
|
||||
const existing = await findDefaultViewOverride(orgId, orgWide, userId);
|
||||
|
||||
if (existing) {
|
||||
const [updated] = await db
|
||||
.update(launcherViews)
|
||||
.set({
|
||||
config: JSON.stringify(config),
|
||||
updatedAt: now
|
||||
})
|
||||
.where(eq(launcherViews.viewId, existing.viewId))
|
||||
.returning();
|
||||
|
||||
return mapViewRow(updated);
|
||||
}
|
||||
|
||||
const [created] = await db
|
||||
.insert(launcherViews)
|
||||
.values({
|
||||
orgId,
|
||||
userId: orgWide ? null : userId,
|
||||
name: LAUNCHER_DEFAULT_OVERRIDE_VIEW_NAME,
|
||||
config: JSON.stringify(config),
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
})
|
||||
.returning();
|
||||
|
||||
return mapViewRow(created);
|
||||
}
|
||||
|
||||
export async function deleteDefaultViewOverride({
|
||||
orgId,
|
||||
userId,
|
||||
orgWide
|
||||
}: {
|
||||
orgId: string;
|
||||
userId: string;
|
||||
orgWide: boolean;
|
||||
}) {
|
||||
const existing = await findDefaultViewOverride(orgId, orgWide, userId);
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(launcherViews)
|
||||
.where(eq(launcherViews.viewId, existing.viewId));
|
||||
}
|
||||
|
||||
export async function deleteAllDefaultViewOverrides(
|
||||
orgId: string,
|
||||
userId: string
|
||||
) {
|
||||
await deleteDefaultViewOverride({ orgId, userId, orgWide: false });
|
||||
await deleteDefaultViewOverride({ orgId, userId, orgWide: true });
|
||||
}
|
||||
@@ -4,22 +4,10 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq, isNull, or } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { launcherViewConfigSchema, type LauncherViewRecord } from "./types";
|
||||
|
||||
function mapViewRow(
|
||||
row: typeof launcherViews.$inferSelect
|
||||
): LauncherViewRecord {
|
||||
return {
|
||||
viewId: row.viewId,
|
||||
orgId: row.orgId,
|
||||
userId: row.userId,
|
||||
name: row.name,
|
||||
config: launcherViewConfigSchema.parse(JSON.parse(row.config)),
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
isOrgWide: row.userId == null
|
||||
};
|
||||
}
|
||||
import {
|
||||
extractDefaultViewOverrides,
|
||||
listVisibleLauncherViews
|
||||
} from "./launcherDefaultView";
|
||||
|
||||
export async function listLauncherViews(
|
||||
req: Request,
|
||||
@@ -51,7 +39,8 @@ export async function listLauncherViews(
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
views: rows.map(mapViewRow)
|
||||
views: listVisibleLauncherViews(rows),
|
||||
defaultViewOverrides: extractDefaultViewOverrides(rows)
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
|
||||
@@ -91,8 +91,28 @@ export type LauncherViewRecord = {
|
||||
isOrgWide: boolean;
|
||||
};
|
||||
|
||||
export type LauncherDefaultViewOverrides = {
|
||||
personal: LauncherViewRecord | null;
|
||||
orgWide: LauncherViewRecord | null;
|
||||
};
|
||||
|
||||
export function getEffectiveDefaultLauncherConfig(
|
||||
overrides: LauncherDefaultViewOverrides
|
||||
): LauncherViewConfig {
|
||||
if (overrides.personal) {
|
||||
return overrides.personal.config;
|
||||
}
|
||||
|
||||
if (overrides.orgWide) {
|
||||
return overrides.orgWide.config;
|
||||
}
|
||||
|
||||
return defaultLauncherViewConfig;
|
||||
}
|
||||
|
||||
export type ListLauncherViewsResponse = {
|
||||
views: LauncherViewRecord[];
|
||||
defaultViewOverrides: LauncherDefaultViewOverrides;
|
||||
};
|
||||
|
||||
export const launcherFilterListQuerySchema = z.strictObject({
|
||||
@@ -161,6 +181,8 @@ export function parseIdListParam(value: string | undefined): number[] {
|
||||
|
||||
export const DEFAULT_LAUNCHER_VIEW_ID = "default" as const;
|
||||
|
||||
export const LAUNCHER_DEFAULT_OVERRIDE_VIEW_NAME = "__default__";
|
||||
|
||||
export type LauncherViewSelection =
|
||||
| { type: "default" }
|
||||
| { type: "saved"; viewId: number };
|
||||
|
||||
@@ -9,6 +9,7 @@ import moment from "moment";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import { isLauncherDefaultOverrideViewName } from "./launcherDefaultView";
|
||||
import { launcherViewConfigSchema } from "./types";
|
||||
|
||||
const updateLauncherViewBodySchema = z.strictObject({
|
||||
@@ -66,6 +67,15 @@ export async function updateLauncherView(
|
||||
);
|
||||
}
|
||||
|
||||
if (isLauncherDefaultOverrideViewName(existing.name)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Use the default view save endpoint for this view"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const isPersonalView = existing.userId === userId;
|
||||
const isOrgWideView = existing.userId == null;
|
||||
const canManageOrgWide = await checkUserActionPermission(
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import { upsertDefaultViewOverride } from "./launcherDefaultView";
|
||||
import { launcherViewConfigSchema } from "./types";
|
||||
|
||||
const upsertLauncherDefaultViewBodySchema = z.strictObject({
|
||||
config: launcherViewConfigSchema,
|
||||
orgWide: z.boolean().optional().default(false)
|
||||
});
|
||||
|
||||
export async function upsertLauncherDefaultView(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = upsertLauncherDefaultViewBodySchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed.data.orgWide) {
|
||||
const canManageOrgWide = await checkUserActionPermission(
|
||||
ActionsEnum.createOrgWideLauncherView,
|
||||
req
|
||||
);
|
||||
if (!canManageOrgWide) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission perform this action"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const view = await upsertDefaultViewOverride({
|
||||
orgId,
|
||||
userId,
|
||||
orgWide: parsed.data.orgWide,
|
||||
config: parsed.data.config
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: view,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher default view saved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error saving launcher default view:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user