feat: Add sort functionality to Media Asset Panel (#6695)

## Overview
Adds sort functionality to the Media Asset Panel. Users can sort assets
by creation time in Cloud environments.

## Key Changes

### 1. Sort Functionality (Cloud Only)
- "Newest first" (most recent)
- "Oldest first" (oldest)
- Sorting based on `create_time` field (output assets)
- Sorting based on `created_at` field (input assets)
- Sort button is only displayed in Cloud environments

### 2. create_time Field Integration
**Related PR**: #6092

Implemented sort functionality using the `create_time` field introduced
in PR #6092. Applied the code from that PR directly to the following
files:

- `src/schemas/apiSchema.ts`: Added `create_time` field to `zExtraData`
- `src/stores/queueStore.ts`: Added `createTime` getter to
`TaskItemImpl`
- `src/platform/remote/comfyui/history/types/historyV2Types.ts`: Added
`create_time` to History V2 API response types
- `src/platform/remote/comfyui/history/adapters/v2ToV1Adapter.ts`: Pass
through `create_time` in V2→V1 adapter
- `src/platform/assets/composables/media/assetMappers.ts`: Include
`create_time` in asset metadata

### 3. Component Structure Improvements
Created new components following existing component styles for
consistency:

- **`MediaAssetSearchBar.vue`**: Component combining existing SearchBox
with sort button
- **`AssetSortButton.vue`**: Same structure as `MoreButton.vue`
(IconButton + Popover)
- **`MediaAssetSortMenu.vue`**: Same style as `MediaAssetMoreMenu.vue`
(using IconTextButton)
- **`AssetsSidebarTab.vue`**: Refactored to use `MediaAssetSearchBar`

### 4. Utility Usage
- Improved sort logic using `es-toolkit`'s `sortBy`
- Follows project guidelines (CLAUDE.md)

## Technical Details

### History V2 API's create_time
- Cloud backend provides `create_time` (in milliseconds) through History
V2 API
- Enables accurate sorting by creation time
- For input assets, uses existing `created_at` (ISO string)

### Sort Implementation
Uses `es-toolkit`'s `sortBy` in `useMediaAssetFiltering` composable:

```typescript
// Get timestamp from asset (either create_time or created_at)
const getAssetTime = (asset: AssetItem): number => {
  return (
    (asset.user_metadata?.create_time as number) ??
    (asset.created_at ? new Date(asset.created_at).getTime() : 0)
  )
}

// Sort by time
if (sortBy.value === 'oldest') {
  return sortByUtil(searchFiltered.value, [getAssetTime])
} else {
  return sortByUtil(searchFiltered.value, [(asset) => -getAssetTime(asset)])
}
```

## Testing
-  Typecheck passed
-  Lint passed
-  Format passed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6695-feat-Add-sort-functionality-to-Media-Asset-Panel-2ab6d73d3650818c818ff3559875d869)
by [Unito](https://www.unito.io)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jin Yi
2025-11-14 13:56:04 +09:00
committed by GitHub
parent 8b8f3538bf
commit f80fc4cf9a
11 changed files with 243 additions and 23 deletions

View File

@@ -2,7 +2,7 @@
<AssetsSidebarTemplate>
<template #top>
<span v-if="!isInFolderView" class="font-bold">
{{ $t('sideToolbar.mediaAssets') }}
{{ $t('sideToolbar.mediaAssets.title') }}
</span>
<div v-else class="flex w-full items-center justify-between gap-2">
<div class="flex items-center gap-2">
@@ -39,14 +39,11 @@
<Tab value="input">{{ $t('sideToolbar.labels.imported') }}</Tab>
<Tab value="output">{{ $t('sideToolbar.labels.generated') }}</Tab>
</TabList>
<!-- Search Bar -->
<div class="pt-2">
<SearchBox
v-model="searchQuery"
:placeholder="$t('sideToolbar.searchAssets')"
size="lg"
/>
</div>
<!-- Filter Bar -->
<MediaAssetFilterBar
v-model:search-query="searchQuery"
v-model:sort-by="sortBy"
/>
</template>
<template #body>
<!-- Loading state -->
@@ -165,12 +162,12 @@ import IconTextButton from '@/components/button/IconTextButton.vue'
import TextButton from '@/components/button/TextButton.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import SearchBox from '@/components/input/SearchBox.vue'
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
import Tab from '@/components/tab/Tab.vue'
import TabList from '@/components/tab/TabList.vue'
import { t } from '@/i18n'
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
import MediaAssetFilterBar from '@/platform/assets/components/MediaAssetFilterBar.vue'
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
import { useAssetSelection } from '@/platform/assets/composables/useAssetSelection'
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
@@ -247,7 +244,8 @@ const baseAssets = computed(() => {
})
// Use media asset filtering composable
const { searchQuery, filteredAssets } = useMediaAssetFiltering(baseAssets)
const { searchQuery, sortBy, filteredAssets } =
useMediaAssetFiltering(baseAssets)
const displayAssets = computed(() => {
return filteredAssets.value