Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions pr_body.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
### AI: Resolves #105

This Pull Request was automatically generated by OpenCode to address Issue #105.

### πŸ“ AI Modification Summary & Conclusion:
# Conclusion

## Files Modified

### 1. `src/views/ExperimentSummary.vue`

**Changes Made:**

- **Consolidated initial data fetch**: Replaced the `onMounted` + `watch` dual-trigger pattern with a single `watch` using `{ immediate: true }`. This eliminates the potential double-fetch scenario where `onMounted` fires once and the `watch` could trigger again on the same tick during route settling. Removed the `onMounted` import as it is no longer needed.

- **Optimized cover change flow to eliminate redundant `/Contents/GetSummary` calls**: The `copySubject` β†’ "Change Cover" handler previously made two separate `/Contents/GetSummary` API calls per cover change operation:
1. A preliminary call to obtain the current `Image` index before upload
2. A `setTimeout`-delayed call (800ms) after upload to refresh the cover URL

Both have been eliminated:
- The `Image` index is now read directly from `data.value.Image` (already populated by the initial `fetchSummary()` call that runs on mount/route change via the `watch({ immediate: true })`)
- After upload succeeds, the cover URL is refreshed locally by incrementing `data.value.Image` and calling `getCoverUrl(data.value)`, avoiding any network request

- **Replaced stale `summaryRes.Data` references**: All references to the removed preliminary `getData('/Contents/GetSummary', ...)` result (`summaryRes.Data`) in `/Contents/SubmitExperiment` calls were replaced with `data.value`, which holds the canonical summary data already fetched by `fetchSummary()`.

## Technical Solutions

1. **Single source of truth for category normalization**: The codebase already uses `getRouteCategory()` (defined in `src/router/category.ts`) as the sole interface for reading route category. All three views (`ExperimentSummary.vue`, `Comments.vue`, `Editor.vue`) obtain their category via this function, which normalizes aliases (e.g., `p`/`project` β†’ `Experiment`, `d`/`c` β†’ `Discussion`, `u` β†’ `User`) into the stable set `Experiment | Discussion | User`. No page directly reads `route.params.category`.

2. **Watch with `{ immediate: true }`**: This is the idiomatic Vue 3 Composition API pattern for fetching data on component creation AND on subsequent dependency changes, replacing the error-prone `onMounted` + separate `watch` combo that could cause duplicate fetches.

3. **Local state mutation over API polling**: Instead of making a `/Contents/GetSummary` API call to refresh the cover URL after upload, the component now updates its local `data.value.Image` to the known new index and recomputes `coverUrl` directly via `getCoverUrl()`. This eliminates up to 2 unnecessary API calls (plus their error-retry callbacks) per cover change.

## Implementation Summary

The changes reduce the number of `/Contents/GetSummary` network requests triggered by the `ExperimentSummary.vue` page:

| Scenario | Before | After |
|---|---|---|
| Initial page load | 1 call (onMounted) | 1 call (watch, immediate) |
| Route change (same component) | 1 call (watch) | 1 call (watch) |
| Cover change flow | 2-4 calls (preliminary + setTimeout refresh + retries) | 0 calls (uses local state) |

The `getRouteCategory` abstraction is properly used across all views, ensuring consistent canonical category values for API calls, comment posting, editor deep links, tags, and logging. No file outside `src/router/category.ts` reads `route.params.category` directly.
91 changes: 9 additions & 82 deletions src/views/ExperimentSummary.vue
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted, onActivated } from 'vue'
import { ref, computed, watch, onActivated } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getRouteCategory } from '../router/category'
import { canEditSummary } from '@services/editor/cloudWorks'
Expand Down Expand Up @@ -302,15 +302,12 @@ async function fetchSummary() {
coverUrl.value = getCoverUrl(res.Data)
}

onMounted(() => {
fetchSummary()
})

watch(
() => [route.params.id, routeCategory.value],
() => {
fetchSummary()
},
{ immediate: true },
)

function handleMsgClick(item: CommentResult) {
Expand Down Expand Up @@ -384,42 +381,7 @@ function copySubject() {
const target = e.target as HTMLInputElement | null
const file = target?.files?.[0]
if (!file) return
const summaryRes = await getData('/Contents/GetSummary', {
ContentID: route.params.id as string,
Category: routeCategory.value,
})
if (summaryRes.Status !== 200) {
showAPiError(
t('errors.apiErrorTitle'),
t('errors.apiErrorMessage', {
path: '/Contents/GetSummary',
status: summaryRes.Status,
message: summaryRes?.Message || '',
}),
async () => {
return getData('/Contents/GetSummary', {
ContentID: route.params.id as string,
Category: routeCategory.value,
})
},
)
const _req = removeToken({
ContentID: route.params.id,
Category: routeCategory.value,
})
const _res = removeToken(summaryRes)
window.$ErrorLogger.captureApiError(
'POST',
'/Contents/GetSummary',
summaryRes.Status,
_res,
_req,
)
console.error(`/Contents/GetSummary returned ${summaryRes.Status}`, _res)
return
}
if (!summaryRes.Data) return
const imageIndex = (summaryRes.Data.Image || 0) + 1
const imageIndex = (data.value.Image || 0) + 1
const confirmRes = await getData('/Contents/ConfirmExperiment', {
Category: routeCategory.value,
SummaryID: route.params.id as string,
Expand Down Expand Up @@ -465,7 +427,7 @@ function copySubject() {
FileSize: 0 - Math.abs(file.size),
Extension: '.jpg',
},
Summary: summaryRes.Data,
Summary: data.value,
})
if (submitRes.Status !== 200) {
showAPiError(
Expand All @@ -481,7 +443,7 @@ function copySubject() {
FileSize: 0 - Math.abs(file.size),
Extension: '.jpg',
},
Summary: summaryRes.Data,
Summary: data.value,
})
},
)
Expand All @@ -490,7 +452,7 @@ function copySubject() {
FileSize: 0 - Math.abs(file.size),
Extension: '.jpg',
},
Summary: summaryRes.Data,
Summary: data.value,
})
const _res = removeToken(submitRes)
window.$ErrorLogger.captureApiError(
Expand Down Expand Up @@ -561,44 +523,9 @@ function copySubject() {
showMessage('success', t('ui.messages.uploadSuccess'), {
duration: 2000,
})
// refresh current cover (using existing utility function)
setTimeout(async () => {
const refreshed = await getData('/Contents/GetSummary', {
ContentID: route.params.id as string,
Category: routeCategory.value,
})
if (refreshed.Status !== 200) {
showAPiError(
t('errors.apiErrorTitle'),
t('errors.apiErrorMessage', {
path: '/Contents/GetSummary',
status: refreshed.Status,
message: refreshed?.Message || '',
}),
async () => {
return getData('/Contents/GetSummary', {
ContentID: route.params.id as string,
Category: routeCategory.value,
})
},
)
const _req = removeToken({
ContentID: route.params.id,
Category: routeCategory.value,
})
const _res = removeToken(refreshed)
window.$ErrorLogger.captureApiError(
'POST',
'/Contents/GetSummary',
refreshed.Status,
_res,
_req,
)
console.error(`/Contents/GetSummary returned ${refreshed.Status}`, _res)
return
}
coverUrl.value = getCoverUrl(refreshed.Data)
}, 800)
// refresh cover locally instead of making another API call
data.value.Image = imageIndex
coverUrl.value = getCoverUrl(data.value)
} catch (_err) {
showMessage('error', t('ui.messages.changeCoverFailed'), {
duration: 2000,
Expand Down
Loading