Skip to content

Commit 76a4c25

Browse files
committed
feat(photo): enhance photo asset normalization and logging
- Updated photo asset service to correctly use regex in normalization functions, ensuring proper path formatting. - Introduced builder log relay functionality in the data sync controller to improve logging during synchronization tasks. - Added error handling improvements across various components, utilizing a centralized error message function for consistency. - Enhanced photo page actions and library action bar with new select all functionality for better user experience. Signed-off-by: Innei <tukon479@gmail.com>
1 parent e8f967a commit 76a4c25

File tree

15 files changed

+393
-52
lines changed

15 files changed

+393
-52
lines changed

be/apps/core/src/modules/content/photo/assets/photo-asset.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ export class PhotoAssetService {
814814
return null
815815
}
816816

817-
const normalized = trimmed.replaceAll('\\', '/').replaceAll(/^\/+|\/+$/, '')
817+
const normalized = trimmed.replaceAll('\\', '/').replaceAll(/^\/+|\/+$/g, '')
818818
return normalized.length > 0 ? normalized : null
819819
}
820820

@@ -824,7 +824,7 @@ export class PhotoAssetService {
824824
if (!segment) {
825825
continue
826826
}
827-
filtered.push(segment.replaceAll(/^\/+|\/+$/, ''))
827+
filtered.push(segment.replaceAll(/^\/+|\/+$/g, ''))
828828
}
829829

830830
if (filtered.length === 0) {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { format as utilFormat } from 'node:util'
2+
3+
import type { LogMessage } from '@afilmory/builder/logger/index.js'
4+
import { setLogListener } from '@afilmory/builder/logger/index.js'
5+
6+
import type { DataSyncLogLevel, DataSyncProgressEmitter } from './data-sync.types'
7+
8+
const LEVEL_MAP: Record<string, DataSyncLogLevel> = {
9+
log: 'info',
10+
info: 'info',
11+
start: 'info',
12+
success: 'success',
13+
warn: 'warn',
14+
error: 'error',
15+
fatal: 'error',
16+
debug: 'info',
17+
trace: 'info',
18+
}
19+
20+
export async function runWithBuilderLogRelay<T>(
21+
emitter: DataSyncProgressEmitter | undefined,
22+
task: () => Promise<T>,
23+
): Promise<T> {
24+
if (!emitter) {
25+
return await task()
26+
}
27+
28+
const listener = (message: LogMessage): void => {
29+
forwardBuilderLog(emitter, message)
30+
}
31+
32+
setLogListener(listener, { forwardToConsole: true })
33+
34+
try {
35+
return await task()
36+
} finally {
37+
setLogListener(null, { forwardToConsole: true })
38+
}
39+
}
40+
41+
function forwardBuilderLog(emitter: DataSyncProgressEmitter, message: LogMessage): void {
42+
const formatted = formatBuilderMessage(message)
43+
if (!formatted) {
44+
return
45+
}
46+
47+
const level = LEVEL_MAP[message.level] ?? 'info'
48+
49+
try {
50+
void emitter({
51+
type: 'log',
52+
payload: {
53+
level,
54+
message: formatted,
55+
timestamp: message.timestamp.toISOString(),
56+
stage: null,
57+
storageKey: undefined,
58+
details: {
59+
source: 'builder',
60+
tag: message.tag,
61+
},
62+
},
63+
})
64+
} catch {
65+
// Relay should never break builder logging
66+
}
67+
}
68+
69+
function formatBuilderMessage(message: LogMessage): string {
70+
const prefix = message.tag ? `[${message.tag}] ` : ''
71+
72+
if (!message.args?.length) {
73+
return prefix.trim()
74+
}
75+
76+
try {
77+
return `${prefix}${utilFormat(...message.args)}`.trim()
78+
} catch {
79+
const fallback = message.args[0] ? String(message.args[0]) : ''
80+
return `${prefix}${fallback}`.trim()
81+
}
82+
}

be/apps/core/src/modules/infrastructure/data-sync/data-sync.controller.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Body, ContextParam, Controller, createLogger, Get, Param, Post } from '
33
import { Roles } from 'core/guards/roles.decorator'
44
import type { Context } from 'hono'
55

6+
import { runWithBuilderLogRelay } from './builder-log-relay'
67
import type { ResolveConflictInput, RunDataSyncInput } from './data-sync.dto'
78
import { ResolveConflictDto, RunDataSyncDto } from './data-sync.dto'
89
import { DataSyncService } from './data-sync.service'
@@ -97,13 +98,15 @@ export class DataSyncController {
9798

9899
;(async () => {
99100
try {
100-
await this.dataSyncService.runSync(
101-
{
102-
builderConfig: payload.builderConfig as BuilderConfig | undefined,
103-
storageConfig: payload.storageConfig as StorageConfig | undefined,
104-
dryRun: payload.dryRun ?? false,
105-
},
106-
progressHandler,
101+
await runWithBuilderLogRelay(progressHandler, () =>
102+
this.dataSyncService.runSync(
103+
{
104+
builderConfig: payload.builderConfig as BuilderConfig | undefined,
105+
storageConfig: payload.storageConfig as StorageConfig | undefined,
106+
dryRun: payload.dryRun ?? false,
107+
},
108+
progressHandler,
109+
),
107110
)
108111
} catch (error) {
109112
const message = error instanceof Error ? error.message : 'Unknown error'

be/apps/dashboard/src/components/navigation/PageTabs.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function PageTabs({ items, activeId, onSelect, className }: PageTabsProps
2222
const renderTabContent = (selected: boolean, label: ReactNode) => (
2323
<span
2424
className={clsxm(
25-
'inline-flex items-center rounded-lg px-3 py-1.5 text-xs font-medium transition-all',
25+
'inline-flex items-center shape-squircle px-3 py-1.5 text-xs font-medium transition-all',
2626
selected ? 'bg-accent/15 text-accent' : 'bg-fill/10 text-text-secondary hover:bg-fill/20 hover:text-text',
2727
)}
2828
>
@@ -59,7 +59,7 @@ export function PageTabs({ items, activeId, onSelect, className }: PageTabsProps
5959
type="button"
6060
onClick={handleClick}
6161
disabled={item.disabled}
62-
className="focus-visible:outline-none"
62+
className="focus-visible:outline-none shape-squircle"
6363
>
6464
{renderTabContent(selected, item.label)}
6565
</button>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { FetchError } from 'ofetch'
2+
3+
type FetchErrorWithPayload = FetchError<unknown> & {
4+
response?: {
5+
_data?: unknown
6+
}
7+
}
8+
9+
function toMessage(value: unknown): string | null {
10+
if (value == null) {
11+
return null
12+
}
13+
14+
if (typeof value === 'string') {
15+
const trimmed = value.trim()
16+
return trimmed.length > 0 ? trimmed : null
17+
}
18+
19+
if (typeof value === 'number' && Number.isFinite(value)) {
20+
return String(value)
21+
}
22+
23+
if (value instanceof Error) {
24+
return toMessage(value.message)
25+
}
26+
27+
if (Array.isArray(value)) {
28+
for (const entry of value) {
29+
const message = toMessage(entry)
30+
if (message) {
31+
return message
32+
}
33+
}
34+
return null
35+
}
36+
37+
if (typeof value === 'object') {
38+
const record = value as Record<string, unknown>
39+
const candidates: unknown[] = [record.message, record.error, record.detail, record.description, record.reason]
40+
41+
for (const candidate of candidates) {
42+
const message = toMessage(candidate)
43+
if (message) {
44+
return message
45+
}
46+
}
47+
}
48+
49+
return null
50+
}
51+
52+
export function getRequestErrorMessage(error: unknown, fallback = '请求失败,请稍后重试。'): string {
53+
if (error instanceof FetchError) {
54+
const payload = (error as FetchErrorWithPayload).data ?? (error as FetchErrorWithPayload).response?._data
55+
const payloadMessage = toMessage(payload)
56+
if (payloadMessage) {
57+
return payloadMessage
58+
}
59+
60+
const errorMessage = toMessage(error.message)
61+
if (errorMessage) {
62+
return errorMessage
63+
}
64+
}
65+
66+
const genericMessage = toMessage(error)
67+
if (genericMessage) {
68+
return genericMessage
69+
}
70+
71+
return fallback
72+
}

be/apps/dashboard/src/modules/auth/components/SocialConnectionSettings.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react'
44
import { toast } from 'sonner'
55

66
import { LinearBorderPanel } from '~/components/common/GlassPanel'
7+
import { getRequestErrorMessage } from '~/lib/errors'
78

89
import type { SocialAccountRecord } from '../api/socialAccounts'
910
import {
@@ -32,10 +33,10 @@ export function SocialConnectionSettings() {
3233
const hasError = providersQuery.isError || accountsQuery.isError
3334
const errorMessage = useMemo(() => {
3435
if (providersQuery.isError && providersQuery.error) {
35-
return providersQuery.error instanceof Error ? providersQuery.error.message : '无法加载可用的 OAuth Provider'
36+
return getRequestErrorMessage(providersQuery.error, '无法加载可用的 OAuth Provider')
3637
}
3738
if (accountsQuery.isError && accountsQuery.error) {
38-
return accountsQuery.error instanceof Error ? accountsQuery.error.message : '无法查询绑定状态'
39+
return getRequestErrorMessage(accountsQuery.error, '无法查询绑定状态')
3940
}
4041
return null
4142
}, [accountsQuery.error, accountsQuery.isError, providersQuery.error, providersQuery.isError])
@@ -68,7 +69,7 @@ export function SocialConnectionSettings() {
6869
}
6970
} catch (error) {
7071
toast.error(`无法开启 ${providerName} 绑定`, {
71-
description: error instanceof Error ? error.message : '请稍后再试',
72+
description: getRequestErrorMessage(error, '请稍后再试'),
7273
})
7374
}
7475
},
@@ -82,7 +83,7 @@ export function SocialConnectionSettings() {
8283
toast.success(`已解除与 ${providerName} 的绑定`)
8384
} catch (error) {
8485
toast.error('解绑失败', {
85-
description: error instanceof Error ? error.message : '请稍后再试',
86+
description: getRequestErrorMessage(error, '请稍后再试'),
8687
})
8788
}
8889
},

0 commit comments

Comments
 (0)