اولش باز کردن resource فرزند داخل modal کاملاً منطقی بود. یک table کوچک بود و چند action. کاربر از صفحه parent خارج نمیشد و برای چیزی که آن زمان ساده بود، ساختن route جدا واقعاً اضافهکاری به نظر میرسید.
مشکل وقتی شروع شد که همان resource خودش form و CRUD پیدا کرد.
edit کردن یکی از rowها modal دیگری باز میکرد. بعد child بعدی هم اضافه شد. بعد filter و permission و workflow آمدند وسط.
UI هنوز کار میکرد، ولی چیزی که داخل overlay بود دیگر یک interaction کوتاه نبود. عملاً یک page کامل بود که فقط اتفاقی داخل modal گیر کرده بود.
Parent Page
↓
Child Modal
↓
Edit Modal
↓
Nested Childاز یک جایی به بعد، مشکل دیگر فقط شلوغی UI نبود. context به state محلی parent وابسته بود، refresh میتوانست flow را از بین ببرد، deep link معنی نداشت و resource فرزند فضای کافی برای خودش نداشت.
چه زمانی modal دیگر کافی نیست؟
نتیجهای که گرفتم این نبود که modal بد است.
confirmation، form کوچک، upload یا selection محدود هنوز داخل modal کاملاً منطقیاند.
فرق در خود resource است.
اگر یک child این چیزها را پیدا کند:
- CRUD مستقل
- table state
- چند action جدی
- permission یا workflow خودش
- child بعدی
- identityای که URL برایش معنی داشته باشد
احتمالاً دیگر فقط محتوای موقت parent نیست.
workspace خودش را میخواهد.
context والد را وارد route کردم
برای resourceهای پیچیدهتر، مدیریت child را از modal بیرون آوردم و به routeهای مستقل React Router منتقل کردم.
parent id داخل route قرار گرفت و page فرزند query خودش را از همان context میساخت.
مثلاً بهشکل عمومی:
/parents/:parentId/children
/parents/:parentId/children/:childId/detailsاینها route واقعی سیستم نیستند، فقط شکل مسئله را نشان میدهند.
نکته مهم این بود که parent دیگر فقط تا وقتی component قبلی mount است وجود نداشت.
بعد از این تغییر:
- refresh همان context را برمیگرداند
- لینک مستقیم معنی داشت
- back/forward مرورگر قابل استفاده شد
- breadcrumb میتوانست hierarchy را دوباره بسازد
- child page دیگر به زنده ماندن parent page وابسته نبود
اولین استفاده جدی این pattern برای childهای بخش Workflow بود. خود business logic آن بخش کاملاً کار من نبود؛ چیزی که من تغییر دادم integration فرانتاند و مدل navigation آن بود.
در همان migration، دو خانواده resource فرزند ــ Step و Transition ــ از مدیریت modalمحور به صفحههای routeشده و parent-aware منتقل شدند. این count کوچک بود، اما برای آزمودن route context، breadcrumb و navigation مستقل روی دو flow واقعی کافی بود.
breadcrumb فقط برای قشنگی نبود
داخل ERP عنوانهایی مثل Steps یا Parts بهتنهایی خیلی بیمعنیاند.
چند parent مختلف میتوانند دقیقاً همین نوع child را داشته باشند.
پس کاربر باید بفهمد table فعلی متعلق به کجاست.
برای همین page فرزند اطلاعات parent را دوباره load میکرد و hierarchy را داخل UI نشان میداد.
من برای خودم یک تست ساده داشتم:
اگر یک page بعد از refresh نتواند فقط از روی route و data context خودش را دوباره بسازد، هنوز بیش از حد به flow قبلی وابسته است.
route مشکل identity را حل کرد، ولی یک مشکل دیگر باقی ماند
بعد از route شدن resourceها، یک رفتار دیگر بیشتر خودش را نشان داد.
کاربر ERP معمولاً یک کار را کامل نمیکند و بعد سراغ بعدی نمیرود.
ممکن است یک resource را باز کند، برود چیزی را در صفحه دیگری چک کند، برگردد به table قبلی که filter شده، بعد دوباره برود سراغ resource اول.
navigation معمول مرورگر این حرکت را خطی میکرد، ولی خود کار خطی نبود.
اینجا workspace چندتب وارد شد.
هر tab نماینده یک route بود، نه یک instance تصادفی از component.
Route A → Tab A
Route B → Tab B
Route C → Tab Cاگر همان path دوباره باز میشد، tab قبلی active میشد و duplicate جدید ساخته نمیشد.
Sidebar هم باید از همین مدل استفاده میکرد.
resourceهای nested هم title وابسته به parent میگرفتند، وگرنه خیلی سریع چند tab با اسمهای عمومی مثل Steps کنار هم داشتیم که هیچکس نمیفهمید کدام مربوط به چیست.
هر tab قرار نبود برای همیشه بماند
اولین نسخه اگر هر مقصد را نگه میداشت، خیلی زود tab bar تبدیل به history شلوغ میشد.
برای همین temporary و retained از هم جدا شدند.
workspace در عمل دو mode اصلی داشت: tab موقت برای navigation گذرا و tab permanent برای contextی که باید حفظ میشد. pin کردن، همان tab موقت را به حالت دوم ارتقا میداد؛ mode سومی با lifecycle جدا وجود نداشت.
navigation معمول میتوانست یک tab موقت بسازد. اگر کاربر میخواست آن context را نگه دارد، میتوانست pin شود.
این تفاوت وقتی table state وارد بازی شد مهمتر شد.
فرض کن کاربر روی یک table:
- search کرده
- filter گذاشته
- sort را تغییر داده
- چند row انتخاب کرده
- یا pagination را جلو برده
در آن نقطه دیگر صفحه فقط یک مقصد گذری نیست. بخشی از کاربر روی آن باقی مانده.
جایگزین شدن بیهشدار چنین tabی با navigation بعدی تجربه خوبی نبود.
drag ordering و clear-all بعداً آمدند، ولی اصل ماجرا همان بود: بعضی routeها فقط برای نگاه کردن باز میشوند، بعضیها تبدیل به workspace واقعی میشوند.
URL و tab state باید درباره یک چیز حرف بزنند
بخش سختتر implementation این بود که چند منبع state داشتیم:
- React Router
- active tab
- لیست tabهای باز
- Sidebar
- titleهای dynamic
- table state
اگر هرکدام مستقل تصمیم میگرفتند، edge caseها خیلی سریع ظاهر میشدند.
مثلاً بستن یک tab میتوانست table state اشتباه را پاک کند، یا route change چیزی را که همین الان بسته شده بود دوباره باز کند.
برای همین tab state متمرکز شد و با route sync شد.
البته از روز اول بینقص نبود.
cleanup اولیه در بعضی حالتها current path را میدید، نه tabی را که واقعاً بسته شده بود. بعضی navigationها history را یکسان تغییر نمیدادند. keyboard behavior یکی از close controlها هم کامل نبود.
اینها بیشتر از اینکه مشکل tab component باشند، نشان میدادند workspace فقط UI نیست. route lifecycle و state lifecycle باید با هم هماهنگ باشند.
mobile را مجبور نکردم شبیه desktop باشد
workspace چندتب برای desktop ساخته شده بود.
روی mobile از قبل navigation مقصدمحور داشتیم. اینکه همان tab bar را کوچک کنیم و در عرض کم بچپانیم فقط ظاهر feature را حفظ میکرد، نه دلیل وجودش را.
پس interaction متفاوت ماند.
desktop میتوانست چند context را همزمان نگه دارد.
mobile سادهتر بین routeها جابهجا میشد و هر route کل فضای صفحه را میگرفت.
hierarchy یکی بود، chrome فرق میکرد.
modal هنوز سر جای خودش ماند
بعد از این تغییر modal حذف نشد.
create/edit متمرکز هنوز میتواند داخل modal باشد.
confirmation هم route نمیخواهد.
upload یا selection چند row هم تا وقتی navigation مستقل ندارند، همچنان interaction موقت خوبی هستند.
مرزی که هنوز برای خودم استفاده میکنم این است:
Temporary interaction → Modal
Resource with identity/state/history → Route
Several active routed contexts → Workspaceاگر برای یک resource لازم باشد URL، breadcrumb، table state و lifecycle مستقل داشته باشم، دیگر زور کردنش داخل modal معمولاً فقط مشکل را عقب میاندازد.