diff --git a/AGENTS.md b/AGENTS.md index e22c595..371090a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -211,6 +211,13 @@ records.each { - In TypeScript and TSX, block bodies for components, functions, callbacks, `if`, `try`, `catch`, `finally`, loops, and JSX nesting use 2 spaces per level. +- In TypeScript and TSX, put the opening brace of `try`, `catch`, and + `finally` blocks on the next line at the same indentation as the keyword. +- Do not indent the opening `{` one level deeper than `try`, `catch`, or + `finally`. +- Indent the block body 2 spaces deeper than the keyword and opening brace. +- Put the closing `}` on its own line at the same indentation as the keyword. +- Do not write `try {`, `catch {`, or `finally {`. - In TypeScript and TSX, use 4-space continuation indentation for wrapped expressions, arguments, conditions, arrays, object literals, JSX attributes, and similar continuations. @@ -578,6 +585,13 @@ and layout reuse, follow `frontend/AGENTS.md`. declarations, unless imports, exports, or file boundaries make that awkward. - In TSX, use 2-space block indentation and 4-space continuation indentation. +- In TypeScript and TSX, put the opening brace of `try`, `catch`, and + `finally` blocks on the next line at the same indentation as the keyword. +- Do not indent the opening `{` one level deeper than `try`, `catch`, or + `finally`. +- Indent the block body 2 spaces deeper than the keyword and opening brace. +- Put the closing `}` on its own line at the same indentation as the keyword. +- Do not write `try {`, `catch {`, or `finally {`. - In TypeScript and TSX, convert every complete leading run of 8 spaces to a tab character. - A leading tab is exactly equivalent to 8 leading spaces. @@ -628,6 +642,22 @@ and layout reuse, follow `frontend/AGENTS.md`. single physical line. - Always add braces around `if`, `else`, or `for` bodies when the body spans two or more physical lines, even if it is one statement. +- `try` / `catch` / `finally` brace placement example: + +```ts +try +{ + doWork () +} +catch +{ + recover () +} +finally +{ + cleanUp () +} +``` - Do not use a leading semicolon for expression statements such as `;([...]).forEach(...)`; rewrite the expression to avoid ASI hazards explicitly, for example with `void`. diff --git a/backend/app/services/preview/http_fetcher.rb b/backend/app/services/preview/http_fetcher.rb index 455cb25..ce4b701 100644 --- a/backend/app/services/preview/http_fetcher.rb +++ b/backend/app/services/preview/http_fetcher.rb @@ -12,9 +12,7 @@ module Preview Response = Data.define(:body, :content_type, :url) - def self.fetch(raw_url, - max_bytes: DEFAULT_MAX_BYTES, - redirects: MAX_REDIRECTS) + def self.fetch(raw_url, max_bytes: DEFAULT_MAX_BYTES, redirects: MAX_REDIRECTS) uri, addresses = UrlSafety.validate(raw_url) response = request(uri, addresses.first, max_bytes) @@ -69,9 +67,7 @@ module Preview log_failure(:timeout, url: uri&.to_s || raw_url, error: e.class.name, message: e.message) raise FetchTimeout, e.message rescue SocketError, SystemCallError, OpenSSL::SSL::SSLError, EOFError => e - log_failure(:network_error, - url: uri&.to_s || raw_url, - error: e.class.name, + log_failure(:network_error, url: uri&.to_s || raw_url, error: e.class.name, message: e.message) raise FetchFailed, e.message end diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 3468571..ba29a77 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -153,6 +153,13 @@ pass or the remaining failure is clearly blocked. closing `)` forms when nearby code uses them. - Block bodies for components, functions, callbacks, `if`, `try`, `catch`, `finally`, loops, and JSX nesting use 2 spaces per level. +- Put the opening brace of `try`, `catch`, and `finally` blocks on the next + line at the same indentation as the keyword. +- Do not indent the opening `{` one level deeper than `try`, `catch`, or + `finally`. +- Indent the block body 2 spaces deeper than the keyword and opening brace. +- Put the closing `}` on its own line at the same indentation as the keyword. +- Do not write `try {`, `catch {`, or `finally {`. - Wrapped expressions, arguments, ternary branches, method chains, and object pairs use 4-space continuation indentation relative to the owning expression. Do not confuse this with 2-space block indentation. @@ -229,6 +236,23 @@ const Component = () => { } ``` +- `try` / `catch` / `finally` brace placement example: + +```ts +try +{ + doWork () +} +catch +{ + recover () +} +finally +{ + cleanUp () +} +``` + - Continuation indentation example: ```ts diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index db5adcd..3fcdbf5 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -19,7 +19,7 @@ import type { FC, MouseEvent } from 'react' import type { Material, Menu, MenuVisibleItem, Tag, User } from '@/types' -type Props = { user: User | null, } +type Props = { user: User | null } export const menuOutline = ( diff --git a/frontend/src/components/dialogues/DialogueProvider.tsx b/frontend/src/components/dialogues/DialogueProvider.tsx index a677b0b..41c887f 100644 --- a/frontend/src/components/dialogues/DialogueProvider.tsx +++ b/frontend/src/components/dialogues/DialogueProvider.tsx @@ -140,15 +140,15 @@ const DialogueProvider: FC = ({ children }) => { setPendingIds (current => [...current, id]) try - { - const shouldClose = await action.onSelect () - if (shouldClose !== false) - closeRequest (id) - } + { + const shouldClose = await action.onSelect () + if (shouldClose !== false) + closeRequest (id) + } finally - { - setPendingIds (current => current.filter (_1 => _1 !== id)) - } + { + setPendingIds (current => current.filter (_1 => _1 !== id)) + } }, [closeRequest, pendingIds]) diff --git a/frontend/src/components/posts/import/PostImportRowForm.tsx b/frontend/src/components/posts/import/PostImportRowForm.tsx index 15448b2..187ac01 100644 --- a/frontend/src/components/posts/import/PostImportRowForm.tsx +++ b/frontend/src/components/posts/import/PostImportRowForm.tsx @@ -119,19 +119,19 @@ const PostImportRowForm: FC = ( const save = useCallback (async (): Promise => { setSaving (true) try - { - const result = await onSave ({ draft, resetRequested }) - if (result.saved) - return true + { + const result = await onSave ({ draft, resetRequested }) + if (result.saved) + return true - if (result.row != null) - setMessageRow (result.row) - return false - } + if (result.row != null) + setMessageRow (result.row) + return false + } finally - { - setSaving (false) - } + { + setSaving (false) + } }, [draft, onSave, resetRequested]) useEffect (() => { diff --git a/frontend/src/components/posts/import/PostImportRowSummary.tsx b/frontend/src/components/posts/import/PostImportRowSummary.tsx index 481be17..3a26766 100644 --- a/frontend/src/components/posts/import/PostImportRowSummary.tsx +++ b/frontend/src/components/posts/import/PostImportRowSummary.tsx @@ -25,88 +25,88 @@ const summaryDate = (row: PostImportRow): string => const PostImportRowSummary: FC = ({ row, onEdit }) => { - const warning = summaryWarning (row) - const displayStatus = displayPostImportStatus (row) + const warning = summaryWarning (row) + const displayStatus = displayPostImportStatus (row) - return ( - <> -
-
-
#{row.sourceRow}
-
- -
-
- {String (row.attributes.title ?? '') || '(タイトル未取得)'} -
-
- {row.url} -
-
- {String (row.attributes.tags ?? '') || 'タグなし'} -
-
- {summaryDate (row) || '日時未取得'} - {row.attributes.duration ? ` / ${ row.attributes.duration }` : ''} -
- {warning && ( -
- {warning} -
)} -
-
- {displayStatus != null && } -
-
- -
-
+ return ( + <> +
+
+
#{row.sourceRow}
+
+ +
+
+ {String (row.attributes.title ?? '') || '(タイトル未取得)'} +
+
+ {row.url} +
+
+ {String (row.attributes.tags ?? '') || 'タグなし'} +
+
+ {summaryDate (row) || '日時未取得'} + {row.attributes.duration ? ` / ${ row.attributes.duration }` : ''} +
+ {warning && ( +
+ {warning} +
)} +
+
+ {displayStatus != null && } +
+
+ +
+
-
-
- -
-
- {String (row.attributes.title ?? '') || '(タイトル未取得)'} -
-
- {row.url} -
-
- {displayStatus != null && } -
- {warning && ( -
- {warning} -
)} -
-
- -
- ) +
+
+ +
+
+ {String (row.attributes.title ?? '') || '(タイトル未取得)'} +
+
+ {row.url} +
+
+ {displayStatus != null && } +
+ {warning && ( +
+ {warning} +
)} +
+
+ +
+ ) } export default PostImportRowSummary diff --git a/frontend/src/components/posts/import/ThumbnailPreview.tsx b/frontend/src/components/posts/import/ThumbnailPreview.tsx index 5788a6e..0c79ac0 100644 --- a/frontend/src/components/posts/import/ThumbnailPreview.tsx +++ b/frontend/src/components/posts/import/ThumbnailPreview.tsx @@ -10,40 +10,40 @@ type Props = { const ThumbnailPreview: FC = ({ url, alt = 'サムネール', className = 'h-16 w-16' }) => { - const [failed, setFailed] = useState (false) + const [failed, setFailed] = useState (false) - useEffect (() => { - setFailed (false) - }, [url]) + useEffect (() => { + setFailed (false) + }, [url]) - if (!(url)) - { - return ( -
- なし -
) - } + if (!(url)) + { + return ( +
+ なし +
) + } - if (failed) - { - return ( -
- サムネールを表示できません -
) - } + if (failed) + { + return ( +
+ サムネールを表示できません +
) + } - return ( - {alt} setFailed (true)}/>) + return ( + {alt} setFailed (true)}/>) } export default ThumbnailPreview diff --git a/frontend/src/lib/postImportRows.ts b/frontend/src/lib/postImportRows.ts index 2e25853..a201077 100644 --- a/frontend/src/lib/postImportRows.ts +++ b/frontend/src/lib/postImportRows.ts @@ -22,15 +22,15 @@ const buildResetSnapshot = (row: PostImportRow) => ({ export const processableImportRows = (rows: PostImportRow[]): PostImportRow[] => rows.filter (row => { - if (row.importStatus === 'created') - return false - if (row.importStatus === 'skipped') - return false - if (row.importStatus === 'failed') - return false - if (hasValidationErrors (row)) - return false - return row.importStatus == null || row.importStatus === 'pending' + if (row.importStatus === 'created') + return false + if (row.importStatus === 'skipped') + return false + if (row.importStatus === 'failed') + return false + if (hasValidationErrors (row)) + return false + return row.importStatus == null || row.importStatus === 'pending' }) export const creatableImportRows = (rows: PostImportRow[]): PostImportRow[] => @@ -40,31 +40,31 @@ export const creatableImportRows = (rows: PostImportRow[]): PostImportRow[] => export const reviewSummaryCounts = (rows: PostImportRow[]) => ({ total: rows.length, submittable: rows.filter (row => - creatableImportRows ([row]).length > 0 - && !(hasValidationErrors (row))).length, + creatableImportRows ([row]).length > 0 + && !(hasValidationErrors (row))).length, skipPlanned: rows.filter (row => - processableImportRows ([row]).length > 0 - && hasSkipReason (row)).length }) + processableImportRows ([row]).length > 0 + && hasSkipReason (row)).length }) export const resultSummaryCounts = (rows: PostImportRow[]) => rows.reduce ( - (counts, row) => { - if (row.importStatus === 'created') - { - ++counts.created - return counts - } - if (row.importStatus === 'skipped') - { - ++counts.skipped - return counts - } - if (row.importStatus === 'failed') - ++counts.failed - return counts - }, - { created: 0, skipped: 0, failed: 0 }) + (counts, row) => { + if (row.importStatus === 'created') + { + ++counts.created + return counts + } + if (row.importStatus === 'skipped') + { + ++counts.skipped + return counts + } + if (row.importStatus === 'failed') + ++counts.failed + return counts + }, + { created: 0, skipped: 0, failed: 0 }) export const mergeValidatedImportRows = ( @@ -73,46 +73,46 @@ export const mergeValidatedImportRows = ( ): PostImportRow[] => { const validatedMap = new Map (validated.map (row => [row.sourceRow, row])) return current.map (previous => { - if ( - previous.importStatus === 'created' - || previous.importStatus === 'skipped' - || previous.importStatus === 'failed') - return previous + if ( + previous.importStatus === 'created' + || previous.importStatus === 'skipped' + || previous.importStatus === 'failed') + return previous - const row = validatedMap.get (previous.sourceRow) - if (row == null) - return previous + const row = validatedMap.get (previous.sourceRow) + if (row == null) + return previous - const fieldWarnings = - Object.keys (row.fieldWarnings).length > 0 || row.metadataUrl !== previous.metadataUrl - ? { ...row.fieldWarnings } - : { ...previous.fieldWarnings } - for (const [field, origin] of Object.entries (row.provenance)) - { - if (origin === 'manual') - delete fieldWarnings[field] - } + const fieldWarnings = + Object.keys (row.fieldWarnings).length > 0 || row.metadataUrl !== previous.metadataUrl + ? { ...row.fieldWarnings } + : { ...previous.fieldWarnings } + for (const [field, origin] of Object.entries (row.provenance)) + { + if (origin === 'manual') + delete fieldWarnings[field] + } - return { - ...previous, - url: row.url, - attributes: row.attributes, - provenance: row.provenance, - tagSources: row.tagSources, - skipReason: row.skipReason, - existingPostId: row.existingPostId, - fieldWarnings, - baseWarnings: - row.baseWarnings.length > 0 || row.metadataUrl !== previous.metadataUrl - ? row.baseWarnings - : previous.baseWarnings, - validationErrors: row.validationErrors, - status: row.status, - metadataUrl: row.metadataUrl, - resetSnapshot: - row.metadataUrl !== previous.metadataUrl - ? buildResetSnapshot (row) - : previous.resetSnapshot } + return { + ...previous, + url: row.url, + attributes: row.attributes, + provenance: row.provenance, + tagSources: row.tagSources, + skipReason: row.skipReason, + existingPostId: row.existingPostId, + fieldWarnings, + baseWarnings: + row.baseWarnings.length > 0 || row.metadataUrl !== previous.metadataUrl + ? row.baseWarnings + : previous.baseWarnings, + validationErrors: row.validationErrors, + status: row.status, + metadataUrl: row.metadataUrl, + resetSnapshot: + row.metadataUrl !== previous.metadataUrl + ? buildResetSnapshot (row) + : previous.resetSnapshot } }) } @@ -123,37 +123,37 @@ export const mergeImportResults = ( ): PostImportRow[] => { const resultMap = new Map (results.map (row => [row.sourceRow, row])) return rows.map (row => { - const result = resultMap.get (row.sourceRow) - if (result == null) - return row + const result = resultMap.get (row.sourceRow) + if (result == null) + return row - switch (result.status) - { - case 'created': - return { - ...row, - importStatus: 'created', - skipReason: undefined, - createdPostId: result.post.id, - existingPostId: undefined, - importErrors: result.errors } - case 'skipped': - return { - ...row, - importStatus: 'skipped', - skipReason: 'existing', - createdPostId: undefined, - existingPostId: result.existingPostId, - importErrors: result.errors } - case 'failed': - return { - ...row, - importStatus: 'failed', - skipReason: undefined, - createdPostId: undefined, - existingPostId: undefined, - importErrors: result.errors } - } + switch (result.status) + { + case 'created': + return { + ...row, + importStatus: 'created', + skipReason: undefined, + createdPostId: result.post.id, + existingPostId: undefined, + importErrors: result.errors } + case 'skipped': + return { + ...row, + importStatus: 'skipped', + skipReason: 'existing', + createdPostId: undefined, + existingPostId: result.existingPostId, + importErrors: result.errors } + case 'failed': + return { + ...row, + importStatus: 'failed', + skipReason: undefined, + createdPostId: undefined, + existingPostId: undefined, + importErrors: result.errors } + } }) } @@ -163,11 +163,11 @@ export const retryImportRow = ( sourceRow: number, ): PostImportRow[] => rows.map (row => - row.sourceRow === sourceRow && row.importStatus === 'failed' - ? { ...row, importStatus: 'pending', importErrors: undefined } - : row) + row.sourceRow === sourceRow && row.importStatus === 'failed' + ? { ...row, importStatus: 'pending', importErrors: undefined } + : row) export const initialisePreviewRows = (rows: PostImportRow[]): PostImportRow[] => rows.map (row => ({ - ...row, - resetSnapshot: buildResetSnapshot (row) })) + ...row, + resetSnapshot: buildResetSnapshot (row) })) diff --git a/frontend/src/lib/postImportSourceValidation.ts b/frontend/src/lib/postImportSourceValidation.ts index 63cb796..1658216 100644 --- a/frontend/src/lib/postImportSourceValidation.ts +++ b/frontend/src/lib/postImportSourceValidation.ts @@ -19,30 +19,30 @@ const normaliseImportUrl = (value: string): string | null => { try { - const url = new URL (trimmed) - if (!(url.protocol === 'http:' || url.protocol === 'https:')) - return null - if (!(url.host)) - return null + const url = new URL (trimmed) + if (!(url.protocol === 'http:' || url.protocol === 'https:')) + return null + if (!(url.host)) + return null - url.hostname = url.hostname.toLowerCase () - if (url.pathname.endsWith ('/')) - url.pathname = url.pathname.replace (/\/+$/, '') - return url.toString () + url.hostname = url.hostname.toLowerCase () + if (url.pathname.endsWith ('/')) + url.pathname = url.pathname.replace (/\/+$/, '') + return url.toString () } catch { - return null + return null } } export const countImportSourceLines = (source: string): number => source - .split (/\r\n|\n|\r/) - .map (_1 => _1.trim ()) - .filter (_1 => _1 !== '') - .length + .split (/\r\n|\n|\r/) + .map (_1 => _1.trim ()) + .filter (_1 => _1 !== '') + .length export const validateImportSource = ( @@ -54,56 +54,56 @@ export const validateImportSource = ( let count = 0 lines.forEach ((rawLine, index) => { - const value = rawLine.trim () - if (!(value)) - return + const value = rawLine.trim () + if (!(value)) + return - ++count - const sourceRow = index + 1 - const displayUrl = truncateUrl (value) - const normalised = normaliseImportUrl (value) - if (count > MAX_ROWS) - { - issues.push ({ - sourceRow, - message: `取込件数は ${ MAX_ROWS } 件までです.`, - url: displayUrl }) - return - } - if (!(value.startsWith ('http://') || value.startsWith ('https://'))) - { - issues.push ({ - sourceRow, - message: 'HTTP または HTTPS の URL ではありません.', - url: displayUrl }) - return - } - if (bytesize (value) > MAX_URL_BYTES) - { - issues.push ({ - sourceRow, - message: 'URL が長すぎます.', - url: displayUrl }) - return - } - if (normalised == null) - { - issues.push ({ - sourceRow, - message: 'URL の形式が不正です.', - url: displayUrl }) - return - } - const duplicateRow = seen.get (normalised) - if (duplicateRow != null) - { - issues.push ({ - sourceRow, - message: `${ duplicateRow } 行目と同じ URL です.`, - url: displayUrl }) - return - } - seen.set (normalised, sourceRow) + ++count + const sourceRow = index + 1 + const displayUrl = truncateUrl (value) + const normalised = normaliseImportUrl (value) + if (count > MAX_ROWS) + { + issues.push ({ + sourceRow, + message: `取込件数は ${ MAX_ROWS } 件までです.`, + url: displayUrl }) + return + } + if (!(value.startsWith ('http://') || value.startsWith ('https://'))) + { + issues.push ({ + sourceRow, + message: 'HTTP または HTTPS の URL ではありません.', + url: displayUrl }) + return + } + if (bytesize (value) > MAX_URL_BYTES) + { + issues.push ({ + sourceRow, + message: 'URL が長すぎます.', + url: displayUrl }) + return + } + if (normalised == null) + { + issues.push ({ + sourceRow, + message: 'URL の形式が不正です.', + url: displayUrl }) + return + } + const duplicateRow = seen.get (normalised) + if (duplicateRow != null) + { + issues.push ({ + sourceRow, + message: `${ duplicateRow } 行目と同じ URL です.`, + url: displayUrl }) + return + } + seen.set (normalised, sourceRow) }) return issues diff --git a/frontend/src/lib/postImportStorage.ts b/frontend/src/lib/postImportStorage.ts index 360963a..738c9a2 100644 --- a/frontend/src/lib/postImportStorage.ts +++ b/frontend/src/lib/postImportStorage.ts @@ -40,12 +40,12 @@ const readStorage = ( try { - return sessionStorage.getItem (key) + return sessionStorage.getItem (key) } catch { - onError?.('保存済みデータを読み込めませんでした.') - return null + onError?.('保存済みデータを読み込めませんでした.') + return null } } @@ -60,13 +60,13 @@ const writeStorage = ( try { - sessionStorage.setItem (key, value) - return true + sessionStorage.setItem (key, value) + return true } catch { - onError?.('ブラウザへ保存できませんでした.') - return false + onError?.('ブラウザへ保存できませんでした.') + return false } } @@ -80,11 +80,11 @@ const removeStorage = ( try { - sessionStorage.removeItem (key) + sessionStorage.removeItem (key) } catch { - onError?.('保存済みデータを削除できませんでした.') + onError?.('保存済みデータを削除できませんでした.') } } @@ -96,9 +96,9 @@ const ensureStringListRecord = (value: unknown): Record | null const result: Record = { } for (const [key, entry] of Object.entries (value)) { - if (!(Array.isArray (entry)) || !(entry.every (_1 => typeof _1 === 'string'))) - return null - result[key] = entry + if (!(Array.isArray (entry)) || !(entry.every (_1 => typeof _1 === 'string'))) + return null + result[key] = entry } return result } @@ -149,7 +149,7 @@ const sanitiseResetSnapshot = (value: unknown): PostImportResetSnapshot | null = if (!(hasOnlyKeys (value.attributes, ATTRIBUTE_KEYS))) return null if (!(Object.values (value.attributes).every (entry => - typeof entry === 'string' || typeof entry === 'number'))) + typeof entry === 'string' || typeof entry === 'number'))) return null if (!(isPlainObject (value.provenance))) return null @@ -169,19 +169,19 @@ const sanitiseResetSnapshot = (value: unknown): PostImportResetSnapshot | null = if (!(hasOnlyKeys (fieldWarnings, WARNING_KEYS))) return null if (!(Array.isArray (value.baseWarnings)) - || !(value.baseWarnings.every (_1 => typeof _1 === 'string'))) + || !(value.baseWarnings.every (_1 => typeof _1 === 'string'))) return null if (value.metadataUrl != null && typeof value.metadataUrl !== 'string') return null return { - url: value.url, - attributes: value.attributes as Record, - provenance: value.provenance as Record, - tagSources: value.tagSources as Record, - fieldWarnings, - baseWarnings: value.baseWarnings, - metadataUrl: value.metadataUrl as string | undefined } + url: value.url, + attributes: value.attributes as Record, + provenance: value.provenance as Record, + tagSources: value.tagSources as Record, + fieldWarnings, + baseWarnings: value.baseWarnings, + metadataUrl: value.metadataUrl as string | undefined } } @@ -220,20 +220,20 @@ const sanitiseRow = (value: unknown): PostImportRow | null => { return null const importErrors = - value.importErrors == null - ? undefined - : ensureStringListRecord (value.importErrors) + value.importErrors == null + ? undefined + : ensureStringListRecord (value.importErrors) if (importErrors === null) return null if (!(Array.isArray (value.baseWarnings)) - || !(value.baseWarnings.every (_1 => typeof _1 === 'string'))) + || !(value.baseWarnings.every (_1 => typeof _1 === 'string'))) return null const provenanceEntries = Object.entries (value.provenance) if (!(hasOnlyKeys (value.attributes, ATTRIBUTE_KEYS))) return null if (!(Object.values (value.attributes).every (entry => - typeof entry === 'string' || typeof entry === 'number'))) + typeof entry === 'string' || typeof entry === 'number'))) return null if (!(hasOnlyKeys (value.provenance, PROVENANCE_KEYS))) return null @@ -242,33 +242,33 @@ const sanitiseRow = (value: unknown): PostImportRow | null => { if (value.tagSources != null) { - if (!(isPlainObject (value.tagSources))) - return null - if (!(hasOnlyKeys (value.tagSources, TAG_SOURCE_KEYS))) - return null - if (!(Object.values (value.tagSources).every (_1 => typeof _1 === 'string'))) - return null + if (!(isPlainObject (value.tagSources))) + return null + if (!(hasOnlyKeys (value.tagSources, TAG_SOURCE_KEYS))) + return null + if (!(Object.values (value.tagSources).every (_1 => typeof _1 === 'string'))) + return null } return { - sourceRow: Number (value.sourceRow), - url: value.url, - attributes: value.attributes as Record, - fieldWarnings, - baseWarnings: value.baseWarnings, - validationErrors, - importErrors, - provenance: value.provenance as Record, - tagSources: value.tagSources as Record | undefined, - status: value.status, - skipReason: value.skipReason ?? undefined, - existingPostId: - isPositiveInteger (value.existingPostId) ? Number (value.existingPostId) : undefined, - metadataUrl: typeof value.metadataUrl === 'string' ? value.metadataUrl : undefined, - resetSnapshot, - createdPostId: - isPositiveInteger (value.createdPostId) ? Number (value.createdPostId) : undefined, - importStatus: value.importStatus ?? undefined } + sourceRow: Number (value.sourceRow), + url: value.url, + attributes: value.attributes as Record, + fieldWarnings, + baseWarnings: value.baseWarnings, + validationErrors, + importErrors, + provenance: value.provenance as Record, + tagSources: value.tagSources as Record | undefined, + status: value.status, + skipReason: value.skipReason ?? undefined, + existingPostId: + isPositiveInteger (value.existingPostId) ? Number (value.existingPostId) : undefined, + metadataUrl: typeof value.metadataUrl === 'string' ? value.metadataUrl : undefined, + resetSnapshot, + createdPostId: + isPositiveInteger (value.createdPostId) ? Number (value.createdPostId) : undefined, + importStatus: value.importStatus ?? undefined } } @@ -292,34 +292,34 @@ export const cleanupExpiredPostImportSessions = ( try { - for (let i = 0; i < sessionStorage.length; ++i) - { - const key = sessionStorage.key (i) - if (key == null || !(key.startsWith (SESSION_PREFIX))) - continue + for (let i = 0; i < sessionStorage.length; ++i) + { + const key = sessionStorage.key (i) + if (key == null || !(key.startsWith (SESSION_PREFIX))) + continue - const raw = sessionStorage.getItem (key) - if (raw == null) - continue - try - { - const value = JSON.parse (raw) as { savedAt?: string } - if (typeof value.savedAt !== 'string' || isExpiredSession (value.savedAt)) - { - sessionStorage.removeItem (key) - --i - } - } - catch - { - sessionStorage.removeItem (key) - --i - } - } + const raw = sessionStorage.getItem (key) + if (raw == null) + continue + try + { + const value = JSON.parse (raw) as { savedAt?: string } + if (typeof value.savedAt !== 'string' || isExpiredSession (value.savedAt)) + { + sessionStorage.removeItem (key) + --i + } + } + catch + { + sessionStorage.removeItem (key) + --i + } + } } catch { - onError?.('保存済みデータを整理できませんでした.') + onError?.('保存済みデータを整理できませんでした.') } } @@ -333,12 +333,12 @@ export const loadPostImportSourceDraft = ( try { - const value = JSON.parse (raw) as { source?: string } - return { source: typeof value.source === 'string' ? value.source : '' } + const value = JSON.parse (raw) as { source?: string } + return { source: typeof value.source === 'string' ? value.source : '' } } catch { - return { source: '' } + return { source: '' } } } @@ -363,12 +363,12 @@ export const savePostImportSession = ( onError?: StorageErrorHandler, ): boolean => writeStorage ( - sessionKey (sessionId), - JSON.stringify ({ - ...session, - version: SESSION_VERSION, - savedAt: new Date ().toISOString () }), - onError) + sessionKey (sessionId), + JSON.stringify ({ + ...session, + version: SESSION_VERSION, + savedAt: new Date ().toISOString () }), + onError) export const loadPostImportSession = ( @@ -381,28 +381,28 @@ export const loadPostImportSession = ( try { - const value = JSON.parse (raw) as Partial - if (value.version !== SESSION_VERSION || !(Array.isArray (value.rows))) - return null - if (typeof value.savedAt !== 'string' || isExpiredSession (value.savedAt)) - { - removeStorage (sessionKey (sessionId), onError) - return null - } + const value = JSON.parse (raw) as Partial + if (value.version !== SESSION_VERSION || !(Array.isArray (value.rows))) + return null + if (typeof value.savedAt !== 'string' || isExpiredSession (value.savedAt)) + { + removeStorage (sessionKey (sessionId), onError) + return null + } - const rows = value.rows.map (sanitiseRow) - if (rows.some (_1 => _1 == null)) - return null + const rows = value.rows.map (sanitiseRow) + if (rows.some (_1 => _1 == null)) + return null - return { - version: SESSION_VERSION, - savedAt: value.savedAt, - source: typeof value.source === 'string' ? value.source : '', - rows: rows as PostImportRow[], - repairMode: value.repairMode === 'failed' ? 'failed' : 'all' } + return { + version: SESSION_VERSION, + savedAt: value.savedAt, + source: typeof value.source === 'string' ? value.source : '', + rows: rows as PostImportRow[], + repairMode: value.repairMode === 'failed' ? 'failed' : 'all' } } catch { - return null + return null } } diff --git a/frontend/src/pages/posts/PostImportResultPage.tsx b/frontend/src/pages/posts/PostImportResultPage.tsx index 9045161..da1b484 100644 --- a/frontend/src/pages/posts/PostImportResultPage.tsx +++ b/frontend/src/pages/posts/PostImportResultPage.tsx @@ -72,92 +72,92 @@ const PostImportResultPage: FC = ({ user }) => { setLoadingRow (sourceRow) try - { - const pendingRows = retryImportRow (session.rows, sourceRow) - setSession ({ ...session, rows: pendingRows }) - const validated = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/validate', { - rows: pendingRows - .filter (row => row.importStatus !== 'created') - .map (row => ({ - sourceRow: row.sourceRow, - url: row.url, - attributes: row.attributes, - provenance: row.provenance, - tagSources: row.tagSources, - metadataUrl: row.metadataUrl })), - changed_row: -1 }) - const validatedRows = mergeValidatedImportRows ( - pendingRows, - initialisePreviewRows (validated.rows)) - const nextSession = { - ...session, - rows: validatedRows, - repairMode: 'failed' as const } - setSession (nextSession) - const target = validatedRows.find (_1 => _1.sourceRow === sourceRow) - if (target == null) - return - if (Object.keys (target.validationErrors ?? { }).length > 0) - { - const saved = savePostImportSession (sessionId, nextSession, message => - toast ({ title: '取込状態を保存できませんでした', description: message })) - if (saved) - navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`) - return - } + { + const pendingRows = retryImportRow (session.rows, sourceRow) + setSession ({ ...session, rows: pendingRows }) + const validated = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/validate', { + rows: pendingRows + .filter (row => row.importStatus !== 'created') + .map (row => ({ + sourceRow: row.sourceRow, + url: row.url, + attributes: row.attributes, + provenance: row.provenance, + tagSources: row.tagSources, + metadataUrl: row.metadataUrl })), + changed_row: -1 }) + const validatedRows = mergeValidatedImportRows ( + pendingRows, + initialisePreviewRows (validated.rows)) + const nextSession = { + ...session, + rows: validatedRows, + repairMode: 'failed' as const } + setSession (nextSession) + const target = validatedRows.find (_1 => _1.sourceRow === sourceRow) + if (target == null) + return + if (Object.keys (target.validationErrors ?? { }).length > 0) + { + const saved = savePostImportSession (sessionId, nextSession, message => + toast ({ title: '取込状態を保存できませんでした', description: message })) + if (saved) + navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`) + return + } - const result = await apiPost<{ - created: number - skipped: number - failed: number - rows: PostImportResultRow[] }> ('/posts/import', { - rows: [{ - sourceRow: target.sourceRow, - url: target.url, - attributes: target.attributes, - provenance: target.provenance, - tagSources: target.tagSources, - metadataUrl: target.metadataUrl }] }) - const mergedRows = mergeImportResults (nextSession.rows, result.rows) - const recoverableRows = result.rows.filter (row => - row.status === 'failed' - && row.recoverable - && Object.keys (row.errors ?? { }).length > 0) - const nextRows = mergedRows.map ((row): PostImportRow => { - const recoverable = recoverableRows.find (_1 => _1.sourceRow === row.sourceRow) - if (recoverable == null) - return row - return { - ...row, - importStatus: 'pending', - validationErrors: recoverable.errors ?? { }, - importErrors: undefined } - }) - const recoverableTarget = nextRows.find (_1 => _1.sourceRow === sourceRow) - const resultSession = { - ...nextSession, - rows: nextRows, - repairMode: recoverableTarget == null ? 'all' as const : 'failed' as const } - setSession (resultSession) - if (recoverableTarget != null - && Object.keys (recoverableTarget.validationErrors).length > 0) - { - const saved = savePostImportSession (sessionId, resultSession, message => - toast ({ title: '取込状態を保存できませんでした', description: message })) - if (saved) - navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`) - return - } - } + const result = await apiPost<{ + created: number + skipped: number + failed: number + rows: PostImportResultRow[] }> ('/posts/import', { + rows: [{ + sourceRow: target.sourceRow, + url: target.url, + attributes: target.attributes, + provenance: target.provenance, + tagSources: target.tagSources, + metadataUrl: target.metadataUrl }] }) + const mergedRows = mergeImportResults (nextSession.rows, result.rows) + const recoverableRows = result.rows.filter (row => + row.status === 'failed' + && row.recoverable + && Object.keys (row.errors ?? { }).length > 0) + const nextRows = mergedRows.map ((row): PostImportRow => { + const recoverable = recoverableRows.find (_1 => _1.sourceRow === row.sourceRow) + if (recoverable == null) + return row + return { + ...row, + importStatus: 'pending', + validationErrors: recoverable.errors ?? { }, + importErrors: undefined } + }) + const recoverableTarget = nextRows.find (_1 => _1.sourceRow === sourceRow) + const resultSession = { + ...nextSession, + rows: nextRows, + repairMode: recoverableTarget == null ? 'all' as const : 'failed' as const } + setSession (resultSession) + if (recoverableTarget != null + && Object.keys (recoverableTarget.validationErrors).length > 0) + { + const saved = savePostImportSession (sessionId, resultSession, message => + toast ({ title: '取込状態を保存できませんでした', description: message })) + if (saved) + navigate (`/posts/import/${ sessionId }/review?edit=${ sourceRow }`) + return + } + } catch - { - setSession (session) - toast ({ title: '再試行に失敗しました' }) - } + { + setSession (session) + toast ({ title: '再試行に失敗しました' }) + } finally - { - setLoadingRow (null) - } + { + setLoadingRow (null) + } } const openRepair = (sourceRow: number) => {