From ca205d5a92fe6ca0d995737c371422a23679b6ab Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 13:09:31 +0000 Subject: [PATCH 01/10] Add edit functionality to KnowledgeSuggestionDetailPage Co-Authored-By: noritaka.ikeda@route06.co.jp --- .../updateKnowledgeSuggestionContent.ts | 45 +++++++ .../EditableContent.module.css | 115 ++++++++++++++++++ .../EditableContent/EditableContent.tsx | 98 +++++++++++++++ .../KnowledgeSuggestionDetailPage.tsx | 17 ++- 4 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 frontend/apps/app/features/projects/actions/updateKnowledgeSuggestionContent.ts create mode 100644 frontend/apps/app/features/projects/components/EditableContent/EditableContent.module.css create mode 100644 frontend/apps/app/features/projects/components/EditableContent/EditableContent.tsx diff --git a/frontend/apps/app/features/projects/actions/updateKnowledgeSuggestionContent.ts b/frontend/apps/app/features/projects/actions/updateKnowledgeSuggestionContent.ts new file mode 100644 index 0000000000..af24cb8c84 --- /dev/null +++ b/frontend/apps/app/features/projects/actions/updateKnowledgeSuggestionContent.ts @@ -0,0 +1,45 @@ +'use server' + +import { createClient } from '@/libs/db/server' +import * as v from 'valibot' + +const formDataSchema = v.object({ + suggestionId: v.pipe( + v.string(), + v.transform((value) => Number(value)), + ), + content: v.string(), +}) + +export const updateKnowledgeSuggestionContent = async (formData: FormData) => { + const formDataObject = { + suggestionId: formData.get('suggestionId'), + content: formData.get('content'), + } + + const parsedData = v.safeParse(formDataSchema, formDataObject) + + if (!parsedData.success) { + throw new Error(`Invalid form data: ${JSON.stringify(parsedData.issues)}`) + } + + const { suggestionId, content } = parsedData.output + + try { + const supabase = await createClient() + + const { error: updateError } = await supabase + .from('KnowledgeSuggestion') + .update({ content, updatedAt: new Date().toISOString() }) + .eq('id', suggestionId) + + if (updateError) { + throw new Error('Failed to update knowledge suggestion content') + } + + return { success: true } + } catch (error) { + console.error('Error updating knowledge suggestion content:', error) + throw error + } +} diff --git a/frontend/apps/app/features/projects/components/EditableContent/EditableContent.module.css b/frontend/apps/app/features/projects/components/EditableContent/EditableContent.module.css new file mode 100644 index 0000000000..448da206d5 --- /dev/null +++ b/frontend/apps/app/features/projects/components/EditableContent/EditableContent.module.css @@ -0,0 +1,115 @@ +.container { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.sectionTitle { + font-size: 1.25rem; + font-weight: 600; + margin: 0; + color: var(--color-primary); + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--color-border); +} + +.editButton { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + background-color: var(--color-background-tertiary); + color: var(--color-primary); + border: 1px solid var(--color-border); + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.editButton:hover { + background-color: var(--color-background-tertiary-hover); + transform: translateY(-1px); +} + +.form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.contentTextarea { + padding: 1.25rem; + background-color: var(--color-background-code); + border-radius: 0.5rem; + font-family: monospace; + font-size: 0.875rem; + line-height: 1.6; + border: 1px solid rgba(0, 0, 0, 0.1); + resize: vertical; + min-height: 200px; + width: 100%; +} + +.actionButtons { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 1rem; +} + +.cancelButton { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + background-color: var(--color-background-tertiary); + color: var(--color-text-primary); + border: 1px solid var(--color-border); + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.cancelButton:hover { + background-color: var(--color-background-tertiary-hover); +} + +.saveButton { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + background-color: var(--color-primary); + color: white; + border: none; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.saveButton:hover { + background-color: var(--color-primary-dark); + transform: translateY(-1px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15); +} + +.saveButton:disabled, +.cancelButton:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; + box-shadow: none; +} diff --git a/frontend/apps/app/features/projects/components/EditableContent/EditableContent.tsx b/frontend/apps/app/features/projects/components/EditableContent/EditableContent.tsx new file mode 100644 index 0000000000..641c31ed90 --- /dev/null +++ b/frontend/apps/app/features/projects/components/EditableContent/EditableContent.tsx @@ -0,0 +1,98 @@ +'use client' + +import React, { useState } from 'react' +import { updateKnowledgeSuggestionContent } from '../../actions/updateKnowledgeSuggestionContent' +import styles from './EditableContent.module.css' + +type EditableContentProps = { + content: string + suggestionId: number + className?: string +} + +export const EditableContent = ({ + content, + suggestionId, + className, +}: EditableContentProps) => { + const [isEditing, setIsEditing] = useState(false) + const [editedContent, setEditedContent] = useState(content) + const [isSaving, setIsSaving] = useState(false) + const [savedContent, setSavedContent] = useState(content) + + const handleEditClick = () => { + setIsEditing(true) + } + + const handleCancelClick = () => { + setEditedContent(savedContent) + setIsEditing(false) + } + + const handleSave = async (formData: FormData) => { + try { + setIsSaving(true) + await updateKnowledgeSuggestionContent(formData) + setSavedContent(editedContent) + setIsEditing(false) + } catch (error) { + console.error('Error saving content:', error) + } finally { + setIsSaving(false) + } + } + + return ( +
+
+
Content
+ {!isEditing && ( + + )} +
+ + {isEditing ? ( +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + await handleSave(formData); + }} className={styles.form}> + +