mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-28 02:41:36 +08:00
fix(settings): accurate View navigation after restore in recently deleted (#4546)
* fix(settings): accurate View navigation after restore in recently deleted
Files now deep-link to /files/{id} (was /files), and folders expand
the restored folder + its parent chain in the sidebar before navigating
to /w so the user actually lands on the item they restored.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: fall back to archived folders for parent-chain lookup
The restored folder may not be in the active folders cache yet when
View is clicked (the invalidation+refetch fires from onSettled, after
onSuccess surfaces the View button). Merge archived folder data — where
the restored item still lives — into the lookup map so the expansion
loop can always resolve the folder and walk its parent chain.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore(styling): canonicalize size-* shorthand for equal height/width
Document the size-* shorthand as the canonical pattern across CLAUDE.md,
AGENTS.md, .claude rules, .cursor + .agents commands, and the emcn
design-review skill. Default icon size is size-[14px]. Treat
h-[Npx] w-[Npx] and h-N w-N pairs as refactor targets.
Also migrate the remaining occurrences in recently-deleted.tsx.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
3ed8615b5d
commit
10f7d36f13
@@ -234,7 +234,7 @@ Use for context menus and action menus:
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<MoreHorizontal className="h-[14px] w-[14px]" />
|
||||
<MoreHorizontal className="size-[14px]" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
@@ -281,19 +281,21 @@ Rules:
|
||||
- Stack multiple skeletons for lists
|
||||
|
||||
### Icons
|
||||
Standard sizing — `h-[14px] w-[14px]` is the dominant pattern (400+ uses):
|
||||
Standard sizing — use the `size-*` shorthand. `size-[14px]` is the dominant pattern:
|
||||
|
||||
```tsx
|
||||
<Icon className="h-[14px] w-[14px] text-[var(--text-icon)]" />
|
||||
<Icon className="size-[14px] text-[var(--text-icon)]" />
|
||||
```
|
||||
|
||||
Size scale by frequency:
|
||||
1. `h-[14px] w-[14px]` — default for inline icons (most common)
|
||||
2. `h-[16px] w-[16px]` — slightly larger inline icons
|
||||
3. `h-3 w-3` (12px) — compact/tight spaces
|
||||
4. `h-4 w-4` (16px) — Tailwind equivalent, also common
|
||||
5. `h-3.5 w-3.5` (14px) — Tailwind equivalent of 14px
|
||||
6. `h-5 w-5` (20px) — larger icons, section headers
|
||||
Always prefer `size-*` over the legacy `h-* w-*` pair. `size-[14px]` is canonical; treat any `h-[Npx] w-[Npx]` or `h-N w-N` pair as a refactor target.
|
||||
|
||||
Size scale (most common first):
|
||||
1. `size-[14px]` — default for inline icons
|
||||
2. `size-[16px]` — slightly larger inline icons
|
||||
3. `size-3` (12px) — compact/tight spaces
|
||||
4. `size-4` (16px) — Tailwind equivalent
|
||||
5. `size-3.5` (14px) — Tailwind equivalent of 14px
|
||||
6. `size-5` (20px) — larger icons, section headers
|
||||
|
||||
Use `text-[var(--text-icon)]` for icon color (113+ uses in codebase).
|
||||
|
||||
@@ -332,4 +334,5 @@ Use `text-[var(--text-icon)]` for icon color (113+ uses in codebase).
|
||||
- Importing from emcn subpaths instead of barrel export
|
||||
- Using arbitrary z-index (`z-50`, `z-[9999]`) instead of z-index tokens
|
||||
- Custom shadows instead of shadow tokens
|
||||
- Icon sizes that don't follow the established scale (default to `h-[14px] w-[14px]`)
|
||||
- Icon sizes that don't follow the established scale (default to `size-[14px]`)
|
||||
- Splitting equal height/width into `h-* w-*` pairs instead of the `size-*` shorthand
|
||||
|
||||
@@ -65,7 +65,7 @@ Modal `size="sm"`, title "Delete/Remove {ItemType}", `variant="destructive"` act
|
||||
|
||||
## Icons
|
||||
|
||||
Default: `h-[14px] w-[14px]` (400+ uses). Color: `text-[var(--text-icon)]`. Scale: 14px > 16px > 12px > 20px.
|
||||
Default: `size-[14px]`. Color: `text-[var(--text-icon)]`. Scale: 14px > 16px > 12px > 20px. Use the `size-*` shorthand — flag `h-[Npx] w-[Npx]` and `h-N w-N` pairs as refactor targets.
|
||||
|
||||
## Anti-patterns to flag
|
||||
|
||||
|
||||
@@ -32,4 +32,5 @@ function Label({ className, ...props }) {
|
||||
- Export component and variants (if using CVA)
|
||||
- TSDoc with usage examples
|
||||
- Consistent tokens: `font-medium`, `text-[12px]`, `rounded-[4px]`
|
||||
- Equal height/width → `size-*` (e.g. `size-[14px]`, `size-4`), never `h-[Npx] w-[Npx]` or `h-N w-N`. Default icon size is `size-[14px]`
|
||||
- `transition-colors` for hover states
|
||||
|
||||
@@ -11,7 +11,8 @@ paths:
|
||||
1. **No inline styles** - Use Tailwind classes
|
||||
2. **No duplicate dark classes** - Skip `dark:` when value matches light mode
|
||||
3. **Exact values** - `text-[14px]`, `h-[26px]`
|
||||
4. **Transitions** - `transition-colors` for interactive states
|
||||
4. **Equal h/w → `size-*`** - Use `size-[14px]` / `size-4`, never `h-[14px] w-[14px]` or `h-4 w-4`. Default icon size is `size-[14px]`
|
||||
5. **Transitions** - `transition-colors` for interactive states
|
||||
|
||||
## Conditional Classes
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ Modal `size="sm"`, title "Delete/Remove {ItemType}", `variant="destructive"` act
|
||||
|
||||
## Icons
|
||||
|
||||
Default: `h-[14px] w-[14px]` (400+ uses). Color: `text-[var(--text-icon)]`. Scale: 14px > 16px > 12px > 20px.
|
||||
Default: `size-[14px]`. Color: `text-[var(--text-icon)]`. Scale: 14px > 16px > 12px > 20px. Use the `size-*` shorthand — flag `h-[Npx] w-[Npx]` and `h-N w-N` pairs as refactor targets.
|
||||
|
||||
## Anti-patterns to flag
|
||||
|
||||
|
||||
@@ -357,6 +357,12 @@ Use Tailwind only, no inline styles. Use `cn()` from `@/lib/utils` for condition
|
||||
<div className={cn('base-classes', isActive && 'active-classes')} />
|
||||
```
|
||||
|
||||
For equal height and width, use the `size-*` shorthand — never `h-[Npx] w-[Npx]` or `h-N w-N`. Default icon size is `size-[14px]`.
|
||||
|
||||
```typescript
|
||||
<Icon className='size-[14px] text-[var(--text-icon)]' />
|
||||
```
|
||||
|
||||
## EMCN Components
|
||||
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA when 2+ variants exist.
|
||||
|
||||
@@ -347,6 +347,12 @@ Use Tailwind only, no inline styles. Use `cn()` from `@/lib/utils` for condition
|
||||
<div className={cn('base-classes', isActive && 'active-classes')} />
|
||||
```
|
||||
|
||||
For equal height and width, use the `size-*` shorthand — never `h-[Npx] w-[Npx]` or `h-N w-N`. Default icon size is `size-[14px]`.
|
||||
|
||||
```typescript
|
||||
<Icon className='size-[14px] text-[var(--text-icon)]' />
|
||||
```
|
||||
|
||||
## EMCN Components
|
||||
|
||||
Import from `@/components/emcn`, never from subpaths (except CSS files). Use CVA when 2+ variants exist.
|
||||
|
||||
+31
-23
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { formatDate } from '@sim/utils/formatting'
|
||||
import { Folder, Search } from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
@@ -21,6 +22,10 @@ import { useKnowledgeBasesQuery, useRestoreKnowledgeBase } from '@/hooks/queries
|
||||
import { useRestoreTable, useTablesList } from '@/hooks/queries/tables'
|
||||
import { useRestoreWorkflow, useWorkflows } from '@/hooks/queries/workflows'
|
||||
import { useRestoreWorkspaceFile, useWorkspaceFiles } from '@/hooks/queries/workspace-files'
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
import type { WorkflowFolder } from '@/stores/folders/types'
|
||||
|
||||
type ResourceType = 'all' | 'workflow' | 'table' | 'knowledge' | 'file' | 'folder'
|
||||
|
||||
function getResourceHref(
|
||||
workspaceId: string,
|
||||
@@ -36,14 +41,12 @@ function getResourceHref(
|
||||
case 'knowledge':
|
||||
return `${base}/knowledge/${id}`
|
||||
case 'file':
|
||||
return `${base}/files`
|
||||
return `${base}/files/${id}`
|
||||
case 'folder':
|
||||
return `${base}/w`
|
||||
}
|
||||
}
|
||||
|
||||
type ResourceType = 'all' | 'workflow' | 'table' | 'knowledge' | 'file' | 'folder'
|
||||
|
||||
type SortColumn = 'deleted' | 'name' | 'type'
|
||||
|
||||
interface SortConfig {
|
||||
@@ -59,7 +62,7 @@ const SORT_OPTIONS: { column: SortColumn; direction: 'asc' | 'desc'; label: stri
|
||||
{ column: 'type', direction: 'asc', label: 'Type (A–Z)' },
|
||||
]
|
||||
|
||||
const ICON_CLASS = 'h-[14px] w-[14px]'
|
||||
const ICON_CLASS = 'size-[14px]'
|
||||
|
||||
const RESOURCE_TYPE_TO_MOTHERSHIP: Partial<
|
||||
Record<Exclude<ResourceType, 'all'>, MothershipResourceType>
|
||||
@@ -119,13 +122,9 @@ function ResourceIcon({ resource }: { resource: DeletedResource }) {
|
||||
const mothershipType = RESOURCE_TYPE_TO_MOTHERSHIP[resource.type]
|
||||
if (!mothershipType) return null
|
||||
const config = RESOURCE_REGISTRY[mothershipType]
|
||||
return (
|
||||
<>
|
||||
{config.renderTabIcon(
|
||||
{ type: mothershipType, id: resource.id, title: resource.name },
|
||||
ICON_CLASS
|
||||
)}
|
||||
</>
|
||||
return config.renderTabIcon(
|
||||
{ type: mothershipType, id: resource.id, title: resource.name },
|
||||
ICON_CLASS
|
||||
)
|
||||
}
|
||||
|
||||
@@ -141,6 +140,7 @@ export function RecentlyDeleted() {
|
||||
|
||||
const workflowsQuery = useWorkflows(workspaceId, { scope: 'archived' })
|
||||
const foldersQuery = useFolders(workspaceId, { scope: 'archived' })
|
||||
const activeFoldersQuery = useFolders(workspaceId)
|
||||
const tablesQuery = useTablesList(workspaceId, 'archived')
|
||||
const knowledgeQuery = useKnowledgeBasesQuery(workspaceId, { scope: 'archived' })
|
||||
const filesQuery = useWorkspaceFiles(workspaceId, 'archived')
|
||||
@@ -220,7 +220,6 @@ export function RecentlyDeleted() {
|
||||
})
|
||||
}
|
||||
|
||||
// Merge back restored items that are no longer in the query data
|
||||
const itemIds = new Set(items.map((i) => i.id))
|
||||
for (const [id, resource] of restoredItems) {
|
||||
if (!itemIds.has(id)) {
|
||||
@@ -267,6 +266,23 @@ export function RecentlyDeleted() {
|
||||
const showNoResults = searchTerm.trim() && filtered.length === 0 && resources.length > 0
|
||||
const selectedSort = activeSort ?? DEFAULT_SORT
|
||||
|
||||
function handleView(resource: DeletedResource) {
|
||||
if (resource.type === 'folder') {
|
||||
const setExpanded = useFolderStore.getState().setExpanded
|
||||
const byId = new Map<string, WorkflowFolder>()
|
||||
for (const folder of foldersQuery.data ?? []) byId.set(folder.id, folder)
|
||||
for (const folder of activeFoldersQuery.data ?? []) byId.set(folder.id, folder)
|
||||
let current: WorkflowFolder | undefined = byId.get(resource.id)
|
||||
const seen = new Set<string>()
|
||||
while (current && !seen.has(current.id)) {
|
||||
seen.add(current.id)
|
||||
setExpanded(current.id, true)
|
||||
current = current.parentId ? byId.get(current.parentId) : undefined
|
||||
}
|
||||
}
|
||||
router.push(getResourceHref(resource.workspaceId, resource.type, resource.id))
|
||||
}
|
||||
|
||||
function handleRestore(resource: DeletedResource) {
|
||||
setRestoringIds((prev) => new Set(prev).add(resource.id))
|
||||
|
||||
@@ -363,7 +379,7 @@ export function RecentlyDeleted() {
|
||||
{error ? (
|
||||
<div className='flex h-full flex-col items-center justify-center gap-2'>
|
||||
<p className='text-[var(--text-error)] text-xs leading-tight'>
|
||||
{error instanceof Error ? error.message : 'Failed to load deleted items'}
|
||||
{toError(error).message || 'Failed to load deleted items'}
|
||||
</p>
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
@@ -387,7 +403,7 @@ export function RecentlyDeleted() {
|
||||
return (
|
||||
<div
|
||||
key={resource.id}
|
||||
className='flex items-center gap-3 rounded-md p-2 hover-hover:bg-[var(--bg-hover)]'
|
||||
className='flex items-center gap-3 rounded-md px-2 py-2 hover-hover:bg-[var(--bg-hover)]'
|
||||
>
|
||||
<ResourceIcon resource={resource} />
|
||||
|
||||
@@ -405,15 +421,7 @@ export function RecentlyDeleted() {
|
||||
{isRestored ? (
|
||||
<div className='flex shrink-0 items-center gap-2'>
|
||||
<span className='text-[var(--text-tertiary)] text-small'>Restored</span>
|
||||
<Button
|
||||
variant='primary'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
router.push(
|
||||
getResourceHref(resource.workspaceId, resource.type, resource.id)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Button variant='primary' size='sm' onClick={() => handleView(resource)}>
|
||||
View
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user