Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Remove attachments without deleting submissions #4984

Draft
wants to merge 31 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6bf51b5
create AttachmentActionsDropdown component
magicznyleszek Jun 24, 2024
122588b
Merge branch 'task-810-replace-deprecated-buttons' into feature/delet…
magicznyleszek Jun 24, 2024
2c5faed
Merge branch 'feature/delete-attachment' into feature_delete-attachme…
magicznyleszek Jun 24, 2024
d04cedb
cleanup data table audio player cell styles
magicznyleszek Jun 24, 2024
4b82d6e
cleanup KoboSelect option styles
magicznyleszek Jun 25, 2024
41792be
cleanup KoboModal default size
magicznyleszek Jun 25, 2024
2a63146
disable deleting in AttachmentActionsDropdown if insufficient permiss…
magicznyleszek Jun 25, 2024
170e4ad
Merge branch 'beta' into feature/delete-attachment
magicznyleszek Jun 25, 2024
1e7e2c8
Merge branch 'feature/delete-attachment' into feature_delete-attachme…
magicznyleszek Jun 25, 2024
9688352
fix bg audio url flow in the modal
magicznyleszek Jun 27, 2024
41372f9
Merge branch 'beta' into feature/delete-attachment
magicznyleszek Jul 2, 2024
835ff2f
Merge branch 'feature/delete-attachment' into feature_delete-attachme…
magicznyleszek Jul 2, 2024
0c32b6e
Merge branch 'beta' into feature/delete-attachment
magicznyleszek Jul 2, 2024
1a30fc1
Merge branch 'feature/delete-attachment' into feature_delete-attachme…
magicznyleszek Jul 2, 2024
fee53d3
use better permissions checking in AttachmentActionsDropdown
magicznyleszek Jul 4, 2024
191f798
Merge branch 'cleanup-submission-modal' into feature/delete-attachment
magicznyleszek Jul 12, 2024
7642be8
Merge branch 'feature/delete-attachment' into feature_delete-attachme…
magicznyleszek Jul 12, 2024
fb1dc83
Merge branch 'beta' into feature/delete-attachment
magicznyleszek Jul 17, 2024
dc74df1
Merge branch 'feature/delete-attachment' into feature_delete-attachme…
magicznyleszek Jul 17, 2024
5cd305b
Merge branch 'beta' into feature/delete-attachment
magicznyleszek Jul 22, 2024
f4b75c5
Merge branch 'feature/delete-attachment' into feature_delete-attachme…
magicznyleszek Jul 22, 2024
2365f3c
post merge fixes
magicznyleszek Jul 22, 2024
a5eb006
handle is_deleted attachments properly (WIP)
magicznyleszek Jul 24, 2024
f76eb20
add is_deleted flag to interface
magicznyleszek Jul 24, 2024
3352d8b
add missing imports
magicznyleszek Jul 24, 2024
5c79d58
Merge branch 'beta' into feature/delete-attachment
magicznyleszek Jul 26, 2024
01964f9
Merge branch 'feature/delete-attachment' into feature_delete-attachme…
magicznyleszek Jul 26, 2024
409ca97
use proper permission
magicznyleszek Jul 30, 2024
1f3e375
Merge pull request #4995 from kobotoolbox/feature_delete-attachment_t…
jamesrkiger Aug 1, 2024
55c71c7
Merge branch 'beta' into feature/delete-attachment
magicznyleszek Sep 25, 2024
ce92a82
Merge branch 'main' into feature/delete-attachment
magicznyleszek Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions jsapp/js/components/bigModal/bigModal.es6
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ function getSubmissionTitle(props) {
*
* There are also two other important methods: `hideModal` and `switchModal`.
*
* The modal rendering is being handled by `app.jsx`.
*
* @prop {object} params - to be passed to the custom modal component
*/
class BigModal extends React.Component {
Expand Down
5 changes: 3 additions & 2 deletions jsapp/js/components/common/koboSelect.scss
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,11 @@ $k-select-menu-padding: 6px;
@include mixins.buttonReset;
font-weight: 400;
position: relative;
color: colors.$kobo-dark-blue;

&:hover,
&.k-select__option--selected {
color: colors.$kobo-gray-800;
color: colors.$kobo-dark-blue;
background-color: colors.$kobo-gray-200;
}

Expand All @@ -114,14 +115,14 @@ $k-select-menu-padding: 6px;
justify-content: space-between;
width: 100%;
height: $k-select-option-height;
color: colors.$kobo-gray-700;
padding: 0 #{16px - 2px};
text-align: initial;
}

.k-select__menu-message {
font-style: italic;
margin: 0;
color: colors.$kobo-gray-24;
}

.k-select__error {
Expand Down
12 changes: 11 additions & 1 deletion jsapp/js/components/formGallery/formGallery.component.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@use 'scss/colors';
@use 'scss/breakpoints';
@use 'scss/mixins';

.gallery {
background-color: colors.$kobo-white;
Expand Down Expand Up @@ -47,12 +48,21 @@
margin-right: 5px;
}

.gallery-grid {
.gallery__grid {
display: flex;
flex-wrap: wrap;
gap: 3px;
}

.gallery-grid-deleted-attachment {
@include mixins.centerRowFlex;
justify-content: center;
min-width: 100px;
min-height: 100px;
border-radius: 6px;
background-color: colors.$kobo-gray-92;
}

@include breakpoints.breakpoint(mediumAndUp) {
.gallery__wrapper {
margin: 20px 50px;
Expand Down
39 changes: 25 additions & 14 deletions jsapp/js/components/formGallery/formGallery.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
selectImageAttachments,
selectShowLoadMore,
} from './formGallery.selectors';
import DeletedAttachment from 'js/components/submissions/deletedAttachment.component';

bem.Gallery = makeBem(null, 'gallery');
bem.Gallery__wrapper = makeBem(bem.Gallery, 'wrapper');
Expand Down Expand Up @@ -159,20 +160,30 @@ export default function FormGallery(props: FormGalleryProps) {
</bem.GalleryFiltersDates>
</bem.GalleryFilters>
<bem.GalleryGrid>
{attachments.map((attachment) => (
<a
key={attachment.id}
href={attachment.download_url}
target='_blank'
>
<img
src={attachment.download_small_url}
alt={attachment.filename}
width='150'
loading='lazy'
/>
</a>
))}
{attachments.map((attachment) => {
if (attachment.is_deleted) {
return (
<span className='gallery-grid-deleted-attachment'>
<DeletedAttachment />
</span>
);
} else {
return (
<a
key={attachment.id}
href={attachment.download_url}
target='_blank'
>
<img
src={attachment.download_url}
alt={attachment.filename}
width='150'
loading='lazy'
/>
</a>
)
}
})}
</bem.GalleryGrid>
{showLoadMore && (
<bem.GalleryFooter>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
@use 'scss/colors';

.mediaWrapper {
border-radius: sizes.$x6;
background-color: colors.$kobo-white;

&.mediaWrapperDeleted {
padding: 14px;
}

&.mediaWrapperAudio {
background-color: colors.$kobo-white;
border-radius: 6px;
// right padding is for the audio player to appear centered
padding: 20px #{20px + 12px} 14px 20px;
}

&.mediaWrapperVideo {
border-radius: 6px;
overflow: hidden;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, {useState} from 'react';
import cx from 'classnames';
import AudioPlayer from 'js/components/common/audioPlayer';
import singleProcessingStore from 'js/components/processing/singleProcessingStore';
import type {AssetContent} from 'js/dataInterface';
import {QUESTION_TYPES} from 'js/constants';
import {getAttachmentForProcessing} from 'js/components/processing/transcript/transcript.utils';
import styles from './sidebarSubmissionMedia.module.scss';
import DeletedAttachment from 'js/components/submissions/deletedAttachment.component';

interface SidebarSubmissionMediaProps {
assetContent: AssetContent | undefined;
Expand All @@ -26,15 +28,23 @@ export default function SidebarSubmissionMedia(
return null;
}

if (attachment.is_deleted) {
return (
<section
className={cx([styles.mediaWrapper, styles.mediaWrapperDeleted])}
key='deleted'
>
<DeletedAttachment />
</section>
);
}

switch (store.currentQuestionType) {
case QUESTION_TYPES.audio.id:
case QUESTION_TYPES['background-audio'].id:
return (
<section
className={`
${styles.mediaWrapper}
${styles.mediaWrapperAudio}
`}
className={cx([styles.mediaWrapper, styles.mediaWrapperAudio])}
key='audio'
>
<AudioPlayer
Expand All @@ -46,10 +56,7 @@ export default function SidebarSubmissionMedia(
case QUESTION_TYPES.video.id:
return (
<section
className={`
${styles.mediaWrapper}
${styles.mediaWrapperVideo}
`}
className={cx([styles.mediaWrapper, styles.mediaWrapperVideo])}
key='video'
>
<video
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Libraries:
import React, {useState} from 'react';
// Components:
import Icon from 'js/components/common/icon';
import Button from 'js/components/common/button';
import KoboDropdown from 'js/components/common/koboDropdown';
import KoboModal from 'js/components/modals/koboModal';
import KoboModalHeader from 'js/components/modals/koboModalHeader';
import KoboModalContent from 'js/components/modals/koboModalContent';
import KoboModalFooter from 'js/components/modals/koboModalFooter';
import bem from 'js/bem';
// Constants
import {QuestionTypeName, MetaQuestionTypeName} from 'js/constants';
// Helpers:
import * as utils from 'js/utils';
import {userHasPermForSubmission} from 'js/components/permissions/utils';
// Types:
import type {AnyRowTypeName} from 'js/constants';
import type {AssetResponse, SubmissionResponse} from 'js/dataInterface';

interface AttachmentActionsDropdownProps {
asset: AssetResponse;
questionType: AnyRowTypeName;
attachmentUrl: string;
submissionData: SubmissionResponse;
/**
* Being called after attachment was deleted succesfully. Is meant to be used
* by parent component to reflect this change in the data it holds, and
* possibly in other places in UI.
*/
onDeleted: () => void;
}

/**
* Displays a "…" button that opens a dropdown with some actions available for
* provided attachment. Delete option would display a safety check modal.
*/
export default function AttachmentActionsDropdown(
props: AttachmentActionsDropdownProps
) {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
const [isDeletePending, setIsDeletePending] = useState<boolean>(false);

const toggleDeleteModal = () => {
setIsDeleteModalOpen(!isDeleteModalOpen);
};

let attachmentTypeName = t('attachment');
if (props.questionType === QuestionTypeName.audio) {
attachmentTypeName = t('audio recording');
} else if (props.questionType === QuestionTypeName.video) {
attachmentTypeName = t('video recording');
} else if (props.questionType === QuestionTypeName.image) {
attachmentTypeName = t('image');
} else if (props.questionType === MetaQuestionTypeName['background-audio']) {
attachmentTypeName = t('background audio recording');
}

function confirmDelete() {
setIsDeletePending(true);

// TODO: replace the timeout with actual API call that will delete
// the attachment.
console.log('confirmDelete');
setTimeout(() => {
// TODO: Upon finishing we need to have the submission data being updated
// both here in Submission Modal and in Data Table.
// Validation status changing in Submission Modal works like this:
// 1. Does the call (both Submission Modal and Data Table listens to same call)
// 2. Call finishes and returns a fresh SubmissionResponse
// 3. Upon finishing, Submission Modal updates the submission from props
// 4. Upon finishing, Data Table updates the submission in the list
// We would need to do something similar here.

// TODO: We would need to confirm from Back-end how would the deleted
// attachment be marked
onAttachmentDeleted();
}, 2000);
}

/**
* Stops pending animation, closes confirmation prompt and displays
* a notification. Also let the parent know through prop callback.
*/
function onAttachmentDeleted() {
setIsDeletePending(false);
toggleDeleteModal();
utils.notify(
t('##Attachment_type## deleted')
.replace('##Attachment_type##', attachmentTypeName)
);
props.onDeleted();
}

function requestDownloadFile() {
utils.downloadUrl(props.attachmentUrl);
}

const userCanChange = userHasPermForSubmission(
'change_submissions',
props.asset,
props.submissionData
);

const uniqueDropdownName = `attachment-actions-${utils.generateUuid()}`;

return (
<>
<KoboDropdown
name={uniqueDropdownName}
placement='down-right'
hideOnMenuClick
triggerContent={
<Button
type='bare'
color='dark-blue'
size='s'
startIcon='more'
/>
}
menuContent={
<bem.KoboSelect__menu>
<bem.KoboSelect__option onClick={requestDownloadFile}>
<Icon name='download' />
<label>{t('Download')}</label>
</bem.KoboSelect__option>

<bem.KoboSelect__option
onClick={toggleDeleteModal}
disabled={!userCanChange}
>
<Icon name='trash' />
<label>{t('Delete')}</label>
</bem.KoboSelect__option>
</bem.KoboSelect__menu>
}
/>

<KoboModal
isOpen={isDeleteModalOpen}
onRequestClose={toggleDeleteModal}
size='medium'
>
<KoboModalHeader onRequestCloseByX={toggleDeleteModal}>
{t('Delete ##attachment_type##').replace('##attachment_type##', attachmentTypeName)}
</KoboModalHeader>

<KoboModalContent>
<p>{t('Are you sure you want to delete this ##attachment_type##?').replace('##attachment_type##', attachmentTypeName)}</p>
</KoboModalContent>

<KoboModalFooter>
<Button
type='frame'
color='dark-blue'
size='l'
onClick={toggleDeleteModal}
label={t('Cancel')}
/>

<Button
type='full'
color='dark-red'
size='l'
onClick={confirmDelete}
label={t('Delete')}
isDisabled={!userCanChange}
isPending={isDeletePending}
/>
</KoboModalFooter>
</KoboModal>
</>
);
}
Loading
Loading