enable deletion of post attachments

This commit is contained in:
riggraz
2025-02-04 14:29:33 +01:00
parent acd9d598e2
commit ec0e59be2d
8 changed files with 125 additions and 54 deletions

View File

@@ -398,5 +398,11 @@ body {
height: 100%;
}
}
&.thumbnailToDelete {
border: 2px solid red;
.thumbnailInner { filter: grayscale(100%); }
}
}
}

View File

@@ -184,7 +184,7 @@
}
.postEditFormButtons {
@extend .d-flex, .justify-content-end;
@extend .d-flex, .justify-content-end, .mt-3;
}
#selectPickerBoard { margin-right: 4px !important; }

View File

@@ -101,7 +101,7 @@ class PostsController < ApplicationController
respond_to do |format|
format.html
format.json { render json: @post.as_json.merge(attachment_urls: @post.attachments.map { |attachment| attachment.blob.url }) }
format.json { render json: @post.as_json.merge(attachment_urls: @post.attachments.order(:created_at).map { |attachment| attachment.blob.url }) }
end
end
@@ -111,6 +111,13 @@ class PostsController < ApplicationController
@post.assign_attributes(post_update_params)
# handle attachment deletion
if params[:post][:attachments_to_delete].present? && @post.attachments.attached?
@post.attachments.order(:created_at).each_with_index do |attachment, index|
attachment.purge if params[:post][:attachments_to_delete].include?(index)
end
end
if @post.save
if @post.post_status_id_previously_changed?
ExecutePostStatusChangeLogicWorkflow.new(
@@ -120,7 +127,7 @@ class PostsController < ApplicationController
).run
end
render json: @post
render json: @post.as_json.merge(attachment_urls: @post.attachments.order(:created_at).map { |attachment| attachment.blob.url })
else
render json: {
error: @post.errors.full_messages
@@ -198,6 +205,10 @@ class PostsController < ApplicationController
def post_update_params
params
.require(:post)
.permit(policy(@post).permitted_attributes_for_update)
.permit(
policy(@post)
.permitted_attributes_for_update
.concat([{ additional_params: [:attachments_to_delete] }])
)
end
end

View File

@@ -50,6 +50,7 @@ export const updatePost = (
description: string,
boardId: number,
postStatusId: number,
attachmentsToDelete: number[],
authenticityToken: string,
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
dispatch(postUpdateStart());
@@ -64,7 +65,8 @@ export const updatePost = (
description,
board_id: boardId,
post_status_id: postStatusId,
}
attachments_to_delete: attachmentsToDelete,
},
}),
});
const json = await res.json();

View File

@@ -9,13 +9,14 @@ import IBoard from '../../interfaces/IBoard';
import Button from '../common/Button';
import Spinner from '../common/Spinner';
import ActionLink from '../common/ActionLink';
import { CancelIcon } from '../common/Icons';
import { CancelIcon, DeleteIcon } from '../common/Icons';
interface Props {
title: string;
description?: string;
boardId: number;
postStatusId?: number;
attachmentUrls?: string[];
isUpdating: boolean;
error: string;
@@ -35,6 +36,7 @@ interface Props {
description: string,
boardId: number,
postStatusId: number,
attachmentsToDelete: number[],
): void;
}
@@ -43,6 +45,7 @@ const PostEditForm = ({
description,
boardId,
postStatusId,
attachmentUrls,
isUpdating,
error,
@@ -58,53 +61,97 @@ const PostEditForm = ({
toggleEditMode,
handleUpdatePost,
}: Props) => (
<div className="postEditForm">
<div className="postHeader">
<input
type="text"
value={title}
onChange={e => handleChangeTitle(e.target.value)}
autoFocus
className="postTitle form-control"
}: Props) => {
const [attachmentsToDelete, setAttachmentsToDelete] = React.useState<number[]>([]);
console.log('attachmentsToDelete', attachmentsToDelete);
return (
<div className="postEditForm">
<div className="postHeader">
<input
type="text"
value={title}
onChange={e => handleChangeTitle(e.target.value)}
autoFocus
className="postTitle form-control"
/>
</div>
{
isPowerUser ?
<div className="postSettings">
<PostBoardSelect
boards={boards}
selectedBoardId={boardId}
handleChange={newBoardId => handleChangeBoard(newBoardId)}
/>
<PostStatusSelect
postStatuses={postStatuses}
selectedPostStatusId={postStatusId}
handleChange={newPostStatusId => handleChangePostStatus(newPostStatusId)}
/>
</div>
:
null
}
<textarea
value={description}
onChange={e => handleChangeDescription(e.target.value)}
rows={5}
className="postDescription form-control"
/>
{ /* Attachments */ }
<div className="thumbnailsContainer">
{
attachmentUrls && attachmentUrls.map((attachmentUrl, i) => (
<div className="thumbnailContainer" key={i}>
<div className={`thumbnail${attachmentsToDelete.includes(i) ? ' thumbnailToDelete' : ''}`}>
<div className="thumbnailInner">
<img
src={attachmentUrl}
className="thumbnailImage"
/>
</div>
</div>
{
attachmentsToDelete.includes(i) ?
<ActionLink
onClick={() => setAttachmentsToDelete(attachmentsToDelete.filter(index => index !== i))}
icon={<CancelIcon />}
customClass="removeThumbnail"
>
{I18n.t('common.buttons.cancel')}
</ActionLink>
:
<ActionLink
onClick={() => setAttachmentsToDelete([...attachmentsToDelete, i])}
icon={<DeleteIcon />}
customClass="removeThumbnail"
>
{I18n.t('common.buttons.delete')}
</ActionLink>
}
</div>
))
}
</div>
<div className="postEditFormButtons">
<ActionLink onClick={toggleEditMode} icon={<CancelIcon />}>
{I18n.t('common.buttons.cancel')}
</ActionLink>
&nbsp;
<Button onClick={() => handleUpdatePost(title, description, boardId, postStatusId, attachmentsToDelete)}>
{ isUpdating ? <Spinner /> : I18n.t('common.buttons.update') }
</Button>
</div>
</div>
{
isPowerUser ?
<div className="postSettings">
<PostBoardSelect
boards={boards}
selectedBoardId={boardId}
handleChange={newBoardId => handleChangeBoard(newBoardId)}
/>
<PostStatusSelect
postStatuses={postStatuses}
selectedPostStatusId={postStatusId}
handleChange={newPostStatusId => handleChangePostStatus(newPostStatusId)}
/>
</div>
:
null
}
<textarea
value={description}
onChange={e => handleChangeDescription(e.target.value)}
rows={5}
className="postDescription form-control"
/>
<div className="postEditFormButtons">
<ActionLink onClick={toggleEditMode} icon={<CancelIcon />}>
{I18n.t('common.buttons.cancel')}
</ActionLink>
&nbsp;
<Button onClick={() => handleUpdatePost(title, description, boardId, postStatusId)}>
{ isUpdating ? <Spinner /> : I18n.t('common.buttons.update') }
</Button>
</div>
</div>
);
);
};
export default PostEditForm;

View File

@@ -59,6 +59,7 @@ interface Props {
description: string,
boardId: number,
postStatusId: number,
attachmentsToDelete: number[],
authenticityToken: string,
): Promise<any>;
@@ -114,7 +115,7 @@ class PostP extends React.Component<Props, State> {
this.props.requestPostStatusChanges(postId);
}
_handleUpdatePost(title: string, description: string, boardId: number, postStatusId: number) {
_handleUpdatePost(title: string, description: string, boardId: number, postStatusId: number, attachmentsToDelete: number[]) {
const {
postId,
post,
@@ -134,6 +135,7 @@ class PostP extends React.Component<Props, State> {
description,
boardId,
postStatusId,
attachmentsToDelete,
authenticityToken,
).then(res => {
if (res?.status !== HttpStatus.OK) return;
@@ -235,6 +237,7 @@ class PostP extends React.Component<Props, State> {
editMode ?
<PostEditForm
{...editForm}
attachmentUrls={postToShow.attachmentUrls}
handleChangeTitle={handleChangeEditFormTitle}
handleChangeDescription={handleChangeEditFormDescription}

View File

@@ -44,9 +44,10 @@ const mapDispatchToProps = (dispatch) => ({
description: string,
boardId: number,
postStatusId: number,
attachmentsToDelete: number[],
authenticityToken: string,
) {
return dispatch(updatePost(postId, title, description, boardId, postStatusId, authenticityToken));
return dispatch(updatePost(postId, title, description, boardId, postStatusId, attachmentsToDelete, authenticityToken));
},
toggleEditMode() {

View File

@@ -63,6 +63,7 @@ const postReducer = (
...state,
title: action.post.title,
description: action.post.description,
attachmentUrls: action.post.attachment_urls,
boardId: action.post.board_id,
postStatusId: action.post.post_status_id,
approvalStatus: action.post.approval_status,