From d66201a787437728876e5c04fdbf93bfe50a6309 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Mon, 9 Dec 2024 14:36:23 +0100 Subject: [PATCH 01/10] add new PostForm component --- app/views/posts/index.html.erb | 2 +- app/views/posts/new.html.erb | 9 ++-- app/webpacker/components/Posts/PostForm.jsx | 47 +++++++++++++++++++ .../{PostsWidget.js => Posts/PostsWidget.jsx} | 12 ++--- 4 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 app/webpacker/components/Posts/PostForm.jsx rename app/webpacker/components/{PostsWidget.js => Posts/PostsWidget.jsx} (91%) diff --git a/app/views/posts/index.html.erb b/app/views/posts/index.html.erb index 300795c61e..bb87f86b33 100644 --- a/app/views/posts/index.html.erb +++ b/app/views/posts/index.html.erb @@ -1,5 +1,5 @@
- <%= react_component("PostsWidget", { + <%= react_component("Posts/PostsWidget", { initialPage: @current_page, }, { id: "posts_widget", diff --git a/app/views/posts/new.html.erb b/app/views/posts/new.html.erb index bfbe81acd1..f46f7c0b8b 100644 --- a/app/views/posts/new.html.erb +++ b/app/views/posts/new.html.erb @@ -1,7 +1,10 @@ <% provide(:title, 'New post') %> -
-

<%= yield(:title) %>

+<% options = all_to_options(PostTag) %> - <%= render "posts/post_form" %> +
+ <%= react_component("Posts/PostForm", { + header: yield(:title), + allTags: options.as_json, + }) %>
diff --git a/app/webpacker/components/Posts/PostForm.jsx b/app/webpacker/components/Posts/PostForm.jsx new file mode 100644 index 0000000000..3f777536d0 --- /dev/null +++ b/app/webpacker/components/Posts/PostForm.jsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { + Button, Form, FormField, Header, +} from 'semantic-ui-react'; +import useInputState from '../../lib/hooks/useInputState'; +import useCheckboxState from '../../lib/hooks/useCheckboxState'; +import MarkdownEditor from '../wca/FormBuilder/input/MarkdownEditor'; + +export default function PostForm({ + header, title, body, tags, allTags, isStickied, showOnHomePage, +}) { + const [formTitle, setFormTitle] = useInputState(title ?? ''); + const [formBody, setFormBody] = useInputState(body ?? ''); + const [formTags, setFormTags] = useState(tags ?? []); + const [formIsStickied, setFormIsStickied] = useCheckboxState(isStickied ?? false); + const [formShowOnHomePage, setFormShowOnHomePage] = useCheckboxState(showOnHomePage ?? true); + console.log(allTags); + + return ( + <> +
+ {header} +
+
+ + + + + + + + + + + + + + + + +

Careful! This is not secure for private data. This is only to prevent cluttering the homepage. Posts that are not shown on the homepage are still accessible to the public via permalink or through tags.

+
+ +
+ + ); +} diff --git a/app/webpacker/components/PostsWidget.js b/app/webpacker/components/Posts/PostsWidget.jsx similarity index 91% rename from app/webpacker/components/PostsWidget.js rename to app/webpacker/components/Posts/PostsWidget.jsx index 7bf4038d1d..7260c3dba9 100644 --- a/app/webpacker/components/PostsWidget.js +++ b/app/webpacker/components/Posts/PostsWidget.jsx @@ -3,12 +3,12 @@ import { Button, Card, Icon, List, Pagination, } from 'semantic-ui-react'; -import useLoadedData from '../lib/hooks/useLoadedData'; -import { postsUrl } from '../lib/requests/routes.js.erb'; -import Loading from './Requests/Loading'; -import Errored from './Requests/Errored'; -import { formattedTextForDate } from '../lib/utils/wca'; -import '../stylesheets/posts_widget.scss'; +import useLoadedData from '../../lib/hooks/useLoadedData'; +import { postsUrl } from '../../lib/requests/routes.js.erb'; +import Loading from '../Requests/Loading'; +import Errored from '../Requests/Errored'; +import { formattedTextForDate } from '../../lib/utils/wca'; +import '../../stylesheets/posts_widget.scss'; function PostTitlesList({ posts, From 9873b8528e67cc003689109e71dbe7f3fd56975c Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Mon, 9 Dec 2024 16:06:04 +0100 Subject: [PATCH 02/10] allow creating a post --- app/assets/javascripts/posts.js | 6 --- app/controllers/posts_controller.rb | 2 +- app/views/posts/new.html.erb | 10 ++--- app/webpacker/components/Posts/CreatePost.jsx | 13 +++++++ app/webpacker/components/Posts/PostForm.jsx | 39 ++++++++++++++----- app/webpacker/components/Posts/api/posts.js | 13 +++++++ app/webpacker/lib/requests/routes.js.erb | 4 ++ 7 files changed, 65 insertions(+), 22 deletions(-) delete mode 100644 app/assets/javascripts/posts.js create mode 100644 app/webpacker/components/Posts/CreatePost.jsx create mode 100644 app/webpacker/components/Posts/api/posts.js diff --git a/app/assets/javascripts/posts.js b/app/assets/javascripts/posts.js deleted file mode 100644 index ae981a3998..0000000000 --- a/app/assets/javascripts/posts.js +++ /dev/null @@ -1,6 +0,0 @@ -onPage('posts#new, posts#create, posts#edit, posts#update', function() { - $('input[name="post[sticky]"]').on('change', function() { - var sticky = this.checked; - $('.date_picker.post_unstick_at').toggle(sticky); - }).trigger('change'); -}); diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index fbd429d8a0..2fcb929330 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -66,7 +66,7 @@ def create @post.author = current_user if @post.save flash[:success] = "Created new post" - redirect_to post_path(@post.slug) + render json: { status: 'ok', post: @post } else render 'new' end diff --git a/app/views/posts/new.html.erb b/app/views/posts/new.html.erb index f46f7c0b8b..046b3afdd6 100644 --- a/app/views/posts/new.html.erb +++ b/app/views/posts/new.html.erb @@ -1,10 +1,10 @@ <% provide(:title, 'New post') %> -<% options = all_to_options(PostTag) %> -
- <%= react_component("Posts/PostForm", { - header: yield(:title), - allTags: options.as_json, + <%= react_component("Posts/CreatePost", { + allTags: PostTag.distinct.pluck(:tag).map { |tag| { value: tag, text: tag, key: tag } }.as_json, + post: @post.as_json({ + + }), }) %>
diff --git a/app/webpacker/components/Posts/CreatePost.jsx b/app/webpacker/components/Posts/CreatePost.jsx new file mode 100644 index 0000000000..6834d9ed7f --- /dev/null +++ b/app/webpacker/components/Posts/CreatePost.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import WCAQueryClientProvider from '../../lib/providers/WCAQueryClientProvider'; +import PostForm from './PostForm'; + +export default function Wrapper({ + allTags, post, +}) { + return ( + + + + ); +} diff --git a/app/webpacker/components/Posts/PostForm.jsx b/app/webpacker/components/Posts/PostForm.jsx index 3f777536d0..367fb7eb91 100644 --- a/app/webpacker/components/Posts/PostForm.jsx +++ b/app/webpacker/components/Posts/PostForm.jsx @@ -1,27 +1,46 @@ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { Button, Form, FormField, Header, } from 'semantic-ui-react'; +import { useMutation } from '@tanstack/react-query'; import useInputState from '../../lib/hooks/useInputState'; import useCheckboxState from '../../lib/hooks/useCheckboxState'; import MarkdownEditor from '../wca/FormBuilder/input/MarkdownEditor'; +import { createOrEditPost } from './api/posts'; export default function PostForm({ - header, title, body, tags, allTags, isStickied, showOnHomePage, + header, allTags, post, }) { - const [formTitle, setFormTitle] = useInputState(title ?? ''); - const [formBody, setFormBody] = useInputState(body ?? ''); - const [formTags, setFormTags] = useState(tags ?? []); - const [formIsStickied, setFormIsStickied] = useCheckboxState(isStickied ?? false); - const [formShowOnHomePage, setFormShowOnHomePage] = useCheckboxState(showOnHomePage ?? true); - console.log(allTags); + console.log(post); + const [formTitle, setFormTitle] = useInputState(post?.title ?? ''); + const [formBody, setFormBody] = useInputState(post?.body ?? ''); + const [formTags, setFormTags] = useState(post?.tags ?? []); + const [formIsStickied, setFormIsStickied] = useCheckboxState(post?.isStickied ?? false); + const [formShowOnHomePage, setFormShowOnHomePage] = useCheckboxState(post?.showOnHomePage ?? true); + + const { mutate } = useMutation({ + mutationFn: createOrEditPost, + onSuccess: ({ data }) => { + window.location = data.post.url; + }, + }); + + const onSubmit = useCallback(() => { + mutate({ + id: post.id, + title: formTitle, + body: formBody, + tags: formTags, + sticky: formIsStickied, + }); + }, [formBody, formIsStickied, formTags, formTitle, mutate, post.id]); return ( <>
{header}
-
+ @@ -31,7 +50,7 @@ export default function PostForm({ - + { setFormTags(data.value); }} value={formTags} multiple /> diff --git a/app/webpacker/components/Posts/api/posts.js b/app/webpacker/components/Posts/api/posts.js new file mode 100644 index 0000000000..c81cac1086 --- /dev/null +++ b/app/webpacker/components/Posts/api/posts.js @@ -0,0 +1,13 @@ +import { fetchJsonOrError } from '../../../lib/requests/fetchWithAuthenticityToken'; +import { postUrl, submitPostUrl } from '../../../lib/requests/routes.js.erb'; + +export const createOrEditPost = (post) => { + const url = post.id ? postUrl(post.id) : submitPostUrl; + return fetchJsonOrError(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ post }), + }); +}; diff --git a/app/webpacker/lib/requests/routes.js.erb b/app/webpacker/lib/requests/routes.js.erb index b61bad7bb1..9c5efa2530 100644 --- a/app/webpacker/lib/requests/routes.js.erb +++ b/app/webpacker/lib/requests/routes.js.erb @@ -24,6 +24,10 @@ export const postsUrl = (page, format = 'json') => { return `<%= CGI.unescape(Rails.application.routes.url_helpers.posts_path(format: "${format}")) %>?page=${page}`; }; +export const submitPostUrl = `<%= CGI.unescape(Rails.application.routes.url_helpers.posts_path) %>`; + +export const postUrl = (id) => `<%= CGI.unescape(Rails.application.routes.url_helpers.post_path("${id}")) %>` + export const incidentsUrl = (perPage, page, tags = undefined, searchString = undefined, competitions = undefined, format = 'json') => { const searchParams = new URLSearchParams(`per_page=${perPage}&page=${page}`); if (tags && tags.length > 0) { From fc957405fcbac83c5bb6f831eb32c5fecfe32023 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Mon, 9 Dec 2024 16:32:32 +0100 Subject: [PATCH 03/10] start of EditPost --- app/views/posts/_post_form.html.erb | 27 ------------------- app/views/posts/edit.html.erb | 9 ++++--- app/views/posts/new.html.erb | 3 --- app/webpacker/components/Posts/CreatePost.jsx | 4 +-- app/webpacker/components/Posts/EditPost.jsx | 13 +++++++++ app/webpacker/components/Posts/PostForm.jsx | 4 +-- 6 files changed, 23 insertions(+), 37 deletions(-) delete mode 100644 app/views/posts/_post_form.html.erb create mode 100644 app/webpacker/components/Posts/EditPost.jsx diff --git a/app/views/posts/_post_form.html.erb b/app/views/posts/_post_form.html.erb deleted file mode 100644 index ef4aa72b22..0000000000 --- a/app/views/posts/_post_form.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -<% add_to_packs("markdown_editor") %> - -<% url = @post.new_record? ? posts_path : post_path(@post) %> -<%= simple_form_for @post, url: url, html: { class: 'form-horizontal' } do |f| %> - <%= f.input :title, disabled: !editable_post_fields.include?(:title), autofocus: true %> - <%= f.input :body, input_html: { class: 'markdown-editor markdown-editor-image-upload' } if editable_post_fields.include? :body %> - - <% if editable_post_fields.include? :tags %> - <%= f.input :tags %> - - <% end %> - - <%= f.input :sticky if editable_post_fields.include? :sticky %> - <% if editable_post_fields.include? :unstick_at %> - <%= f.input :unstick_at, as: :date_picker, input_html: { value: @post.unstick_at || 2.weeks.from_now.to_date } %> - <% end %> - <%= f.input :show_on_homepage if editable_post_fields.include? :show_on_homepage %> - <%= f.button :submit %> - - <% if @post.persisted? %> - <%= link_to post_path(@post.slug), method: "delete", data: { confirm: I18n.t('posts.confirm_delete_post') }, class: "btn btn-danger" do %> - Delete post - <% end %> - <% end %> -<% end %> diff --git a/app/views/posts/edit.html.erb b/app/views/posts/edit.html.erb index e76e5063a0..61e4f84211 100644 --- a/app/views/posts/edit.html.erb +++ b/app/views/posts/edit.html.erb @@ -1,7 +1,10 @@ <% provide(:title, 'Edit post') %>
-

<%= yield(:title) %>

- - <%= render "posts/post_form" %> + <%= react_component("Posts/EditPost", { + allTags: PostTag.distinct.pluck(:tag).map { |tag| { value: tag, text: tag, key: tag } }.as_json, + post: @post.as_json({ + only: %w[title sticky tags world_readable], + }), + }) %>
diff --git a/app/views/posts/new.html.erb b/app/views/posts/new.html.erb index 046b3afdd6..afa00a055d 100644 --- a/app/views/posts/new.html.erb +++ b/app/views/posts/new.html.erb @@ -3,8 +3,5 @@
<%= react_component("Posts/CreatePost", { allTags: PostTag.distinct.pluck(:tag).map { |tag| { value: tag, text: tag, key: tag } }.as_json, - post: @post.as_json({ - - }), }) %>
diff --git a/app/webpacker/components/Posts/CreatePost.jsx b/app/webpacker/components/Posts/CreatePost.jsx index 6834d9ed7f..5e00cb72de 100644 --- a/app/webpacker/components/Posts/CreatePost.jsx +++ b/app/webpacker/components/Posts/CreatePost.jsx @@ -3,11 +3,11 @@ import WCAQueryClientProvider from '../../lib/providers/WCAQueryClientProvider'; import PostForm from './PostForm'; export default function Wrapper({ - allTags, post, + allTags, }) { return ( - + ); } diff --git a/app/webpacker/components/Posts/EditPost.jsx b/app/webpacker/components/Posts/EditPost.jsx new file mode 100644 index 0000000000..97ac6abc3f --- /dev/null +++ b/app/webpacker/components/Posts/EditPost.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import WCAQueryClientProvider from '../../lib/providers/WCAQueryClientProvider'; +import PostForm from './PostForm'; + +export default function Wrapper({ + allTags, post, +}) { + return ( + + + + ); +} diff --git a/app/webpacker/components/Posts/PostForm.jsx b/app/webpacker/components/Posts/PostForm.jsx index 367fb7eb91..03e9a37ef2 100644 --- a/app/webpacker/components/Posts/PostForm.jsx +++ b/app/webpacker/components/Posts/PostForm.jsx @@ -15,8 +15,8 @@ export default function PostForm({ const [formTitle, setFormTitle] = useInputState(post?.title ?? ''); const [formBody, setFormBody] = useInputState(post?.body ?? ''); const [formTags, setFormTags] = useState(post?.tags ?? []); - const [formIsStickied, setFormIsStickied] = useCheckboxState(post?.isStickied ?? false); - const [formShowOnHomePage, setFormShowOnHomePage] = useCheckboxState(post?.showOnHomePage ?? true); + const [formIsStickied, setFormIsStickied] = useCheckboxState(post?.sticky ?? false); + const [formShowOnHomePage, setFormShowOnHomePage] = useCheckboxState(post?.world_readable ?? true); const { mutate } = useMutation({ mutationFn: createOrEditPost, From 51c56fdc2d1c1b6b204dca01946828aea0b5d35b Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Mon, 9 Dec 2024 17:20:45 +0100 Subject: [PATCH 04/10] correct edit requests --- app/controllers/posts_controller.rb | 2 +- app/views/posts/edit.html.erb | 3 ++- app/webpacker/components/Posts/PostForm.jsx | 15 +++++++++------ app/webpacker/components/Posts/api/posts.js | 3 ++- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 2fcb929330..b706b0d80a 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -80,7 +80,7 @@ def update @post = find_post if @post.update(post_params) flash[:success] = "Updated post" - redirect_to post_path(@post.slug) + render json: { status: 'ok', post: @post } else render 'edit' end diff --git a/app/views/posts/edit.html.erb b/app/views/posts/edit.html.erb index 61e4f84211..e88024ca10 100644 --- a/app/views/posts/edit.html.erb +++ b/app/views/posts/edit.html.erb @@ -4,7 +4,8 @@ <%= react_component("Posts/EditPost", { allTags: PostTag.distinct.pluck(:tag).map { |tag| { value: tag, text: tag, key: tag } }.as_json, post: @post.as_json({ - only: %w[title sticky tags world_readable], + only: ["id", "title", "sticky", "show_on_homepage"], + methods: ["tags_array"], }), }) %>
diff --git a/app/webpacker/components/Posts/PostForm.jsx b/app/webpacker/components/Posts/PostForm.jsx index 03e9a37ef2..e931a735e0 100644 --- a/app/webpacker/components/Posts/PostForm.jsx +++ b/app/webpacker/components/Posts/PostForm.jsx @@ -14,9 +14,9 @@ export default function PostForm({ console.log(post); const [formTitle, setFormTitle] = useInputState(post?.title ?? ''); const [formBody, setFormBody] = useInputState(post?.body ?? ''); - const [formTags, setFormTags] = useState(post?.tags ?? []); + const [formTags, setFormTags] = useState(post?.tags_array ?? []); const [formIsStickied, setFormIsStickied] = useCheckboxState(post?.sticky ?? false); - const [formShowOnHomePage, setFormShowOnHomePage] = useCheckboxState(post?.world_readable ?? true); + const [formShowOnHomePage, setFormShowOnHomePage] = useCheckboxState(post?.show_on_homepage ?? true); const { mutate } = useMutation({ mutationFn: createOrEditPost, @@ -32,8 +32,9 @@ export default function PostForm({ body: formBody, tags: formTags, sticky: formIsStickied, + show_on_homepage: formShowOnHomePage, }); - }, [formBody, formIsStickied, formTags, formTitle, mutate, post.id]); + }, [formBody, formIsStickied, formShowOnHomePage, formTags, formTitle, mutate, post.slug]); return ( <> @@ -53,13 +54,15 @@ export default function PostForm({ { setFormTags(data.value); }} value={formTags} multiple /> - + - +

Careful! This is not secure for private data. This is only to prevent cluttering the homepage. Posts that are not shown on the homepage are still accessible to the public via permalink or through tags.

- + + { post + && } ); diff --git a/app/webpacker/components/Posts/api/posts.js b/app/webpacker/components/Posts/api/posts.js index c81cac1086..e45cd1eac1 100644 --- a/app/webpacker/components/Posts/api/posts.js +++ b/app/webpacker/components/Posts/api/posts.js @@ -3,8 +3,9 @@ import { postUrl, submitPostUrl } from '../../../lib/requests/routes.js.erb'; export const createOrEditPost = (post) => { const url = post.id ? postUrl(post.id) : submitPostUrl; + const method = post.id ? 'PATCH' : 'POST'; return fetchJsonOrError(url, { - method: 'POST', + method, headers: { 'Content-Type': 'application/json', }, From 0e6b5baaa55c82d4098d5897c1de2e0d8ab96ab6 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Mon, 9 Dec 2024 17:41:30 +0100 Subject: [PATCH 05/10] add deletion --- app/controllers/posts_controller.rb | 2 +- app/webpacker/components/Posts/EditPost.jsx | 5 +- app/webpacker/components/Posts/PostForm.jsx | 72 +++++++++++++++++---- app/webpacker/components/Posts/api/posts.js | 30 +++++---- 4 files changed, 83 insertions(+), 26 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index b706b0d80a..9c502fab34 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -90,7 +90,7 @@ def destroy @post = find_post @post.destroy flash[:success] = "Deleted post" - redirect_to root_url + render json: { status: 'ok' } end private def editable_post_fields diff --git a/app/webpacker/components/Posts/EditPost.jsx b/app/webpacker/components/Posts/EditPost.jsx index 97ac6abc3f..3708a713eb 100644 --- a/app/webpacker/components/Posts/EditPost.jsx +++ b/app/webpacker/components/Posts/EditPost.jsx @@ -1,13 +1,16 @@ import React from 'react'; import WCAQueryClientProvider from '../../lib/providers/WCAQueryClientProvider'; import PostForm from './PostForm'; +import ConfirmProvider from '../../lib/providers/ConfirmProvider'; export default function Wrapper({ allTags, post, }) { return ( - + + + ); } diff --git a/app/webpacker/components/Posts/PostForm.jsx b/app/webpacker/components/Posts/PostForm.jsx index e931a735e0..771635149b 100644 --- a/app/webpacker/components/Posts/PostForm.jsx +++ b/app/webpacker/components/Posts/PostForm.jsx @@ -6,35 +6,81 @@ import { useMutation } from '@tanstack/react-query'; import useInputState from '../../lib/hooks/useInputState'; import useCheckboxState from '../../lib/hooks/useCheckboxState'; import MarkdownEditor from '../wca/FormBuilder/input/MarkdownEditor'; -import { createOrEditPost } from './api/posts'; +import { createPost, deletePost, editPost } from './api/posts'; +import { useConfirm } from '../../lib/providers/ConfirmProvider'; export default function PostForm({ header, allTags, post, }) { - console.log(post); const [formTitle, setFormTitle] = useInputState(post?.title ?? ''); const [formBody, setFormBody] = useInputState(post?.body ?? ''); const [formTags, setFormTags] = useState(post?.tags_array ?? []); const [formIsStickied, setFormIsStickied] = useCheckboxState(post?.sticky ?? false); const [formShowOnHomePage, setFormShowOnHomePage] = useCheckboxState(post?.show_on_homepage ?? true); - const { mutate } = useMutation({ - mutationFn: createOrEditPost, + const confirm = useConfirm(); + + const { mutate: createMutation } = useMutation({ + mutationFn: createPost, + onSuccess: ({ data }) => { + window.location = data.post.url; + }, + }); + + const { mutate: editMutation } = useMutation({ + mutationFn: editPost, onSuccess: ({ data }) => { window.location = data.post.url; }, }); + const { mutate: deleteMutation } = useMutation({ + mutationFn: deletePost, + onSuccess: () => { + window.location = '/posts'; + }, + }); + const onSubmit = useCallback(() => { - mutate({ - id: post.id, - title: formTitle, - body: formBody, - tags: formTags, - sticky: formIsStickied, - show_on_homepage: formShowOnHomePage, + if (post.id) { + editMutation({ + id: post.id, + title: formTitle, + body: formBody, + tags: formTags, + sticky: formIsStickied, + show_on_homepage: formShowOnHomePage, + }); + } else { + createMutation({ + title: formTitle, + body: formBody, + tags: formTags, + sticky: formIsStickied, + show_on_homepage: formShowOnHomePage, + }); + } + }, [ + createMutation, + editMutation, + formBody, + formIsStickied, + formShowOnHomePage, + formTags, + formTitle, + post.id, + ]); + + const deletePostAttempt = useCallback((event) => { + event.preventDefault(); + confirm({ + content: 'Do you want to delete this post?', + }).then(() => { + deleteMutation({ + id: post.id, + }); }); - }, [formBody, formIsStickied, formShowOnHomePage, formTags, formTitle, mutate, post.slug]); + }); return ( <> @@ -62,7 +108,7 @@ export default function PostForm({ { post - && } + && } ); diff --git a/app/webpacker/components/Posts/api/posts.js b/app/webpacker/components/Posts/api/posts.js index e45cd1eac1..7f278dc3dd 100644 --- a/app/webpacker/components/Posts/api/posts.js +++ b/app/webpacker/components/Posts/api/posts.js @@ -1,14 +1,22 @@ import { fetchJsonOrError } from '../../../lib/requests/fetchWithAuthenticityToken'; import { postUrl, submitPostUrl } from '../../../lib/requests/routes.js.erb'; -export const createOrEditPost = (post) => { - const url = post.id ? postUrl(post.id) : submitPostUrl; - const method = post.id ? 'PATCH' : 'POST'; - return fetchJsonOrError(url, { - method, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ post }), - }); -}; +export const createPost = (post) => fetchJsonOrError(submitPostUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ post }), +}); + +export const editPost = (post) => fetchJsonOrError(postUrl(post.id), { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ post }), +}); + +export const deletePost = (post) => fetchJsonOrError(postUrl(post.id), { + method: 'DELETE', +}); From 4ba7101787599d70be2c4accf54f5d38f6e3226c Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Tue, 10 Dec 2024 11:26:29 +0100 Subject: [PATCH 06/10] fix post widget on the homepage --- app/views/posts/homepage.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/homepage.html.erb b/app/views/posts/homepage.html.erb index c806e4eb48..a77cea9cd3 100644 --- a/app/views/posts/homepage.html.erb +++ b/app/views/posts/homepage.html.erb @@ -40,7 +40,7 @@

<%= t('homepage.announcements') %>

- <%= react_component("PostsWidget", { + <%= react_component("Posts/PostsWidget", { titleOnly: true, }, { id: "posts_widget", From 62533e3784149b96a24b8b468305d6c494c474f9 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Tue, 10 Dec 2024 12:24:33 +0100 Subject: [PATCH 07/10] implement date picker --- app/views/posts/edit.html.erb | 2 +- app/webpacker/components/Posts/PostForm.jsx | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/views/posts/edit.html.erb b/app/views/posts/edit.html.erb index e88024ca10..28bf2d0f3e 100644 --- a/app/views/posts/edit.html.erb +++ b/app/views/posts/edit.html.erb @@ -4,7 +4,7 @@ <%= react_component("Posts/EditPost", { allTags: PostTag.distinct.pluck(:tag).map { |tag| { value: tag, text: tag, key: tag } }.as_json, post: @post.as_json({ - only: ["id", "title", "sticky", "show_on_homepage"], + only: ["id", "title", "sticky", "show_on_homepage", "unstick_at"], methods: ["tags_array"], }), }) %> diff --git a/app/webpacker/components/Posts/PostForm.jsx b/app/webpacker/components/Posts/PostForm.jsx index 771635149b..3816a0cb2b 100644 --- a/app/webpacker/components/Posts/PostForm.jsx +++ b/app/webpacker/components/Posts/PostForm.jsx @@ -3,11 +3,13 @@ import { Button, Form, FormField, Header, } from 'semantic-ui-react'; import { useMutation } from '@tanstack/react-query'; +import DatePicker from 'react-datepicker'; import useInputState from '../../lib/hooks/useInputState'; import useCheckboxState from '../../lib/hooks/useCheckboxState'; import MarkdownEditor from '../wca/FormBuilder/input/MarkdownEditor'; import { createPost, deletePost, editPost } from './api/posts'; import { useConfirm } from '../../lib/providers/ConfirmProvider'; +import UtcDatePicker from '../wca/UtcDatePicker'; export default function PostForm({ header, allTags, post, @@ -17,6 +19,7 @@ export default function PostForm({ const [formTags, setFormTags] = useState(post?.tags_array ?? []); const [formIsStickied, setFormIsStickied] = useCheckboxState(post?.sticky ?? false); const [formShowOnHomePage, setFormShowOnHomePage] = useCheckboxState(post?.show_on_homepage ?? true); + const [unstickAt, setUnstickAt] = useState(post.unstick_at ?? null); const confirm = useConfirm(); @@ -48,6 +51,7 @@ export default function PostForm({ title: formTitle, body: formBody, tags: formTags, + unstick_at: formIsStickied ? unstickAt : null, sticky: formIsStickied, show_on_homepage: formShowOnHomePage, }); @@ -57,6 +61,7 @@ export default function PostForm({ body: formBody, tags: formTags, sticky: formIsStickied, + unstick_at: formIsStickied ? unstickAt : null, show_on_homepage: formShowOnHomePage, }); } @@ -68,6 +73,7 @@ export default function PostForm({ formShowOnHomePage, formTags, formTitle, + unstickAt, post.id, ]); @@ -80,7 +86,7 @@ export default function PostForm({ id: post.id, }); }); - }); + }, [confirm, deleteMutation, post.id]); return ( <> @@ -101,6 +107,13 @@ export default function PostForm({ + { formIsStickied + && ( + setUnstickAt(date)} + /> + ) } From a38105032e534449d79080b56da7f537bacdafc6 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Tue, 10 Dec 2024 12:37:27 +0100 Subject: [PATCH 08/10] internationalize Post Form --- app/webpacker/components/Posts/PostForm.jsx | 19 +++++++++++-------- config/i18n.yml | 2 ++ config/locales/en.yml | 4 ---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/webpacker/components/Posts/PostForm.jsx b/app/webpacker/components/Posts/PostForm.jsx index 3816a0cb2b..f400d7b26b 100644 --- a/app/webpacker/components/Posts/PostForm.jsx +++ b/app/webpacker/components/Posts/PostForm.jsx @@ -3,13 +3,14 @@ import { Button, Form, FormField, Header, } from 'semantic-ui-react'; import { useMutation } from '@tanstack/react-query'; -import DatePicker from 'react-datepicker'; +import I18n from '../../lib/i18n'; import useInputState from '../../lib/hooks/useInputState'; import useCheckboxState from '../../lib/hooks/useCheckboxState'; import MarkdownEditor from '../wca/FormBuilder/input/MarkdownEditor'; import { createPost, deletePost, editPost } from './api/posts'; import { useConfirm } from '../../lib/providers/ConfirmProvider'; import UtcDatePicker from '../wca/UtcDatePicker'; +import I18nHTMLTranslate from '../I18nHTMLTranslate'; export default function PostForm({ header, allTags, post, @@ -95,29 +96,31 @@ export default function PostForm({
- + - + + {/* i18n-tasks-use t('simple_form.hints.post.body') */} + - - { setFormTags(data.value); }} value={formTags} multiple /> + { setFormTags(data.value); }} value={formTags} multiple /> - + { formIsStickied && ( setUnstickAt(date)} /> ) } - -

Careful! This is not secure for private data. This is only to prevent cluttering the homepage. Posts that are not shown on the homepage are still accessible to the public via permalink or through tags.

+ +

{I18n.t('simple_form.hints.post.show_on_homepage')}

{ post diff --git a/config/i18n.yml b/config/i18n.yml index d43221c507..0d80f07e52 100644 --- a/config/i18n.yml +++ b/config/i18n.yml @@ -74,3 +74,5 @@ translations: - "*.attempts.*" - "*.time_limit.*" - "*.users.edit.*" + - "*.activerecord.attributes.post.*" + - "*.simple_form.hints.post.*" diff --git a/config/locales/en.yml b/config/locales/en.yml index ecebb907af..02995bd589 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -489,10 +489,6 @@ en: #context: for a post post: body: "Use the <!-- break --> tag to show a preview on the home page. Any post on the home page will show all text before this divider. The actual post will display the full body." - sticky: "" - unstick_at: "" - title: "" - tags: "" show_on_homepage: "Careful! This is not secure for private data. This is only to prevent cluttering the homepage. Posts that are not shown on the homepage are still accessible to the public via permalink or through tags." #context: for a delegate report delegate_report: From 7208373d0cabfeac22610301fa5c47b6db93f004 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Tue, 10 Dec 2024 17:39:12 +0100 Subject: [PATCH 09/10] handle errors --- app/controllers/posts_controller.rb | 4 +- app/webpacker/components/Posts/PostForm.jsx | 68 +++++++++++++++------ 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 9c502fab34..4c10610f32 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -68,7 +68,7 @@ def create flash[:success] = "Created new post" render json: { status: 'ok', post: @post } else - render 'new' + render json: { status: 'validation failed', errors: @post.errors }, status: :bad_request end end @@ -82,7 +82,7 @@ def update flash[:success] = "Updated post" render json: { status: 'ok', post: @post } else - render 'edit' + render json: { status: 'validation failed', errors: @post.errors }, status: :bad_request end end diff --git a/app/webpacker/components/Posts/PostForm.jsx b/app/webpacker/components/Posts/PostForm.jsx index f400d7b26b..f58779bc7a 100644 --- a/app/webpacker/components/Posts/PostForm.jsx +++ b/app/webpacker/components/Posts/PostForm.jsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react'; import { - Button, Form, FormField, Header, + Button, Form, FormField, Header, Message, } from 'semantic-ui-react'; import { useMutation } from '@tanstack/react-query'; import I18n from '../../lib/i18n'; @@ -15,29 +15,33 @@ import I18nHTMLTranslate from '../I18nHTMLTranslate'; export default function PostForm({ header, allTags, post, }) { - const [formTitle, setFormTitle] = useInputState(post?.title ?? ''); - const [formBody, setFormBody] = useInputState(post?.body ?? ''); - const [formTags, setFormTags] = useState(post?.tags_array ?? []); - const [formIsStickied, setFormIsStickied] = useCheckboxState(post?.sticky ?? false); - const [formShowOnHomePage, setFormShowOnHomePage] = useCheckboxState(post?.show_on_homepage ?? true); - const [unstickAt, setUnstickAt] = useState(post.unstick_at ?? null); + const [formPost, setFormPost] = useState(post); + const [formTitle, setFormTitle] = useInputState(formPost?.title ?? ''); + const [formBody, setFormBody] = useInputState(formPost?.body ?? ''); + const [formTags, setFormTags] = useState(formPost?.tags_array ?? []); + const [formIsStickied, setFormIsStickied] = useCheckboxState(formPost?.sticky ?? false); + const [formShowOnHomePage, setFormShowOnHomePage] = useCheckboxState(formPost?.show_on_homepage ?? true); + const [unstickAt, setUnstickAt] = useState(formPost?.unstick_at ?? null); const confirm = useConfirm(); - const { mutate: createMutation } = useMutation({ + const { mutate: createMutation, isSuccess: postCreated, error: createError } = useMutation({ mutationFn: createPost, onSuccess: ({ data }) => { - window.location = data.post.url; + setFormPost(data.post); + window.history.replaceState({}, '', `${data.post.url}/edit`); }, }); - const { mutate: editMutation } = useMutation({ + const { mutate: editMutation, error: editError, isSuccess: postUpdated } = useMutation({ mutationFn: editPost, onSuccess: ({ data }) => { - window.location = data.post.url; + setFormPost(data.post); }, }); + const { errors } = (createError?.json || editError?.json || {}); + const { mutate: deleteMutation } = useMutation({ mutationFn: deletePost, onSuccess: () => { @@ -46,9 +50,9 @@ export default function PostForm({ }); const onSubmit = useCallback(() => { - if (post.id) { + if (formPost?.id) { editMutation({ - id: post.id, + id: formPost.id, title: formTitle, body: formBody, tags: formTags, @@ -75,7 +79,7 @@ export default function PostForm({ formTags, formTitle, unstickAt, - post.id, + formPost?.id, ]); const deletePostAttempt = useCallback((event) => { @@ -84,10 +88,10 @@ export default function PostForm({ content: 'Do you want to delete this post?', }).then(() => { deleteMutation({ - id: post.id, + id: formPost.id, }); }); - }, [confirm, deleteMutation, post.id]); + }, [confirm, deleteMutation, formPost?.id]); return ( <> @@ -122,8 +126,36 @@ export default function PostForm({

{I18n.t('simple_form.hints.post.show_on_homepage')}

- - { post + { errors && ( + + Request Failed: + + {(Object.keys(errors).map((err) => ( + + {err} + {' '} + {errors[err].join(',')} + + )))} + + + )} + { postCreated && ( + + Post successfully created. View it + {' '} + here + + )} + { postUpdated && ( + + Post successfully updated. View it + {' '} + here + + )} + + { formPost && } From a0b2cb3a472839b339d19e5e4de4e33733ef593c Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Wed, 11 Dec 2024 10:31:06 +0100 Subject: [PATCH 10/10] don't allow deleting from post form --- app/controllers/posts_controller.rb | 2 +- app/webpacker/components/Posts/PostForm.jsx | 22 --------------------- app/webpacker/components/Posts/api/posts.js | 4 ---- 3 files changed, 1 insertion(+), 27 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 4c10610f32..a6ac7dd09a 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -90,7 +90,7 @@ def destroy @post = find_post @post.destroy flash[:success] = "Deleted post" - render json: { status: 'ok' } + redirect_to root_url end private def editable_post_fields diff --git a/app/webpacker/components/Posts/PostForm.jsx b/app/webpacker/components/Posts/PostForm.jsx index f58779bc7a..21dbd77187 100644 --- a/app/webpacker/components/Posts/PostForm.jsx +++ b/app/webpacker/components/Posts/PostForm.jsx @@ -23,8 +23,6 @@ export default function PostForm({ const [formShowOnHomePage, setFormShowOnHomePage] = useCheckboxState(formPost?.show_on_homepage ?? true); const [unstickAt, setUnstickAt] = useState(formPost?.unstick_at ?? null); - const confirm = useConfirm(); - const { mutate: createMutation, isSuccess: postCreated, error: createError } = useMutation({ mutationFn: createPost, onSuccess: ({ data }) => { @@ -42,13 +40,6 @@ export default function PostForm({ const { errors } = (createError?.json || editError?.json || {}); - const { mutate: deleteMutation } = useMutation({ - mutationFn: deletePost, - onSuccess: () => { - window.location = '/posts'; - }, - }); - const onSubmit = useCallback(() => { if (formPost?.id) { editMutation({ @@ -82,17 +73,6 @@ export default function PostForm({ formPost?.id, ]); - const deletePostAttempt = useCallback((event) => { - event.preventDefault(); - confirm({ - content: 'Do you want to delete this post?', - }).then(() => { - deleteMutation({ - id: formPost.id, - }); - }); - }, [confirm, deleteMutation, formPost?.id]); - return ( <>
@@ -155,8 +135,6 @@ export default function PostForm({ )} - { formPost - && } ); diff --git a/app/webpacker/components/Posts/api/posts.js b/app/webpacker/components/Posts/api/posts.js index 7f278dc3dd..af37d74b56 100644 --- a/app/webpacker/components/Posts/api/posts.js +++ b/app/webpacker/components/Posts/api/posts.js @@ -16,7 +16,3 @@ export const editPost = (post) => fetchJsonOrError(postUrl(post.id), { }, body: JSON.stringify({ post }), }); - -export const deletePost = (post) => fetchJsonOrError(postUrl(post.id), { - method: 'DELETE', -});