mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-17 19:16:32 +02:00
add save default view
This commit is contained in:
+10
-3
@@ -3571,12 +3571,16 @@
|
|||||||
"memberPortalResourceDisabled": "Resource Disabled",
|
"memberPortalResourceDisabled": "Resource Disabled",
|
||||||
"memberPortalShowingResources": "Showing {start}-{end} of {total} resources",
|
"memberPortalShowingResources": "Showing {start}-{end} of {total} resources",
|
||||||
"resourceLauncherTitle": "Resource Launcher",
|
"resourceLauncherTitle": "Resource Launcher",
|
||||||
"resourceLauncherDescription": "View resource details and launch them from one place",
|
"resourceLauncherDescription": "View all available resources and launch them from one central hub",
|
||||||
"resourceLauncherSearchPlaceholder": "Search all sites...",
|
"resourceLauncherSearchPlaceholder": "Search your resources...",
|
||||||
"resourceLauncherDefaultView": "Default",
|
"resourceLauncherDefaultView": "Default",
|
||||||
"resourceLauncherSaveView": "Save View",
|
"resourceLauncherSaveView": "Save View",
|
||||||
"resourceLauncherSaveToCurrentView": "Save to Current View",
|
"resourceLauncherSaveToCurrentView": "Save to Current View",
|
||||||
|
"resourceLauncherSaveDefaultPersonal": "Save for Me",
|
||||||
"resourceLauncherResetView": "Reset View",
|
"resourceLauncherResetView": "Reset View",
|
||||||
|
"resourceLauncherResetSystemDefault": "Reset to System Default",
|
||||||
|
"resourceLauncherSystemDefaultRestored": "System default restored",
|
||||||
|
"resourceLauncherSystemDefaultRestoredDescription": "The default view has been reset to the original settings.",
|
||||||
"resourceLauncherSaveAsNewView": "Save as New View",
|
"resourceLauncherSaveAsNewView": "Save as New View",
|
||||||
"resourceLauncherSaveAsNewViewDescription": "Give this view a name to save your current filters and layout.",
|
"resourceLauncherSaveAsNewViewDescription": "Give this view a name to save your current filters and layout.",
|
||||||
"resourceLauncherSaveForEveryone": "Save for Everyone",
|
"resourceLauncherSaveForEveryone": "Save for Everyone",
|
||||||
@@ -3598,6 +3602,9 @@
|
|||||||
"resourceLauncherShowSiteTags": "Show Site Tags",
|
"resourceLauncherShowSiteTags": "Show Site Tags",
|
||||||
"resourceLauncherShowRecents": "Show Recents",
|
"resourceLauncherShowRecents": "Show Recents",
|
||||||
"resourceLauncherDeleteView": "Delete View",
|
"resourceLauncherDeleteView": "Delete View",
|
||||||
|
"resourceLauncherDeleteViewTitle": "Delete View",
|
||||||
|
"resourceLauncherDeleteViewQuestion": "Are you sure you want to delete this launcher view?",
|
||||||
|
"resourceLauncherDeleteViewConfirm": "Delete View",
|
||||||
"resourceLauncherViewAsAdmin": "View as Admin",
|
"resourceLauncherViewAsAdmin": "View as Admin",
|
||||||
"resourceLauncherResourceDetailsDescription": "View details for this resource.",
|
"resourceLauncherResourceDetailsDescription": "View details for this resource.",
|
||||||
"resourceLauncherUnlabeled": "Unlabeled",
|
"resourceLauncherUnlabeled": "Unlabeled",
|
||||||
@@ -3609,7 +3616,7 @@
|
|||||||
"resourceLauncherEmptyStateNoResultsDescription": "No resources match your current search or filters. Try adjusting them to find what you are looking for.",
|
"resourceLauncherEmptyStateNoResultsDescription": "No resources match your current search or filters. Try adjusting them to find what you are looking for.",
|
||||||
"resourceLauncherEmptyStateNoResultsWithQuery": "No resources match \"{query}\". Try adjusting your search or clearing filters to see all resources.",
|
"resourceLauncherEmptyStateNoResultsWithQuery": "No resources match \"{query}\". Try adjusting your search or clearing filters to see all resources.",
|
||||||
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
|
"resourceLauncherSearchFirstTitle": "Search or Filter to Browse",
|
||||||
"resourceLauncherSearchFirstDescription": "This organization has many resources. Use search or filter by site or label to find what you need.",
|
"resourceLauncherSearchFirstDescription": "You have access to many resources. Use search or filter by site or label to find what you need.",
|
||||||
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
|
"resourceLauncherSiteGroupingDisabled": "Site grouping is unavailable at this scale. Filter by site to group a smaller set.",
|
||||||
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
|
"resourceLauncherLabelGroupingDisabled": "Label grouping is unavailable at this scale.",
|
||||||
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
|
"resourceLauncherCompactModeHint": "Showing a simplified list for faster browsing. Use search or filters to narrow results.",
|
||||||
|
|||||||
@@ -512,6 +512,18 @@ authenticated.put(
|
|||||||
launcher.updateLauncherView
|
launcher.updateLauncherView
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/launcher/default-view",
|
||||||
|
verifyOrgAccess,
|
||||||
|
launcher.upsertLauncherDefaultView
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/org/:orgId/launcher/default-view",
|
||||||
|
verifyOrgAccess,
|
||||||
|
launcher.deleteLauncherDefaultView
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.delete(
|
authenticated.delete(
|
||||||
"/org/:orgId/launcher/views/:viewId",
|
"/org/:orgId/launcher/views/:viewId",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import moment from "moment";
|
|||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||||
|
import { isLauncherDefaultOverrideViewName } from "./launcherDefaultView";
|
||||||
import { launcherViewConfigSchema } from "./types";
|
import { launcherViewConfigSchema } from "./types";
|
||||||
|
|
||||||
const createLauncherViewBodySchema = z.strictObject({
|
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) {
|
if (parsed.data.orgWide) {
|
||||||
const canCreateOrgWide = await checkUserActionPermission(
|
const canCreateOrgWide = await checkUserActionPermission(
|
||||||
ActionsEnum.createOrgWideLauncherView,
|
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 { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||||
|
import { isLauncherDefaultOverrideViewName } from "./launcherDefaultView";
|
||||||
|
|
||||||
export async function deleteLauncherView(
|
export async function deleteLauncherView(
|
||||||
req: Request,
|
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 isPersonalView = existing.userId === userId;
|
||||||
const isOrgWideView = existing.userId == null;
|
const isOrgWideView = existing.userId == null;
|
||||||
const canManageOrgWide = await checkUserActionPermission(
|
const canManageOrgWide = await checkUserActionPermission(
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ export { listLauncherViews } from "./listLauncherViews";
|
|||||||
export { createLauncherView } from "./createLauncherView";
|
export { createLauncherView } from "./createLauncherView";
|
||||||
export { updateLauncherView } from "./updateLauncherView";
|
export { updateLauncherView } from "./updateLauncherView";
|
||||||
export { deleteLauncherView } from "./deleteLauncherView";
|
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 { and, eq, isNull, or } from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { launcherViewConfigSchema, type LauncherViewRecord } from "./types";
|
import {
|
||||||
|
extractDefaultViewOverrides,
|
||||||
function mapViewRow(
|
listVisibleLauncherViews
|
||||||
row: typeof launcherViews.$inferSelect
|
} from "./launcherDefaultView";
|
||||||
): 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 async function listLauncherViews(
|
export async function listLauncherViews(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -51,7 +39,8 @@ export async function listLauncherViews(
|
|||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: {
|
data: {
|
||||||
views: rows.map(mapViewRow)
|
views: listVisibleLauncherViews(rows),
|
||||||
|
defaultViewOverrides: extractDefaultViewOverrides(rows)
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -91,8 +91,28 @@ export type LauncherViewRecord = {
|
|||||||
isOrgWide: boolean;
|
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 = {
|
export type ListLauncherViewsResponse = {
|
||||||
views: LauncherViewRecord[];
|
views: LauncherViewRecord[];
|
||||||
|
defaultViewOverrides: LauncherDefaultViewOverrides;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const launcherFilterListQuerySchema = z.strictObject({
|
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 DEFAULT_LAUNCHER_VIEW_ID = "default" as const;
|
||||||
|
|
||||||
|
export const LAUNCHER_DEFAULT_OVERRIDE_VIEW_NAME = "__default__";
|
||||||
|
|
||||||
export type LauncherViewSelection =
|
export type LauncherViewSelection =
|
||||||
| { type: "default" }
|
| { type: "default" }
|
||||||
| { type: "saved"; viewId: number };
|
| { type: "saved"; viewId: number };
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import moment from "moment";
|
|||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||||
|
import { isLauncherDefaultOverrideViewName } from "./launcherDefaultView";
|
||||||
import { launcherViewConfigSchema } from "./types";
|
import { launcherViewConfigSchema } from "./types";
|
||||||
|
|
||||||
const updateLauncherViewBodySchema = z.strictObject({
|
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 isPersonalView = existing.userId === userId;
|
||||||
const isOrgWideView = existing.userId == null;
|
const isOrgWideView = existing.userId == null;
|
||||||
const canManageOrgWide = await checkUserActionPermission(
|
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"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,7 @@ export default async function OrgPage(props: OrgPageProps) {
|
|||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
isAdmin={isAdminOrOwner}
|
isAdmin={isAdminOrOwner}
|
||||||
views={launcherData.views}
|
views={launcherData.views}
|
||||||
|
defaultViewOverrides={launcherData.defaultViewOverrides}
|
||||||
activeViewId={launcherData.activeViewId}
|
activeViewId={launcherData.activeViewId}
|
||||||
config={launcherData.config}
|
config={launcherData.config}
|
||||||
savedConfig={launcherData.savedConfig}
|
savedConfig={launcherData.savedConfig}
|
||||||
|
|||||||
@@ -24,22 +24,18 @@ import { Settings } from "lucide-react";
|
|||||||
|
|
||||||
type LauncherSettingsMenuProps = {
|
type LauncherSettingsMenuProps = {
|
||||||
config: LauncherViewConfig;
|
config: LauncherViewConfig;
|
||||||
isDefaultView: boolean;
|
|
||||||
capabilities: LauncherScaleCapabilities;
|
capabilities: LauncherScaleCapabilities;
|
||||||
isCompactMode: boolean;
|
isCompactMode: boolean;
|
||||||
selectedGroupBy: LauncherViewConfig["groupBy"];
|
selectedGroupBy: LauncherViewConfig["groupBy"];
|
||||||
onConfigChange: (patch: Partial<LauncherViewConfig>) => void;
|
onConfigChange: (patch: Partial<LauncherViewConfig>) => void;
|
||||||
onDeleteView: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LauncherSettingsMenu({
|
export function LauncherSettingsMenu({
|
||||||
config,
|
config,
|
||||||
isDefaultView,
|
|
||||||
capabilities,
|
capabilities,
|
||||||
isCompactMode,
|
isCompactMode,
|
||||||
selectedGroupBy,
|
selectedGroupBy,
|
||||||
onConfigChange,
|
onConfigChange
|
||||||
onDeleteView
|
|
||||||
}: LauncherSettingsMenuProps) {
|
}: LauncherSettingsMenuProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -157,16 +153,6 @@ export function LauncherSettingsMenu({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isDefaultView ? (
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
className="w-full rounded-xl"
|
|
||||||
onClick={onDeleteView}
|
|
||||||
>
|
|
||||||
{t("resourceLauncherDeleteView")}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -68,11 +68,14 @@ type LauncherSaveViewMenuProps = {
|
|||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isOrgWideView: boolean;
|
isOrgWideView: boolean;
|
||||||
hasUnsavedChanges: boolean;
|
hasUnsavedChanges: boolean;
|
||||||
|
canResetSystemDefault: boolean;
|
||||||
onSaveToCurrent: () => void;
|
onSaveToCurrent: () => void;
|
||||||
onSaveAsNew: () => void;
|
onSaveAsNew: () => void;
|
||||||
onSaveForEveryone: () => void;
|
onSaveForEveryone: () => void;
|
||||||
onMakePersonal: () => void;
|
onMakePersonal: () => void;
|
||||||
onResetView: () => void;
|
onResetView: () => void;
|
||||||
|
onResetSystemDefault: () => void;
|
||||||
|
onDeleteView: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LauncherSaveViewMenu({
|
export function LauncherSaveViewMenu({
|
||||||
@@ -80,13 +83,17 @@ export function LauncherSaveViewMenu({
|
|||||||
isAdmin,
|
isAdmin,
|
||||||
isOrgWideView,
|
isOrgWideView,
|
||||||
hasUnsavedChanges,
|
hasUnsavedChanges,
|
||||||
|
canResetSystemDefault,
|
||||||
onSaveToCurrent,
|
onSaveToCurrent,
|
||||||
onSaveAsNew,
|
onSaveAsNew,
|
||||||
onSaveForEveryone,
|
onSaveForEveryone,
|
||||||
onMakePersonal,
|
onMakePersonal,
|
||||||
onResetView
|
onResetView,
|
||||||
|
onResetSystemDefault,
|
||||||
|
onDeleteView
|
||||||
}: LauncherSaveViewMenuProps) {
|
}: LauncherSaveViewMenuProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const canDeleteView = !isDefaultView && (isAdmin || !isOrgWideView);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -108,7 +115,26 @@ export function LauncherSaveViewMenu({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{!isDefaultView && (isAdmin || !isOrgWideView) ? (
|
{isDefaultView && canResetSystemDefault ? (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem onSelect={onResetSystemDefault}>
|
||||||
|
{t("resourceLauncherResetSystemDefault")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{isDefaultView && hasUnsavedChanges ? (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem onSelect={onSaveToCurrent}>
|
||||||
|
{t("resourceLauncherSaveDefaultPersonal")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{isAdmin ? (
|
||||||
|
<DropdownMenuItem onSelect={onSaveForEveryone}>
|
||||||
|
{t("resourceLauncherSaveForEveryone")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : !isDefaultView && (isAdmin || !isOrgWideView) ? (
|
||||||
<DropdownMenuItem onSelect={onSaveToCurrent}>
|
<DropdownMenuItem onSelect={onSaveToCurrent}>
|
||||||
{t("resourceLauncherSaveToCurrentView")}
|
{t("resourceLauncherSaveToCurrentView")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -126,6 +152,17 @@ export function LauncherSaveViewMenu({
|
|||||||
{t("resourceLauncherMakePersonal")}
|
{t("resourceLauncherMakePersonal")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
|
{canDeleteView ? (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={onDeleteView}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
{t("resourceLauncherDeleteView")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,11 +36,13 @@ import {
|
|||||||
shouldShowSearchFirstGate
|
shouldShowSearchFirstGate
|
||||||
} from "@app/lib/launcherScale";
|
} from "@app/lib/launcherScale";
|
||||||
import { launcherQueries } from "@app/lib/queries";
|
import { launcherQueries } from "@app/lib/queries";
|
||||||
import type {
|
import {
|
||||||
LauncherGroup,
|
getEffectiveDefaultLauncherConfig,
|
||||||
LauncherScaleInfo,
|
type LauncherDefaultViewOverrides,
|
||||||
LauncherViewConfig,
|
type LauncherGroup,
|
||||||
LauncherViewRecord
|
type LauncherScaleInfo,
|
||||||
|
type LauncherViewConfig,
|
||||||
|
type LauncherViewRecord
|
||||||
} from "@server/routers/launcher/types";
|
} from "@server/routers/launcher/types";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
@@ -67,12 +69,14 @@ import { LauncherRefreshButton } from "./LauncherRefreshButton";
|
|||||||
import { LauncherSettingsMenu } from "./LauncherSettingsMenu";
|
import { LauncherSettingsMenu } from "./LauncherSettingsMenu";
|
||||||
import { LauncherSortButton } from "./LauncherSortButton";
|
import { LauncherSortButton } from "./LauncherSortButton";
|
||||||
import { LauncherSaveViewMenu, LauncherViewTabs } from "./LauncherViewTabs";
|
import { LauncherSaveViewMenu, LauncherViewTabs } from "./LauncherViewTabs";
|
||||||
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
|
||||||
type ResourceLauncherProps = {
|
type ResourceLauncherProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
views: LauncherViewRecord[];
|
views: LauncherViewRecord[];
|
||||||
|
defaultViewOverrides: LauncherDefaultViewOverrides;
|
||||||
activeViewId: LauncherActiveViewId;
|
activeViewId: LauncherActiveViewId;
|
||||||
config: LauncherViewConfig;
|
config: LauncherViewConfig;
|
||||||
savedConfig: LauncherViewConfig;
|
savedConfig: LauncherViewConfig;
|
||||||
@@ -89,6 +93,7 @@ export default function ResourceLauncher({
|
|||||||
orgId,
|
orgId,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
views,
|
views,
|
||||||
|
defaultViewOverrides,
|
||||||
activeViewId,
|
activeViewId,
|
||||||
config,
|
config,
|
||||||
savedConfig,
|
savedConfig,
|
||||||
@@ -107,6 +112,7 @@ export default function ResourceLauncher({
|
|||||||
|
|
||||||
const [searchInputResetKey, setSearchInputResetKey] = useState(0);
|
const [searchInputResetKey, setSearchInputResetKey] = useState(0);
|
||||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [newViewName, setNewViewName] = useState("");
|
const [newViewName, setNewViewName] = useState("");
|
||||||
const [saveOrgWide, setSaveOrgWide] = useState(false);
|
const [saveOrgWide, setSaveOrgWide] = useState(false);
|
||||||
|
|
||||||
@@ -173,13 +179,24 @@ export default function ResourceLauncher({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseConfig = getLauncherUrlBaseConfig(lastView, views);
|
const baseConfig = getLauncherUrlBaseConfig(
|
||||||
|
lastView,
|
||||||
|
views,
|
||||||
|
defaultViewOverrides
|
||||||
|
);
|
||||||
const params = serializeLauncherUrlState({
|
const params = serializeLauncherUrlState({
|
||||||
viewId: lastView,
|
viewId: lastView,
|
||||||
config: baseConfig
|
config: baseConfig
|
||||||
});
|
});
|
||||||
navigate({ searchParams: params, replace: true });
|
navigate({ searchParams: params, replace: true });
|
||||||
}, [activeViewId, navigate, orgId, searchParams, views]);
|
}, [
|
||||||
|
activeViewId,
|
||||||
|
defaultViewOverrides,
|
||||||
|
navigate,
|
||||||
|
orgId,
|
||||||
|
searchParams,
|
||||||
|
views
|
||||||
|
]);
|
||||||
|
|
||||||
const navigateToConfig = useCallback(
|
const navigateToConfig = useCallback(
|
||||||
(viewId: LauncherActiveViewId, nextConfig: LauncherViewConfig) => {
|
(viewId: LauncherActiveViewId, nextConfig: LauncherViewConfig) => {
|
||||||
@@ -202,10 +219,14 @@ export default function ResourceLauncher({
|
|||||||
const selectView = useCallback(
|
const selectView = useCallback(
|
||||||
(viewId: LauncherActiveViewId) => {
|
(viewId: LauncherActiveViewId) => {
|
||||||
writeLauncherLastView(orgId, viewId);
|
writeLauncherLastView(orgId, viewId);
|
||||||
const baseConfig = getLauncherUrlBaseConfig(viewId, views);
|
const baseConfig = getLauncherUrlBaseConfig(
|
||||||
|
viewId,
|
||||||
|
views,
|
||||||
|
defaultViewOverrides
|
||||||
|
);
|
||||||
navigateToConfig(viewId, baseConfig);
|
navigateToConfig(viewId, baseConfig);
|
||||||
},
|
},
|
||||||
[navigateToConfig, orgId, views]
|
[defaultViewOverrides, navigateToConfig, orgId, views]
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeSavedView = useMemo(
|
const activeSavedView = useMemo(
|
||||||
@@ -219,6 +240,10 @@ export default function ResourceLauncher({
|
|||||||
const isDefaultView = activeViewId === "default";
|
const isDefaultView = activeViewId === "default";
|
||||||
const isOrgWideView = Boolean(activeSavedView?.isOrgWide);
|
const isOrgWideView = Boolean(activeSavedView?.isOrgWide);
|
||||||
const hasUnsavedChanges = !isLauncherConfigEqual(config, savedConfig);
|
const hasUnsavedChanges = !isLauncherConfigEqual(config, savedConfig);
|
||||||
|
const canResetSystemDefault =
|
||||||
|
isDefaultView &&
|
||||||
|
(Boolean(defaultViewOverrides.personal) ||
|
||||||
|
(isAdmin && Boolean(defaultViewOverrides.orgWide)));
|
||||||
|
|
||||||
const selectedSites: Selectedsite[] = useMemo(
|
const selectedSites: Selectedsite[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -314,6 +339,87 @@ export default function ResourceLauncher({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const saveDefaultViewMutation = useMutation({
|
||||||
|
mutationFn: async (payload: {
|
||||||
|
config: LauncherViewConfig;
|
||||||
|
orgWide: boolean;
|
||||||
|
}) => {
|
||||||
|
const res = await api.put(
|
||||||
|
`/org/${orgId}/launcher/default-view`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
return res.data.data as LauncherViewRecord;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
writeLauncherLastView(orgId, "default");
|
||||||
|
const params = serializeLauncherUrlState({
|
||||||
|
viewId: "default",
|
||||||
|
config
|
||||||
|
});
|
||||||
|
navigate({ searchParams: params, replace: true });
|
||||||
|
router.refresh();
|
||||||
|
toast({
|
||||||
|
title: t("resourceLauncherViewSaved"),
|
||||||
|
description: t("resourceLauncherViewSavedDescription")
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("resourceLauncherViewSaveFailed"),
|
||||||
|
description: formatAxiosError(
|
||||||
|
error,
|
||||||
|
t("resourceLauncherViewSaveFailedDescription")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetSystemDefaultMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const resetAll = isAdmin && Boolean(defaultViewOverrides.orgWide);
|
||||||
|
await api.delete(`/org/${orgId}/launcher/default-view`, {
|
||||||
|
data: resetAll ? { all: true } : { orgWide: false }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
writeLauncherLastView(orgId, "default");
|
||||||
|
const nextOverrides: LauncherDefaultViewOverrides = {
|
||||||
|
personal: null,
|
||||||
|
orgWide:
|
||||||
|
isAdmin && defaultViewOverrides.orgWide
|
||||||
|
? null
|
||||||
|
: defaultViewOverrides.orgWide
|
||||||
|
};
|
||||||
|
const targetConfig =
|
||||||
|
getEffectiveDefaultLauncherConfig(nextOverrides);
|
||||||
|
searchInputRef.current = targetConfig.query;
|
||||||
|
setSearchInputResetKey((key) => key + 1);
|
||||||
|
const params = serializeLauncherUrlState({
|
||||||
|
viewId: "default",
|
||||||
|
config: targetConfig
|
||||||
|
});
|
||||||
|
navigate({ searchParams: params, replace: true });
|
||||||
|
router.refresh();
|
||||||
|
toast({
|
||||||
|
title: t("resourceLauncherSystemDefaultRestored"),
|
||||||
|
description: t(
|
||||||
|
"resourceLauncherSystemDefaultRestoredDescription"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("resourceLauncherViewSaveFailed"),
|
||||||
|
description: formatAxiosError(
|
||||||
|
error,
|
||||||
|
t("resourceLauncherViewSaveFailedDescription")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const deleteViewMutation = useMutation({
|
const deleteViewMutation = useMutation({
|
||||||
mutationFn: async (viewId: number) => {
|
mutationFn: async (viewId: number) => {
|
||||||
await api.delete(`/org/${orgId}/launcher/views/${viewId}`);
|
await api.delete(`/org/${orgId}/launcher/views/${viewId}`);
|
||||||
@@ -322,7 +428,11 @@ export default function ResourceLauncher({
|
|||||||
writeLauncherLastView(orgId, "default");
|
writeLauncherLastView(orgId, "default");
|
||||||
const params = serializeLauncherUrlState({
|
const params = serializeLauncherUrlState({
|
||||||
viewId: "default",
|
viewId: "default",
|
||||||
config: getLauncherUrlBaseConfig("default", views)
|
config: getLauncherUrlBaseConfig(
|
||||||
|
"default",
|
||||||
|
views,
|
||||||
|
defaultViewOverrides
|
||||||
|
)
|
||||||
});
|
});
|
||||||
navigate({ searchParams: params, replace: true });
|
navigate({ searchParams: params, replace: true });
|
||||||
router.refresh();
|
router.refresh();
|
||||||
@@ -372,6 +482,10 @@ export default function ResourceLauncher({
|
|||||||
navigateToConfig(activeViewIdRef.current, savedConfig);
|
navigateToConfig(activeViewIdRef.current, savedConfig);
|
||||||
}, [navigateToConfig, savedConfig]);
|
}, [navigateToConfig, savedConfig]);
|
||||||
|
|
||||||
|
const handleResetSystemDefault = useCallback(() => {
|
||||||
|
resetSystemDefaultMutation.mutate();
|
||||||
|
}, [resetSystemDefaultMutation]);
|
||||||
|
|
||||||
const refreshData = () => {
|
const refreshData = () => {
|
||||||
startRefreshTransition(async () => {
|
startRefreshTransition(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -387,7 +501,14 @@ export default function ResourceLauncher({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveToCurrent = () => {
|
const handleSaveToCurrent = () => {
|
||||||
if (isDefaultView || (isOrgWideView && !isAdmin)) {
|
if (isDefaultView) {
|
||||||
|
saveDefaultViewMutation.mutate({
|
||||||
|
config,
|
||||||
|
orgWide: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isOrgWideView && !isAdmin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateViewMutation.mutate({
|
updateViewMutation.mutate({
|
||||||
@@ -404,6 +525,10 @@ export default function ResourceLauncher({
|
|||||||
|
|
||||||
const handleSaveForEveryone = () => {
|
const handleSaveForEveryone = () => {
|
||||||
if (isDefaultView) {
|
if (isDefaultView) {
|
||||||
|
saveDefaultViewMutation.mutate({
|
||||||
|
config,
|
||||||
|
orgWide: true
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateViewMutation.mutate({
|
updateViewMutation.mutate({
|
||||||
@@ -456,19 +581,8 @@ export default function ResourceLauncher({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderToolbarActions = () => (
|
const renderToolbarFilterSort = () => (
|
||||||
<>
|
<>
|
||||||
<LauncherSaveViewMenu
|
|
||||||
isDefaultView={isDefaultView}
|
|
||||||
isAdmin={isAdmin}
|
|
||||||
isOrgWideView={isOrgWideView}
|
|
||||||
hasUnsavedChanges={hasUnsavedChanges}
|
|
||||||
onSaveToCurrent={handleSaveToCurrent}
|
|
||||||
onSaveAsNew={handleSaveAsNew}
|
|
||||||
onSaveForEveryone={handleSaveForEveryone}
|
|
||||||
onMakePersonal={handleMakePersonal}
|
|
||||||
onResetView={handleResetView}
|
|
||||||
/>
|
|
||||||
<LauncherFilterPopover
|
<LauncherFilterPopover
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
selectedSites={selectedSites}
|
selectedSites={selectedSites}
|
||||||
@@ -492,18 +606,31 @@ export default function ResourceLauncher({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderToolbarActions = () => (
|
||||||
|
<>
|
||||||
|
<LauncherSaveViewMenu
|
||||||
|
isDefaultView={isDefaultView}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
isOrgWideView={isOrgWideView}
|
||||||
|
hasUnsavedChanges={hasUnsavedChanges}
|
||||||
|
canResetSystemDefault={canResetSystemDefault}
|
||||||
|
onSaveToCurrent={handleSaveToCurrent}
|
||||||
|
onSaveAsNew={handleSaveAsNew}
|
||||||
|
onSaveForEveryone={handleSaveForEveryone}
|
||||||
|
onMakePersonal={handleMakePersonal}
|
||||||
|
onResetView={handleResetView}
|
||||||
|
onResetSystemDefault={handleResetSystemDefault}
|
||||||
|
onDeleteView={() => setDeleteDialogOpen(true)}
|
||||||
|
/>
|
||||||
<LauncherSettingsMenu
|
<LauncherSettingsMenu
|
||||||
config={config}
|
config={config}
|
||||||
isDefaultView={isDefaultView}
|
|
||||||
capabilities={scale.capabilities}
|
capabilities={scale.capabilities}
|
||||||
isCompactMode={scale.mode === "compact"}
|
isCompactMode={scale.mode === "compact"}
|
||||||
selectedGroupBy={effectiveConfig.groupBy}
|
selectedGroupBy={effectiveConfig.groupBy}
|
||||||
onConfigChange={applyConfigPatch}
|
onConfigChange={applyConfigPatch}
|
||||||
onDeleteView={() => {
|
|
||||||
if (!isDefaultView) {
|
|
||||||
deleteViewMutation.mutate(activeViewId);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<LauncherRefreshButton
|
<LauncherRefreshButton
|
||||||
onRefresh={refreshData}
|
onRefresh={refreshData}
|
||||||
@@ -530,6 +657,9 @@ export default function ResourceLauncher({
|
|||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
<div className="mb-6 flex w-full min-w-0 items-center gap-3">
|
<div className="mb-6 flex w-full min-w-0 items-center gap-3">
|
||||||
{renderToolbarSearch("w-64")}
|
{renderToolbarSearch("w-64")}
|
||||||
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
|
{renderToolbarFilterSort()}
|
||||||
|
</div>
|
||||||
<div className="min-w-0 flex-1 overflow-x-auto">
|
<div className="min-w-0 flex-1 overflow-x-auto">
|
||||||
{renderToolbarViews()}
|
{renderToolbarViews()}
|
||||||
</div>
|
</div>
|
||||||
@@ -542,7 +672,12 @@ export default function ResourceLauncher({
|
|||||||
<div className="flex items-center gap-2 overflow-x-auto">
|
<div className="flex items-center gap-2 overflow-x-auto">
|
||||||
{renderToolbarActions()}
|
{renderToolbarActions()}
|
||||||
</div>
|
</div>
|
||||||
{renderToolbarSearch("w-full")}
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
{renderToolbarSearch("w-full")}
|
||||||
|
</div>
|
||||||
|
{renderToolbarFilterSort()}
|
||||||
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
{renderToolbarViews()}
|
{renderToolbarViews()}
|
||||||
</div>
|
</div>
|
||||||
@@ -569,6 +704,22 @@ export default function ResourceLauncher({
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{activeSavedView ? (
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={deleteDialogOpen}
|
||||||
|
setOpen={setDeleteDialogOpen}
|
||||||
|
string={activeSavedView.name}
|
||||||
|
title={t("resourceLauncherDeleteViewTitle")}
|
||||||
|
buttonText={t("resourceLauncherDeleteViewConfirm")}
|
||||||
|
dialog={<p>{t("resourceLauncherDeleteViewQuestion")}</p>}
|
||||||
|
onConfirm={async () => {
|
||||||
|
await deleteViewMutation.mutateAsync(
|
||||||
|
activeSavedView.viewId
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Credenza open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
|
<Credenza open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
|
||||||
<CredenzaContent>
|
<CredenzaContent>
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { buildLauncherSearchParams } from "@app/lib/launcherSearchParams";
|
|||||||
import type {
|
import type {
|
||||||
LauncherGroup,
|
LauncherGroup,
|
||||||
LauncherScaleInfo,
|
LauncherScaleInfo,
|
||||||
|
LauncherDefaultViewOverrides,
|
||||||
LauncherViewConfig,
|
LauncherViewConfig,
|
||||||
LauncherViewRecord,
|
LauncherViewRecord,
|
||||||
ListLauncherGroupsResponse,
|
ListLauncherGroupsResponse,
|
||||||
@@ -16,6 +17,7 @@ import { AxiosResponse } from "axios";
|
|||||||
|
|
||||||
export type LauncherPageData = {
|
export type LauncherPageData = {
|
||||||
views: LauncherViewRecord[];
|
views: LauncherViewRecord[];
|
||||||
|
defaultViewOverrides: LauncherDefaultViewOverrides;
|
||||||
activeViewId: LauncherActiveViewId;
|
activeViewId: LauncherActiveViewId;
|
||||||
config: LauncherViewConfig;
|
config: LauncherViewConfig;
|
||||||
savedConfig: LauncherViewConfig;
|
savedConfig: LauncherViewConfig;
|
||||||
@@ -36,17 +38,23 @@ export async function fetchLauncherPageData(
|
|||||||
>
|
>
|
||||||
): Promise<LauncherPageData> {
|
): Promise<LauncherPageData> {
|
||||||
let views: LauncherViewRecord[] = [];
|
let views: LauncherViewRecord[] = [];
|
||||||
|
let defaultViewOverrides: LauncherDefaultViewOverrides = {
|
||||||
|
personal: null,
|
||||||
|
orgWide: null
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const viewsRes = await internal.get<
|
const viewsRes = await internal.get<
|
||||||
AxiosResponse<ListLauncherViewsResponse>
|
AxiosResponse<ListLauncherViewsResponse>
|
||||||
>(`/org/${orgId}/launcher/views`, cookieHeader);
|
>(`/org/${orgId}/launcher/views`, cookieHeader);
|
||||||
views = viewsRes.data.data.views;
|
views = viewsRes.data.data.views;
|
||||||
|
defaultViewOverrides = viewsRes.data.data.defaultViewOverrides;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
const { activeViewId, config, savedConfig } = resolveLauncherStateFromUrl(
|
const { activeViewId, config, savedConfig } = resolveLauncherStateFromUrl(
|
||||||
searchParams,
|
searchParams,
|
||||||
views,
|
views,
|
||||||
null
|
null,
|
||||||
|
defaultViewOverrides
|
||||||
);
|
);
|
||||||
|
|
||||||
const scaleFilters = {
|
const scaleFilters = {
|
||||||
@@ -110,6 +118,7 @@ export async function fetchLauncherPageData(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
views,
|
views,
|
||||||
|
defaultViewOverrides,
|
||||||
activeViewId,
|
activeViewId,
|
||||||
config,
|
config,
|
||||||
savedConfig,
|
savedConfig,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { LauncherActiveViewId } from "@app/lib/launcherLocalStorage";
|
import type { LauncherActiveViewId } from "@app/lib/launcherLocalStorage";
|
||||||
import {
|
import {
|
||||||
defaultLauncherViewConfig,
|
defaultLauncherViewConfig,
|
||||||
|
getEffectiveDefaultLauncherConfig,
|
||||||
parseIdListParam,
|
parseIdListParam,
|
||||||
|
type LauncherDefaultViewOverrides,
|
||||||
type LauncherViewConfig,
|
type LauncherViewConfig,
|
||||||
type LauncherViewRecord
|
type LauncherViewRecord
|
||||||
} from "@server/routers/launcher/types";
|
} from "@server/routers/launcher/types";
|
||||||
@@ -64,10 +66,13 @@ export function isLauncherConfigEqual(
|
|||||||
|
|
||||||
export function getLauncherUrlBaseConfig(
|
export function getLauncherUrlBaseConfig(
|
||||||
viewId: LauncherActiveViewId,
|
viewId: LauncherActiveViewId,
|
||||||
views: LauncherViewRecord[]
|
views: LauncherViewRecord[],
|
||||||
|
defaultViewOverrides?: LauncherDefaultViewOverrides
|
||||||
): LauncherViewConfig {
|
): LauncherViewConfig {
|
||||||
if (viewId === "default") {
|
if (viewId === "default") {
|
||||||
return defaultLauncherViewConfig;
|
return defaultViewOverrides
|
||||||
|
? getEffectiveDefaultLauncherConfig(defaultViewOverrides)
|
||||||
|
: defaultLauncherViewConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedView = views.find((view) => view.viewId === viewId);
|
const savedView = views.find((view) => view.viewId === viewId);
|
||||||
@@ -172,7 +177,8 @@ function isValidActiveViewId(
|
|||||||
export function resolveLauncherStateFromUrl(
|
export function resolveLauncherStateFromUrl(
|
||||||
searchParams: URLSearchParams,
|
searchParams: URLSearchParams,
|
||||||
views: LauncherViewRecord[],
|
views: LauncherViewRecord[],
|
||||||
fallbackViewId: LauncherActiveViewId | null
|
fallbackViewId: LauncherActiveViewId | null,
|
||||||
|
defaultViewOverrides?: LauncherDefaultViewOverrides
|
||||||
): ResolvedLauncherState {
|
): ResolvedLauncherState {
|
||||||
const parsed = parseLauncherUrlState(searchParams);
|
const parsed = parseLauncherUrlState(searchParams);
|
||||||
|
|
||||||
@@ -188,18 +194,26 @@ export function resolveLauncherStateFromUrl(
|
|||||||
: "default";
|
: "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedConfig = getLauncherUrlBaseConfig(activeViewId, views);
|
const savedConfig = getLauncherUrlBaseConfig(
|
||||||
|
activeViewId,
|
||||||
|
views,
|
||||||
|
defaultViewOverrides
|
||||||
|
);
|
||||||
|
|
||||||
let config: LauncherViewConfig;
|
let config: LauncherViewConfig;
|
||||||
if (hasLauncherConfigParams(searchParams)) {
|
if (hasLauncherConfigParams(searchParams)) {
|
||||||
config = resolveLauncherConfig(
|
config = resolveLauncherConfig(
|
||||||
defaultLauncherViewConfig,
|
getEffectiveDefaultLauncherConfig(
|
||||||
|
defaultViewOverrides ?? { personal: null, orgWide: null }
|
||||||
|
),
|
||||||
parsed.configOverrides
|
parsed.configOverrides
|
||||||
);
|
);
|
||||||
} else if (activeViewId !== "default") {
|
} else if (activeViewId !== "default") {
|
||||||
config = savedConfig;
|
config = savedConfig;
|
||||||
} else {
|
} else {
|
||||||
config = defaultLauncherViewConfig;
|
config = getEffectiveDefaultLauncherConfig(
|
||||||
|
defaultViewOverrides ?? { personal: null, orgWide: null }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+1
-1
@@ -1190,7 +1190,7 @@ export const launcherQueries = {
|
|||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<ListLauncherViewsResponse>
|
AxiosResponse<ListLauncherViewsResponse>
|
||||||
>(`/org/${orgId}/launcher/views`, { signal });
|
>(`/org/${orgId}/launcher/views`, { signal });
|
||||||
return res.data.data.views;
|
return res.data.data;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
sites: ({
|
sites: ({
|
||||||
|
|||||||
Reference in New Issue
Block a user