Impl: vue editor - #27
Conversation
WalkthroughVueフロントエンドにTanStack Vue QueryとTipTap依存を追加し、プレスリリースの読み込み・編集・保存を行うApp.vueコンポーネント、グローバルCSS、およびVueQueryプラグイン登録を実装しました。 Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant UI as App.vue
participant VQ as VueQuery
participant API as API Server
participant DB as Database
User->>UI: ページ読み込み
UI->>VQ: useQuery(queryKey) を実行
VQ->>API: GET /press-releases/1
API->>DB: データ取得
DB-->>API: タイトル・コンテンツ返却
API-->>VQ: JSON レスポンス
VQ-->>UI: data, isPending, isError を提供
UI->>UI: watcher で editor.setContent(data.content), title を設定
Note over UI: ユーザーがタイトル/コンテンツを編集
User->>UI: 保存ボタンをクリック
UI->>VQ: mutate({ title, content }) を呼ぶ
VQ->>API: POST /press-releases/1 (JSON)
API->>DB: データ保存
DB-->>API: 保存成功
API-->>VQ: 成功レスポンス
VQ->>VQ: queryKey を invalidate -> 再フェッチ
VQ->>API: GET /press-releases/1 (最新取得)
VQ-->>UI: 更新された data を返す
UI-->>User: 保存完了を反映
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@webapp/frontend/vue/src/App.vue`:
- Around line 33-43: The watch callback uses title.value === '' as an
initialization flag and calls JSON.parse directly, which can overwrite
in-progress edits and throw on bad JSON; introduce an explicit initialization
ref (e.g., isInitialized) and use it instead of title.value to run the initial
fill only once (or until you intentionally reset), and wrap the JSON.parse of
newData.content in a try/catch (or use a safeParse helper) before calling
editor.value.commands.setContent so parse errors are handled without crashing;
update the watch to check isInitialized and set it true after the first
successful initialization and leave editor/title untouched on subsequent data
updates unless a deliberate reset occurs.
- Line 11: The BASE_URL constant is hardcoded to 'http://localhost:8080' (and
similar hardcoded URLs elsewhere), break out environment-specific configuration
and read the value from a Vite env var instead; replace occurrences of the
BASE_URL constant with something like reading import.meta.env.VITE_API_BASE_URL
(e.g. const BASE_URL = import.meta.env.VITE_API_BASE_URL ||
'http://localhost:8080') and ensure .env/.env.production define
VITE_API_BASE_URL so the app picks the correct backend per environment; update
any other references (the other hardcoded URL usages found in the file) to use
the same constant.
- Around line 82-106: The template currently renders nothing when isPending or
isError or !data, so add explicit fallback UIs: inside the same template
containing the <div v-if="!isPending && !isError && data" block, add
complementary v-else-if / v-else blocks that render a loading state when
isPending (e.g., spinner and "読み込み中..."), an error state when isError (e.g.,
error message and retry button that calls an existing retry or fetch method),
and a "no data" message when !data; ensure these blocks reference the same
reactive flags (isPending, isError, data) and reuse handleSave/isSaving or
EditorContent only in the main block to avoid showing editor when in fallback
states.
In `@webapp/frontend/vue/src/assets/main.css`:
- Around line 6-7: Remove the stray empty line and fix the casing of the
text-rendering value in main.css: delete the blank line before the property and
change the value used in the text-rendering declaration (the token
"optimizeLegibility") to the lowercase form required by stylelint so the rule
for value keyword case passes.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
webapp/frontend/vue/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (4)
webapp/frontend/vue/package.jsonwebapp/frontend/vue/src/App.vuewebapp/frontend/vue/src/assets/main.csswebapp/frontend/vue/src/main.ts
| import Text from '@tiptap/extension-text' | ||
|
|
||
| const queryKey = ['fetch-press-release'] | ||
| const BASE_URL = 'http://localhost:8080' |
There was a problem hiding this comment.
APIベースURLのハードコードをやめてください。
Line [11] が localhost 固定のため、環境ごとの切り替えで取得・保存が壊れます。VITE_ 環境変数経由にしてください。
🔧 修正案
-const BASE_URL = 'http://localhost:8080'
+const BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? '').replace(/\/$/, '')
+if (!BASE_URL) {
+ throw new Error('VITE_API_BASE_URL が未設定です')
+}Also applies to: 16-16, 50-50
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/frontend/vue/src/App.vue` at line 11, The BASE_URL constant is
hardcoded to 'http://localhost:8080' (and similar hardcoded URLs elsewhere),
break out environment-specific configuration and read the value from a Vite env
var instead; replace occurrences of the BASE_URL constant with something like
reading import.meta.env.VITE_API_BASE_URL (e.g. const BASE_URL =
import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080') and ensure
.env/.env.production define VITE_API_BASE_URL so the app picks the correct
backend per environment; update any other references (the other hardcoded URL
usages found in the file) to use the same constant.
| watch( | ||
| data, | ||
| (newData) => { | ||
| if (newData && title.value === '') { | ||
| title.value = newData.title | ||
| if (editor.value) { | ||
| editor.value.commands.setContent(JSON.parse(newData.content)) | ||
| } | ||
| } | ||
| }, | ||
| { immediate: true }, |
There was a problem hiding this comment.
初期化条件が脆く、再取得時に編集中データを上書きする可能性があります。
Line [36] の title.value === '' を初期化フラグに使うと、再取得時に意図せず内容を戻す挙動になります。加えて Line [39] の JSON.parse 失敗を未処理で落とすため、初期化フラグとパースガードを分離してください。
🔧 修正案
const title = ref('')
+const isInitialized = ref(false)
+
+const parseContent = (raw: string) => {
+ try {
+ return JSON.parse(raw)
+ } catch {
+ return { type: 'doc', content: [{ type: 'paragraph' }] }
+ }
+}
watch(
- data,
- (newData) => {
- if (newData && title.value === '') {
- title.value = newData.title
- if (editor.value) {
- editor.value.commands.setContent(JSON.parse(newData.content))
- }
- }
+ [data, editor],
+ ([newData, currentEditor]) => {
+ if (!newData || !currentEditor || isInitialized.value) return
+ title.value = newData.title ?? ''
+ currentEditor.commands.setContent(parseContent(newData.content))
+ isInitialized.value = true
},
{ immediate: true },
)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/frontend/vue/src/App.vue` around lines 33 - 43, The watch callback
uses title.value === '' as an initialization flag and calls JSON.parse directly,
which can overwrite in-progress edits and throw on bad JSON; introduce an
explicit initialization ref (e.g., isInitialized) and use it instead of
title.value to run the initial fill only once (or until you intentionally
reset), and wrap the JSON.parse of newData.content in a try/catch (or use a
safeParse helper) before calling editor.value.commands.setContent so parse
errors are handled without crashing; update the watch to check isInitialized and
set it true after the first successful initialization and leave editor/title
untouched on subsequent data updates unless a deliberate reset occurs.
| font-synthesis: none; | ||
| text-rendering: optimizeLegibility; |
There was a problem hiding this comment.
Stylelintエラーを解消してください。
Line [6] の空行と Line [7] の text-rendering 値のケースで lint が失敗します。CI 通過のため修正が必要です。
🔧 修正案
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
-
font-synthesis: none;
- text-rendering: optimizeLegibility;
+ text-rendering: optimizelegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| font-synthesis: none; | |
| text-rendering: optimizeLegibility; | |
| :root { | |
| font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; | |
| line-height: 1.5; | |
| font-weight: 400; | |
| font-synthesis: none; | |
| text-rendering: optimizelegibility; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| } |
🧰 Tools
🪛 Stylelint (17.3.0)
[error] 6-6: Unexpected empty line before declaration (declaration-empty-line-before)
(declaration-empty-line-before)
[error] 7-7: Expected "optimizeLegibility" to be "optimizelegibility" (value-keyword-case)
(value-keyword-case)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/frontend/vue/src/assets/main.css` around lines 6 - 7, Remove the stray
empty line and fix the casing of the text-rendering value in main.css: delete
the blank line before the property and change the value used in the
text-rendering declaration (the token "optimizeLegibility") to the lowercase
form required by stylelint so the rule for value keyword case passes.
There was a problem hiding this comment.
♻️ Duplicate comments (2)
webapp/frontend/vue/src/App.vue (2)
11-11:⚠️ Potential issue | 🟠 MajorAPIベースURLのハードコードを解消してください。
Line [11] が
localhost固定のままなので、環境切り替え時に取得・保存の両方が壊れます。import.meta.envから読み込む形に統一してください。
Based on learnings: Applies to **/*.{ts,tsx,js,jsx} : Never commit secrets; use environment variables with PUBLIC_ prefix for client-visible values.🔧 修正例
-const BASE_URL = 'http://localhost:8080' +const BASE_URL = ( + import.meta.env.PUBLIC_API_BASE_URL ?? + import.meta.env.VITE_API_BASE_URL ?? + '' +).replace(/\/$/, '') + +if (!BASE_URL) { + throw new Error('API base URL が未設定です') +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/frontend/vue/src/App.vue` at line 11, Replace the hardcoded BASE_URL constant with a value read from the client environment (use import.meta.env.PUBLIC_API_BASE_URL) so the app uses environment-configured API base URLs; update the declaration of BASE_URL (the const BASE_URL in App.vue) to read import.meta.env.PUBLIC_API_BASE_URL with a sensible fallback (e.g., window.location.origin or empty string) and ensure any other modules referencing BASE_URL (or importing it) use this new env-backed value; also update documentation/env sample to include PUBLIC_API_BASE_URL so builds provide the correct client-visible variable.
33-45:⚠️ Potential issue | 🟠 Major初期化ロジックが不安定で、再取得時に編集中データを上書きする可能性があります。
Line [33] 以降の
watch(data, ...)は再フェッチのたびにtitle/本文を再代入するため、保存後のinvalidateQueriesや再取得で編集中内容を戻すリスクがあります。加えて Line [41] のJSON.parse未ガードでクラッシュし得ます。editorの準備完了待ちも含めて初期化を一度だけに分離してください。🔧 修正例
const title = ref('') +const isInitialized = ref(false) + +const parseContent = (raw: string) => { + try { + return JSON.parse(raw) + } catch { + return { type: 'doc', content: [{ type: 'paragraph' }] } + } +} -watch( - data, - (newData) => { - if (!newData) return - - title.value = newData.title - - if (editor.value) { - editor.value.commands.setContent(JSON.parse(newData.content)) - } - }, - { immediate: true }, -) +watch( + [data, editor], + ([newData, currentEditor]) => { + if (!newData || !currentEditor || isInitialized.value) return + title.value = newData.title ?? '' + currentEditor.commands.setContent(parseContent(newData.content)) + isInitialized.value = true + }, + { immediate: true }, +)#!/bin/bash # 検証内容: # 1) watch が data 単体監視かどうか # 2) JSON.parse(newData.content) が未ガードかどうか # 期待結果: # - watch(data, ...) と JSON.parse(...) が見つかれば、指摘の再現性あり rg -n -C3 'watch\(' webapp/frontend/vue/src/App.vue rg -n -C2 'JSON\.parse\(newData\.content\)' webapp/frontend/vue/src/App.vue🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/frontend/vue/src/App.vue` around lines 33 - 45, The watch on data unconditionally reassigns title and editor content on every refetch and calls JSON.parse(newData.content) without guarding, risking overwrites and crashes; modify the watch(data, ...) logic so it only performs initial setup once and only after the editor is ready: add an initialization flag (e.g. isInitialized) or replace with a one-time effect that waits for editor.value to be truthy before setting title.value and calling editor.value.commands.setContent; protect JSON.parse(newData.content) with a try/catch and fallback to a safe default (e.g. empty doc) and only call setContent when parse succeeds, referencing the existing symbols data, title.value, editor.value, editor.value.commands.setContent, newData.content, and watch to locate the code to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@webapp/frontend/vue/src/App.vue`:
- Line 11: Replace the hardcoded BASE_URL constant with a value read from the
client environment (use import.meta.env.PUBLIC_API_BASE_URL) so the app uses
environment-configured API base URLs; update the declaration of BASE_URL (the
const BASE_URL in App.vue) to read import.meta.env.PUBLIC_API_BASE_URL with a
sensible fallback (e.g., window.location.origin or empty string) and ensure any
other modules referencing BASE_URL (or importing it) use this new env-backed
value; also update documentation/env sample to include PUBLIC_API_BASE_URL so
builds provide the correct client-visible variable.
- Around line 33-45: The watch on data unconditionally reassigns title and
editor content on every refetch and calls JSON.parse(newData.content) without
guarding, risking overwrites and crashes; modify the watch(data, ...) logic so
it only performs initial setup once and only after the editor is ready: add an
initialization flag (e.g. isInitialized) or replace with a one-time effect that
waits for editor.value to be truthy before setting title.value and calling
editor.value.commands.setContent; protect JSON.parse(newData.content) with a
try/catch and fallback to a safe default (e.g. empty doc) and only call
setContent when parse succeeds, referencing the existing symbols data,
title.value, editor.value, editor.value.commands.setContent, newData.content,
and watch to locate the code to change.
Summary by CodeRabbit
リリースノート