From 24e85e8aa99d64177de847f0efa89866735dbf49 Mon Sep 17 00:00:00 2001 From: greenflame089 Date: Sat, 30 Aug 2025 16:30:40 -0400 Subject: [PATCH 1/4] Fix publish modals --- src/components/common/AudioPublishModal.tsx | 10 +++++++- src/components/common/GenericPublishModal.tsx | 24 +++++++++++++------ src/components/common/ImagePanel.tsx | 2 +- src/components/common/VideoPublishModal.tsx | 10 +++++++- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/components/common/AudioPublishModal.tsx b/src/components/common/AudioPublishModal.tsx index ea29406..e10a67e 100644 --- a/src/components/common/AudioPublishModal.tsx +++ b/src/components/common/AudioPublishModal.tsx @@ -20,6 +20,8 @@ import { toBase64 } from '../../utils/toBase64'; import AddIcon from '@mui/icons-material/Add'; import CloseIcon from '@mui/icons-material/Close'; import { usePublishAudio } from './PublishAudio'; +import { useDispatch } from 'react-redux'; +import { setNotification } from '../../state/features/notificationsSlice'; const StyledModal = styled(Modal)(({ theme }) => ({ display: 'flex', @@ -141,6 +143,7 @@ export const AudioModal: React.FC = ({ onPublish, editVideoIdentifier, }) => { + const dispatch = useDispatch(); const [file, setFile] = useState(null); const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); @@ -189,7 +192,12 @@ export const AudioModal: React.FC = ({ if (missingFields.length > 0) { const missingFieldsString = missingFields.join(', '); const errMsg = `Missing: ${missingFieldsString}`; - + dispatch( + setNotification({ + msg: errMsg, + alertType: 'error', + }), + ); return; } if (!file) return; diff --git a/src/components/common/GenericPublishModal.tsx b/src/components/common/GenericPublishModal.tsx index 043ad9b..473930b 100644 --- a/src/components/common/GenericPublishModal.tsx +++ b/src/components/common/GenericPublishModal.tsx @@ -86,20 +86,25 @@ export const GenericModal: React.FC = ({ const { publishGeneric } = usePublishGeneric(); const dispatch = useDispatch(); - let acceptedFile = {}; + // Build accept prop for dropzone correctly + let accept: Record | undefined = undefined; if (acceptedFileType) { - acceptedFile = { - [acceptedFileType]: [], - }; + accept = { [acceptedFileType]: [] } as any; + } else if (acceptedFileTypes && acceptedFileTypes.length > 0) { + accept = acceptedFileTypes.reduce((acc, t) => { + acc[t] = [] as any; + return acc; + }, {} as Record); } + const { getRootProps, getInputProps } = useDropzone({ - ...acceptedFile, + accept, maxFiles: 1, maxSize, onDrop: (acceptedFiles) => { setFile(acceptedFiles[0]); }, - onDropRejected: (rejectedFiles) => { + onDropRejected: () => { dispatch( setNotification({ msg: 'Your file is over the 500mb limit.', @@ -137,7 +142,12 @@ export const GenericModal: React.FC = ({ if (missingFields.length > 0) { const missingFieldsString = missingFields.join(', '); const errMsg = `Missing: ${missingFieldsString}`; - + dispatch( + setNotification({ + msg: errMsg, + alertType: 'error', + }), + ); return; } if (!file) return; diff --git a/src/components/common/ImagePanel.tsx b/src/components/common/ImagePanel.tsx index b09e1ac..bc7ed89 100644 --- a/src/components/common/ImagePanel.tsx +++ b/src/components/common/ImagePanel.tsx @@ -200,7 +200,7 @@ export const ImagePanel: React.FC = ({ onSelect, height, width setIsOpen(false); }} > - + diff --git a/src/components/common/VideoPublishModal.tsx b/src/components/common/VideoPublishModal.tsx index 8fad354..b1bb98c 100644 --- a/src/components/common/VideoPublishModal.tsx +++ b/src/components/common/VideoPublishModal.tsx @@ -20,6 +20,8 @@ import { usePublishVideo } from './PublishVideo'; import { toBase64 } from '../../utils/toBase64'; import AddIcon from '@mui/icons-material/Add'; import CloseIcon from '@mui/icons-material/Close'; +import { useDispatch } from 'react-redux'; +import { setNotification } from '../../state/features/notificationsSlice'; const StyledModal = styled(Modal)(({ theme }) => ({ display: 'flex', alignItems: 'center', @@ -62,6 +64,7 @@ const VideoModal: React.FC = ({ onPublish, editVideoIdentifier, }) => { + const dispatch = useDispatch(); const [file, setFile] = useState(null); const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); @@ -110,7 +113,12 @@ const VideoModal: React.FC = ({ if (missingFields.length > 0) { const missingFieldsString = missingFields.join(', '); const errMsg = `Missing: ${missingFieldsString}`; - + dispatch( + setNotification({ + msg: errMsg, + alertType: 'error', + }), + ); return; } if (!file) return; -- 2.43.0 From 948f65a75765e2953765145bf2d8628c2a2cca6a Mon Sep 17 00:00:00 2001 From: greenflame089 Date: Tue, 16 Sep 2025 13:19:17 -0400 Subject: [PATCH 2/4] Fix publishing new posts --- .../BlogIndividualProfile.tsx | 19 +++++++++++++++++++ src/pages/CreatePost/CreatePostBuilder.tsx | 6 ++++-- src/pages/CreatePost/CreatePostMinimal.tsx | 6 ++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx b/src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx index d39134d..465f0ea 100644 --- a/src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx +++ b/src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx @@ -7,6 +7,7 @@ import { Typography, Box, Button, useTheme } from '@mui/material'; import EditIcon from '@mui/icons-material/Edit'; import BlogPostPreview from '../BlogList/PostPreview'; import { + setCurrentBlog, setIsLoadingGlobal, setVisitingBlog, toggleEditBlogModal, @@ -188,6 +189,24 @@ export const BlogIndividualProfile = () => { } const responseData = await response.json(); dispatch(setVisitingBlog({ ...responseData, name })); + const isOwner = user?.name && username && user.name === username; + if (isOwner) { + dispatch( + setCurrentBlog({ + createdAt: responseData?.createdAt || Date.now(), + blogId: blog, + title: responseData?.title || '', + description: responseData?.description || '', + blogImage: responseData?.blogImage || '', + category: responseData?.category, + tags: responseData?.tags || [], + navbarConfig: responseData?.navbarConfig || null, + wikiEnabled: (responseData as any)?.wikiEnabled ?? false, + editorWhitelist: (responseData as any)?.editorWhitelist || [], + editorBlacklist: (responseData as any)?.editorBlacklist || [], + }), + ); + } setUserBlog(responseData); setNotFound(false); } catch (error) { diff --git a/src/pages/CreatePost/CreatePostBuilder.tsx b/src/pages/CreatePost/CreatePostBuilder.tsx index f80cd2a..600b700 100644 --- a/src/pages/CreatePost/CreatePostBuilder.tsx +++ b/src/pages/CreatePost/CreatePostBuilder.tsx @@ -421,6 +421,10 @@ export const CreatePostBuilder = ({ const errMsg = `Missing: ${missingFieldsString}`; errorMsg = errMsg; } + const blogIdForPublish = computeBlogIdForPublish(currentBlog, blogMetadataForEdit); + if (!blogIdForPublish && !errorMsg) { + errorMsg = 'Cannot determine which blog to publish to. Open your blog and try again.'; + } // In edit mode, allow updating even if content array is empty (legacy/minimal posts) if (errorMsg) { @@ -446,7 +450,6 @@ export const CreatePostBuilder = ({ layoutGeneralSettings, }; try { - if (!currentBlog) return; const id = uid(); let createTitleId = title .replace(/[^a-zA-Z0-9\s-]/g, '') @@ -468,7 +471,6 @@ export const CreatePostBuilder = ({ createTitleId = createTitleId.slice(1); } createTitleId = createTitleId.slice(0, 24); - const blogIdForPublish = computeBlogIdForPublish(currentBlog, blogMetadataForEdit); const identifier = postIdForEdit ? `${blogIdForPublish ?? ''}-post-${postIdForEdit}` : `${blogIdForPublish ?? currentBlog?.blogId ?? ''}-post-${createTitleId}-${id}`; diff --git a/src/pages/CreatePost/CreatePostMinimal.tsx b/src/pages/CreatePost/CreatePostMinimal.tsx index 557e575..74055f0 100644 --- a/src/pages/CreatePost/CreatePostMinimal.tsx +++ b/src/pages/CreatePost/CreatePostMinimal.tsx @@ -309,6 +309,10 @@ export const CreatePostMinimal = ({ const errMsg = `Missing: ${missingFieldsString}`; errorMsg = errMsg; } + const blogIdForPublish = computeBlogIdForPublish(currentBlog, blogMetadataForEdit); + if (!blogIdForPublish && !errorMsg) { + errorMsg = 'Cannot determine which blog to publish to. Open your blog and try again.'; + } // In edit mode, allow updating even if content array is empty (legacy/minimal posts) if (errorMsg) { @@ -334,7 +338,6 @@ export const CreatePostMinimal = ({ layoutGeneralSettings, }; try { - if (!currentBlog) return; const id = uid(); let createTitleId = title .replace(/[^a-zA-Z0-9\s-]/g, '') @@ -356,7 +359,6 @@ export const CreatePostMinimal = ({ createTitleId = createTitleId.slice(1); } createTitleId = createTitleId.slice(0, 24); - const blogIdForPublish = computeBlogIdForPublish(currentBlog, blogMetadataForEdit); const identifier = postIdForEdit ? `${blogIdForPublish ?? ''}-post-${postIdForEdit}` : `${blogIdForPublish ?? currentBlog?.blogId ?? ''}-post-${createTitleId}-${id}`; -- 2.43.0 From 45c9b7c59cdadae1176ffdd312e5222e8099bd27 Mon Sep 17 00:00:00 2001 From: greenflame089 Date: Tue, 16 Sep 2025 14:05:34 -0400 Subject: [PATCH 3/4] test: publish new post --- package-lock.json | 4 +- package.json | 2 +- tests/pages/CreatePost.publish.new.test.tsx | 139 ++++++++++++++++++++ 3 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 tests/pages/CreatePost.publish.new.test.tsx diff --git a/package-lock.json b/package-lock.json index 7a0ac6e..e180c56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "q-blog", - "version": "0.3.0", + "version": "0.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "q-blog", - "version": "0.3.0", + "version": "0.3.2", "dependencies": { "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", diff --git a/package.json b/package.json index c856f13..5acdbda 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "q-blog", "private": true, - "version": "0.3.1", + "version": "0.3.2", "type": "module", "scripts": { "dev": "vite", diff --git a/tests/pages/CreatePost.publish.new.test.tsx b/tests/pages/CreatePost.publish.new.test.tsx new file mode 100644 index 0000000..86805d0 --- /dev/null +++ b/tests/pages/CreatePost.publish.new.test.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import { describe, it, expect, vi } from 'vitest'; +import { configureStore } from '@reduxjs/toolkit'; +import { Provider } from 'react-redux'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { http, HttpResponse } from 'msw'; + +import { CreatePost } from '@/pages/CreatePost/CreatePost'; +import notificationsReducer from '@/state/features/notificationsSlice'; +import authReducer, { addUser } from '@/state/features/authSlice'; +import globalReducer, { setCurrentBlog } from '@/state/features/globalSlice'; +import blogReducer from '@/state/features/blogSlice'; +import mailReducer from '@/state/features/mailSlice'; +import bookmarksReducer from '@/state/features/bookmarksSlice'; +import { server } from '../msw/server'; + +const createTestStore = () => + configureStore({ + reducer: { + notifications: notificationsReducer, + auth: authReducer, + global: globalReducer, + blog: blogReducer, + mail: mailReducer, + bookmarks: bookmarksReducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: false, + }), + }); + +type TestStore = ReturnType; + +const renderCreatePost = (store: TestStore) => + render( + + + + + } /> + + + + , + ); + +const selectMinimalEditor = async () => { + const user = userEvent.setup(); + const option = await screen.findByText('Minimal Editor'); + await user.click(option); + await screen.findByPlaceholderText('Title'); + return user; +}; + +describe('CreatePost (new publish flow)', () => { + const categoriesHandler = http.get('/arbitrary/categories', () => HttpResponse.json([])); + + it('surfaces an error when no blog is selected', async () => { + server.use(categoriesHandler); + const store = createTestStore(); + globalThis.qortalRequest = vi.fn().mockResolvedValue({ ok: true }); + store.dispatch(addUser({ address: 'QADDR', publicKey: 'PUB', name: 'alice' })); + + renderCreatePost(store); + const user = await selectMinimalEditor(); + + const titleField = await screen.findByPlaceholderText('Title'); + await user.type(titleField, 'Unsaved Draft'); + + const publishTrigger = await screen.findByRole('button', { name: /^Publish$/i }); + await user.click(publishTrigger); + + const submitButton = await screen.findByRole('button', { name: /Submit/i }); + await user.click(submitButton); + + await waitFor(() => { + expect(store.getState().notifications.alertTypes.alertError).toBe( + 'Cannot determine which blog to publish to. Open your blog and try again.', + ); + }); + expect(globalThis.qortalRequest).not.toHaveBeenCalled(); + }, 10000); + + it('publishes a new post when currentBlog is available', async () => { + server.use(categoriesHandler); + const store = createTestStore(); + globalThis.qortalRequest = vi.fn().mockResolvedValue({ ok: true }); + store.dispatch(addUser({ address: 'QADDR', publicKey: 'PUB', name: 'alice' })); + store.dispatch( + setCurrentBlog({ + createdAt: Date.now(), + blogId: 'q-blog-myblog', + title: 'My Blog', + description: '', + blogImage: '', + category: '', + tags: [], + wikiEnabled: false, + editorWhitelist: [], + editorBlacklist: [], + }), + ); + + renderCreatePost(store); + const user = await selectMinimalEditor(); + + const titleField = await screen.findByPlaceholderText('Title'); + await user.type(titleField, 'My New Post'); + + const publishTrigger = await screen.findByRole('button', { name: /^Publish$/i }); + await user.click(publishTrigger); + + const submitButton = await screen.findByRole('button', { name: /Submit/i }); + await user.click(submitButton); + + await waitFor(() => { + expect(globalThis.qortalRequest).toHaveBeenCalled(); + }); + + const publishCall = (globalThis.qortalRequest as any).mock.calls.find( + (call: any[]) => call[0]?.action === 'PUBLISH_QDN_RESOURCE', + ); + expect(publishCall).toBeTruthy(); + expect(publishCall?.[0]).toMatchObject({ + action: 'PUBLISH_QDN_RESOURCE', + service: 'BLOG_POST', + }); + expect(publishCall?.[0]?.identifier.startsWith('q-blog-myblog-post-')).toBe(true); + await waitFor(() => { + expect(store.getState().notifications.alertTypes.alertSuccess).toBe( + 'Blog post successfully published', + ); + }); + }, 10000); +}); -- 2.43.0 From 4d809f4ccf1447f09c3c484d3757926567e192dd Mon Sep 17 00:00:00 2001 From: greenflame089 Date: Tue, 16 Sep 2025 14:08:23 -0400 Subject: [PATCH 4/4] =?UTF-8?q?release:=20v0.3.2=20=E2=80=94=20Publish=20n?= =?UTF-8?q?ew=20posts=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/RELEASE_NOTES_v0.3.2.md | 10 ++++++++++ docs/USER_ANNOUNCEMENT_v0.3.2.md | 11 +++++++++++ 2 files changed, 21 insertions(+) create mode 100644 docs/RELEASE_NOTES_v0.3.2.md create mode 100644 docs/USER_ANNOUNCEMENT_v0.3.2.md diff --git a/docs/RELEASE_NOTES_v0.3.2.md b/docs/RELEASE_NOTES_v0.3.2.md new file mode 100644 index 0000000..6019cd4 --- /dev/null +++ b/docs/RELEASE_NOTES_v0.3.2.md @@ -0,0 +1,10 @@ +# Q-Blog v0.3.2 Release Notes + +## Fixes + +- Resolved an issue where creating a brand-new post and confirming "Publish" would silently close the modal instead of requesting the transaction. The publish flow now correctly detects the active blog and surfaces clear errors if the context is missing. +- Updated the embedded content publishing panels (video, audio, file, image, poll) to use consistent "Publish" wording and to emit notifications when QDN publishes fail, making it easier to understand what went wrong. + +## Testing + +- Added end-to-end Vitest coverage for the new-post publish modal, exercising both the success path and the missing-blog error case so regressions are caught automatically before release. diff --git a/docs/USER_ANNOUNCEMENT_v0.3.2.md b/docs/USER_ANNOUNCEMENT_v0.3.2.md new file mode 100644 index 0000000..b9e9e86 --- /dev/null +++ b/docs/USER_ANNOUNCEMENT_v0.3.2.md @@ -0,0 +1,11 @@ +# Q-Blog v0.3.2 User Announcement + +Hi everyone, + +We discovered that publishing brand-new posts from the "Create Post" button sometimes closed the modal without ever prompting for a transaction. That should never happen, and we're sorry both for the disruption and for not catching the bug earlier with better tests. The flow now confirms which blog you're publishing to and reports a clear error if anything is missing so you can retry immediately. + +While we were in there we also cleaned up the embedded content publishers—video, audio, files, images, and polls now use consistent "Publish" terminology, and you’ll see explicit notifications whenever an upload to QDN fails. + +To keep this from happening again, we added automated Vitest coverage that drives the new-post modal end to end, checking both the happy path and the missing-blog error path. These tests now run with the suite so we'll spot regressions before they reach you. + +Thanks for your patience, and please let us know if anything still looks off. -- 2.43.0