Skip to content

Commit 0da1a8b

Browse files
committed
Fix for duplicating widget id clash
1 parent c954ae4 commit 0da1a8b

2 files changed

Lines changed: 19 additions & 17 deletions

File tree

apps/webapp/app/hooks/useDashboardEditor.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,9 @@ export function useDashboardEditor({
351351
dispatch({ type: "ADD_WIDGET", payload: { id, widget, layoutItem } });
352352

353353
// Queue sync to server (processed sequentially)
354+
// Send the client-generated ID so the server uses the same ID
354355
queueWidgetSync("add", {
356+
widgetId: id,
355357
title,
356358
query,
357359
config: JSON.stringify(config),
@@ -373,7 +375,9 @@ export function useDashboardEditor({
373375
dispatch({ type: "ADD_WIDGET", payload: { id, widget, layoutItem } });
374376

375377
// Queue sync to server (processed sequentially)
378+
// Send the client-generated ID so the server uses the same ID
376379
queueWidgetSync("add", {
380+
widgetId: id,
377381
title,
378382
query: "",
379383
config: JSON.stringify(config),
@@ -425,9 +429,8 @@ export function useDashboardEditor({
425429
dispatch({ type: "DUPLICATE_WIDGET", payload: { id: widgetId, newId } });
426430

427431
// Queue sync to server (processed sequentially)
428-
// Note: Server will generate its own ID, but our local state uses newId
429-
// This is fine since we're optimistic - the server state will be consistent
430-
queueWidgetSync("duplicate", { widgetId });
432+
// Send the client-generated newId so the server uses the same ID for the duplicate
433+
queueWidgetSync("duplicate", { widgetId, newId });
431434
},
432435
[countedWidgets, widgetLimit, onWidgetLimitReached, queueWidgetSync]
433436
);

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type ActionFunctionArgs, redirect } from "@remix-run/node";
1+
import { type ActionFunctionArgs } from "@remix-run/node";
22
import { nanoid } from "nanoid";
33
import { z } from "zod";
44
import { typedjson } from "remix-typedjson";
@@ -12,10 +12,11 @@ import {
1212
} from "~/presenters/v3/MetricDashboardPresenter.server";
1313
import { getCurrentPlan } from "~/services/platform.v3.server";
1414
import { requireUserId } from "~/services/session.server";
15-
import { EnvironmentParamSchema, v3CustomDashboardPath } from "~/utils/pathBuilder";
15+
import { EnvironmentParamSchema } from "~/utils/pathBuilder";
1616

1717
// Schemas for each action type
1818
const AddWidgetSchema = z.object({
19+
widgetId: z.string().min(1, "Widget ID is required").optional(),
1920
title: z.string().min(1, "Title is required"),
2021
query: z.string().default(""),
2122
config: z.string().transform((str, ctx) => {
@@ -77,6 +78,7 @@ const DeleteWidgetSchema = z.object({
7778

7879
const DuplicateWidgetSchema = z.object({
7980
widgetId: z.string().min(1, "Widget ID is required"),
81+
newId: z.string().min(1, "New widget ID is required").optional(),
8082
});
8183

8284
const SaveLayoutSchema = z.object({
@@ -188,6 +190,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
188190
switch (action) {
189191
case "add": {
190192
const rawData = {
193+
widgetId: formData.get("widgetId"),
191194
title: formData.get("title"),
192195
query: formData.get("query"),
193196
config: formData.get("config"),
@@ -210,8 +213,9 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
210213
await checkWidgetLimit();
211214
}
212215

213-
// Generate new widget ID
214-
const widgetId = nanoid(8);
216+
// Use client-provided widget ID if available, otherwise generate one
217+
// Using the client's ID ensures optimistic UI state stays in sync with the server
218+
const widgetId = result.data.widgetId || nanoid(8);
215219

216220
// Calculate position at the bottom
217221
let maxBottom = 0;
@@ -256,14 +260,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
256260
},
257261
});
258262

259-
// Redirect to the dashboard
260-
const dashboardPath = v3CustomDashboardPath(
261-
{ slug: organizationSlug },
262-
{ slug: projectParam },
263-
{ slug: envParam },
264-
{ friendlyId: dashboardId }
265-
);
266-
return redirect(dashboardPath);
263+
return typedjson({ success: true, widgetId });
267264
}
268265

269266
case "update": {
@@ -395,6 +392,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
395392

396393
const rawData = {
397394
widgetId: formData.get("widgetId"),
395+
newId: formData.get("newId"),
398396
};
399397

400398
const result = DuplicateWidgetSchema.safeParse(rawData);
@@ -416,8 +414,9 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
416414
throw new Response("Widget layout not found", { status: 404 });
417415
}
418416

419-
// Generate new widget ID
420-
const newWidgetId = nanoid(8);
417+
// Use client-provided ID if available, otherwise generate one
418+
// Using the client's ID ensures optimistic UI state stays in sync with the server
419+
const newWidgetId = result.data.newId || nanoid(8);
421420

422421
// Calculate position at the bottom
423422
let maxBottom = 0;

0 commit comments

Comments
 (0)