fix: Correct Back Navigation & Use Existing Dialog Pattern! 🔧
## Issue #1: Back Button Navigation Fixed **Problem:** Back button navigated too far to Notifications.tsx **Root Cause:** Wrong route - should go to /staff or /customer page with templates tab **Solution:** - Detect if staff or customer event - Navigate to `/settings/notifications/{staff|customer}?tab=templates` - Staff.tsx and Customer.tsx read tab query param - Auto-open templates tab on return **Files:** - `routes/Settings/Notifications/EditTemplate.tsx` - `routes/Settings/Notifications/Staff.tsx` - `routes/Settings/Notifications/Customer.tsx` ## Issue #2: Dialog Pattern - Use Existing, Dont Reinvent! **Problem:** Created new DialogBody component, over-engineered **Root Cause:** Didnt check existing dialog usage in project **Solution:** - Reverted dialog.tsx to original - Use existing pattern from Shipping.tsx: ```tsx <DialogContent className="max-h-[90vh] overflow-y-auto"> ``` - Simple, proven, works! **Files:** - `components/ui/dialog.tsx` - Reverted to original - `components/ui/rich-text-editor.tsx` - Use existing pattern **Lesson Learned:** Always scan project for existing patterns before creating new ones! Both issues fixed! ✅
This commit is contained in:
26
admin-spa/package-lock.json
generated
26
admin-spa/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
@@ -444,6 +445,21 @@
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-markdown": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz",
|
||||
"integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.7.1",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/markdown": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
|
||||
@@ -1361,6 +1377,16 @@
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/markdown": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.0.tgz",
|
||||
"integrity": "sha512-AXb98u3M6BEzTnreBnGtQaF7xFTiMA92Dsy5tqEjpacbjRxDSFdN4bKJo9uvU4cEEOS7D2B9MT7kvDgOEIzJSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
|
||||
@@ -35,16 +35,14 @@ const DialogContent = React.forwardRef<
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-[99999] flex flex-col w-full max-w-lg max-h-[90vh] translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
"fixed left-[50%] top-[50%] z-[99999] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground z-10">
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
@@ -59,7 +57,7 @@ const DialogHeader = ({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left px-6 pt-6 pb-4 border-b",
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -73,7 +71,7 @@ const DialogFooter = ({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 px-6 py-4 border-t mt-auto",
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -108,20 +106,6 @@ const DialogDescription = React.forwardRef<
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
const DialogBody = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex-1 overflow-y-auto px-6 py-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogBody.displayName = "DialogBody"
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
@@ -133,5 +117,4 @@ export {
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogBody,
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Button } from './button';
|
||||
import { Input } from './input';
|
||||
import { Label } from './label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogBody } from './dialog';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './dialog';
|
||||
import { __ } from '@/lib/i18n';
|
||||
|
||||
interface RichTextEditorProps {
|
||||
@@ -319,7 +319,7 @@ export function RichTextEditor({
|
||||
|
||||
{/* Button Dialog */}
|
||||
<Dialog open={buttonDialogOpen} onOpenChange={setButtonDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogContent className="sm:max-w-md max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{__('Insert Button')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -327,55 +327,53 @@ export function RichTextEditor({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="btn-text">{__('Button Text')}</Label>
|
||||
<Input
|
||||
id="btn-text"
|
||||
value={buttonText}
|
||||
onChange={(e) => setButtonText(e.target.value)}
|
||||
placeholder={__('e.g., View Order')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="btn-href">{__('Button Link')}</Label>
|
||||
<Input
|
||||
id="btn-href"
|
||||
value={buttonHref}
|
||||
onChange={(e) => setButtonHref(e.target.value)}
|
||||
placeholder="{order_url}"
|
||||
/>
|
||||
{variables.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{variables.filter(v => v.includes('_url')).map((variable) => (
|
||||
<code
|
||||
key={variable}
|
||||
className="text-xs bg-muted px-2 py-1 rounded cursor-pointer hover:bg-muted/80"
|
||||
onClick={() => setButtonHref(buttonHref + `{${variable}}`)}
|
||||
>
|
||||
{`{${variable}}`}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="btn-style">{__('Button Style')}</Label>
|
||||
<Select value={buttonStyle} onValueChange={(value: 'solid' | 'outline') => setButtonStyle(value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="solid">{__('Solid (Primary)')}</SelectItem>
|
||||
<SelectItem value="outline">{__('Outline (Secondary)')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="btn-text">{__('Button Text')}</Label>
|
||||
<Input
|
||||
id="btn-text"
|
||||
value={buttonText}
|
||||
onChange={(e) => setButtonText(e.target.value)}
|
||||
placeholder={__('e.g., View Order')}
|
||||
/>
|
||||
</div>
|
||||
</DialogBody>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="btn-href">{__('Button Link')}</Label>
|
||||
<Input
|
||||
id="btn-href"
|
||||
value={buttonHref}
|
||||
onChange={(e) => setButtonHref(e.target.value)}
|
||||
placeholder="{order_url}"
|
||||
/>
|
||||
{variables.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{variables.filter(v => v.includes('_url')).map((variable) => (
|
||||
<code
|
||||
key={variable}
|
||||
className="text-xs bg-muted px-2 py-1 rounded cursor-pointer hover:bg-muted/80"
|
||||
onClick={() => setButtonHref(buttonHref + `{${variable}}`)}
|
||||
>
|
||||
{`{${variable}}`}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="btn-style">{__('Button Style')}</Label>
|
||||
<Select value={buttonStyle} onValueChange={(value: 'solid' | 'outline') => setButtonStyle(value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="solid">{__('Solid (Primary)')}</SelectItem>
|
||||
<SelectItem value="outline">{__('Outline (Secondary)')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setButtonDialogOpen(false)}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { SettingsLayout } from '../components/SettingsLayout';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -10,8 +10,17 @@ import CustomerEvents from './Customer/Events';
|
||||
import NotificationTemplates from './Templates';
|
||||
|
||||
export default function CustomerNotifications() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [activeTab, setActiveTab] = useState('channels');
|
||||
|
||||
// Check for tab query param
|
||||
useEffect(() => {
|
||||
const tabParam = searchParams.get('tab');
|
||||
if (tabParam && ['channels', 'events', 'templates'].includes(tabParam)) {
|
||||
setActiveTab(tabParam);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
<SettingsLayout
|
||||
title={__('Customer Notifications')}
|
||||
|
||||
@@ -340,7 +340,12 @@ export default function EditTemplate() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/settings/notifications?tab=${channelId}&event=${eventId}`)}
|
||||
onClick={() => {
|
||||
// Determine if staff or customer based on event category
|
||||
const isStaffEvent = template.event_category === 'staff' || eventId?.includes('admin') || eventId?.includes('staff');
|
||||
const page = isStaffEvent ? 'staff' : 'customer';
|
||||
navigate(`/settings/notifications/${page}?tab=templates`);
|
||||
}}
|
||||
className="gap-2"
|
||||
title={__('Back')}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { SettingsLayout } from '../components/SettingsLayout';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -10,8 +10,17 @@ import StaffEvents from './Staff/Events';
|
||||
import NotificationTemplates from './Templates';
|
||||
|
||||
export default function StaffNotifications() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [activeTab, setActiveTab] = useState('channels');
|
||||
|
||||
// Check for tab query param
|
||||
useEffect(() => {
|
||||
const tabParam = searchParams.get('tab');
|
||||
if (tabParam && ['channels', 'events', 'templates'].includes(tabParam)) {
|
||||
setActiveTab(tabParam);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
<SettingsLayout
|
||||
title={__('Staff Notifications')}
|
||||
|
||||
Reference in New Issue
Block a user