1- import { type ActionFunctionArgs , redirect } from "@remix-run/node" ;
1+ import { type ActionFunctionArgs } from "@remix-run/node" ;
22import { nanoid } from "nanoid" ;
33import { z } from "zod" ;
44import { typedjson } from "remix-typedjson" ;
@@ -12,10 +12,11 @@ import {
1212} from "~/presenters/v3/MetricDashboardPresenter.server" ;
1313import { getCurrentPlan } from "~/services/platform.v3.server" ;
1414import { 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
1818const 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
7879const 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
8284const 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