diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..b58b603f --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..03d9549e --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..8db3b771 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/sphinx-tribes-frontend.iml b/.idea/sphinx-tribes-frontend.iml new file mode 100644 index 00000000..24643cc3 --- /dev/null +++ b/.idea/sphinx-tribes-frontend.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/cypress/e2e/58_addUserStoriesToFeature.cy.ts b/cypress/e2e/58_addUserStoriesToFeature.cy.ts index 51d29a1f..42353c2e 100644 --- a/cypress/e2e/58_addUserStoriesToFeature.cy.ts +++ b/cypress/e2e/58_addUserStoriesToFeature.cy.ts @@ -3,7 +3,7 @@ describe('Add user stories to features', () => { cy.login('carol'); cy.wait(1000); - const WorkSpaceName = 'user story'; + const WorkSpaceName = 'user story8'; const workspace = { loggedInAs: 'carol', @@ -46,12 +46,36 @@ describe('Add user stories to features', () => { cy.wait(1000); const userStory = 'this is the story of a user'; - cy.get('[data-testid="story-input"]').type(userStory); - cy.get('[data-testid="story-input-update-btn"]').click(); + for (let i = 1; i <= 2; i++) { + const userStoryWithNumber = `${userStory} ${i}`; + cy.get('[data-testid="story-input"]').type(userStoryWithNumber); + cy.get('[data-testid="story-input-update-btn"]').click(); + cy.wait(1000); + + cy.contains(userStoryWithNumber).should('exist', { timeout: 1000 }); + cy.wait(1000); + } + + cy.get('[data-testid="0-user-story-option-btn"]').click(); + cy.get('[data-testid="user-story-edit-btn"]').click(); cy.wait(1000); + cy.get('[data-testid="edit-story-input"]').clear(); + const updatedUserStory = 'this is the story of a user changed'; + cy.get('[data-testid="edit-story-input"]').type(updatedUserStory); + cy.get('[data-testid="user-story-save-btn"]').click(); + cy.wait(1000); + + cy.contains(updatedUserStory).should('exist', { timeout: 1000 }); - cy.contains(userStory).should('exist', { timeout: 1000 }); - cy.wait(5000); + cy.get('[data-testid="1-user-story-option-btn"]').click(); + cy.get('[data-testid="user-story-edit-btn"]').click(); + cy.wait(1000); + cy.get('[data-testid="user-story-delete-btn"]').click(); + cy.wait(1000); + cy.contains('Delete').click({ force: true }); + cy.wait(1000); + const userStoryWithNumber = `${userStory} ${2}`; + cy.contains(userStoryWithNumber).should('not.exist', { timeout: 1000 }); cy.logout('carol'); }); }); diff --git a/src/pages/tickets/style.ts b/src/pages/tickets/style.ts index b1b76e95..30482e87 100644 --- a/src/pages/tickets/style.ts +++ b/src/pages/tickets/style.ts @@ -171,6 +171,24 @@ export const OptionsWrap = styled.div` } `; +export const UserStoryOptionWrap = styled.div` + position: relative; + display: inline-block; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + + button { + border: 0.5px solid #000000; + font-size: 0.8rem; + font-weight: 700; + border-radius: 5px; + padding: 2px 10px; + } +`; export const TextArea = styled.textarea` padding: 0.5rem 1rem; border-radius: 0.375rem; diff --git a/src/people/widgetViews/WorkspaceFeature.tsx b/src/people/widgetViews/WorkspaceFeature.tsx index 465f65c6..2ee18ee7 100644 --- a/src/people/widgetViews/WorkspaceFeature.tsx +++ b/src/people/widgetViews/WorkspaceFeature.tsx @@ -9,16 +9,19 @@ import { OptionsWrap, TextArea, InputField, - Input + Input, + UserStoryOptionWrap } from 'pages/tickets/style'; import React, { useCallback, useEffect, useState } from 'react'; import history from 'config/history'; import { useParams } from 'react-router-dom'; import { useStores } from 'store'; import { mainStore } from 'store/main'; -import { Feature } from 'store/interface'; +import { Feature, FeatureStory } from 'store/interface'; import MaterialIcon from '@material/react-material-icon'; -import { FeatureStory } from 'store/interface'; +import { EuiOverlayMask, EuiModalHeader, EuiModalFooter, EuiText } from '@elastic/eui'; +import { Box } from '@mui/system'; +import { useDeleteConfirmationModal } from '../../components/common'; import { ActionButton, ButtonWrap, @@ -27,7 +30,12 @@ import { WorkspaceName, WorkspaceOption, UserStoryField, - UserStoryFields + UserStoryFields, + UserStoryOption, + StyledModal, + ModalBody, + ButtonGroup, + StoryButtonWrap } from './workspace/style'; type DispatchSetStateAction = React.Dispatch>; @@ -175,6 +183,81 @@ const WorkspaceEditableField = ({ ); }; +interface UserStoryModalProps { + open: boolean; + storyDescription: string; + handleClose: () => void; + handleSave: (inputValue: string) => void; + handleDelete: () => void; +} + +const UserStoryModal: React.FC = ({ + open, + storyDescription, + handleClose, + handleSave, + handleDelete +}: UserStoryModalProps) => { + const [inputValue, setInputValue] = useState(storyDescription); + + useEffect(() => { + setInputValue(storyDescription); + }, [storyDescription]); + const handleInputChange = (event: React.ChangeEvent) => { + setInputValue(event.target.value); + }; + + return ( + <> + {open && ( + + + + +

Edit User Story

+
+
+ + + + + + + + handleSave(inputValue)} + > + Save + + + Cancel + + + + Delete + + + +
+
+ )} + + ); +}; + const WorkspaceFeature: React.FC = () => { const { main, ui } = useStores(); const { feature_uuid } = useParams<{ feature_uuid: string }>(); @@ -192,6 +275,11 @@ const WorkspaceFeature: React.FC = () => { const [displayBriefOptions, setDisplayBriefOptions] = useState(false); const [displayArchitectureOptions, setDisplayArchitectureOptions] = useState(false); const [displayRequirementsOptions, setDisplayRequirementsOptions] = useState(false); + const [displayUserStoryOptions, setDisplayUserStoryOptions] = useState>( + {} + ); + const [modalOpen, setModalOpen] = useState(false); + const [editUserStory, setEditUserStory] = useState(); const getFeatureData = useCallback(async (): Promise => { if (!feature_uuid) return; @@ -250,6 +338,69 @@ const WorkspaceFeature: React.FC = () => { setUserStory(e.target.value); }; + const handleUserStoryOptionClick = (storyId: number) => { + setDisplayUserStoryOptions((prev: Record) => ({ + ...prev, + [storyId]: !prev[storyId] + })); + }; + + const handleUserStoryEdit = (featureStory: FeatureStory) => { + const storyId = featureStory?.id as number; + setEditUserStory(featureStory); + setModalOpen(true); + setDisplayUserStoryOptions((prev: Record) => ({ + ...prev, + [storyId]: !prev[storyId] + })); + }; + + const handleModalClose = () => { + setModalOpen(false); + }; + + const handleModalSave = async (inputValue: string) => { + const body = { + uuid: editUserStory?.uuid, + feature_uuid: editUserStory?.feature_uuid ?? '', + description: inputValue, + priority: editUserStory?.priority + }; + await main.addFeatureStory(body); + await getFeatureStoryData(); + setModalOpen(false); + }; + + const deleteUserStory = async () => { + await main.deleteFeatureStory( + editUserStory?.feature_uuid as string, + editUserStory?.uuid as string + ); + await getFeatureStoryData(); + return; + }; + + const { openDeleteConfirmation } = useDeleteConfirmationModal(); + + const deleteHandler = () => { + openDeleteConfirmation({ + onDelete: deleteUserStory, + children: ( + + Are you sure you want to
+ + delete this User Story? + +
+ ) + }); + }; + + const handleModalDelete = async () => { + setModalOpen(false); + deleteHandler(); + }; + const toastsEl = ( { - {featureStories?.map((story: FeatureStory) => ( - - - {story.description} - - ))} + {featureStories + ?.sort((a: FeatureStory, b: FeatureStory) => a.priority - b.priority) + ?.map((story: FeatureStory) => ( + + + handleUserStoryOptionClick(story.id as number)} + data-testid={`${story.priority}-user-story-option-btn`} + /> + {displayUserStoryOptions[story?.id as number] && ( + +
    +
  • handleUserStoryEdit(story)} + > + Edit +
  • +
+
+ )} +
+ {story.description} +
+ ))}
@@ -356,6 +527,13 @@ const WorkspaceFeature: React.FC = () => { /> {toastsEl} + ); }; diff --git a/src/people/widgetViews/workspace/style.ts b/src/people/widgetViews/workspace/style.ts index 7f5e96c6..80f857d8 100644 --- a/src/people/widgetViews/workspace/style.ts +++ b/src/people/widgetViews/workspace/style.ts @@ -996,12 +996,14 @@ interface ButtonProps { color?: string; marginTop?: string; height?: string; + borderRadius?: string; } export const ActionButton = styled.button` padding: 5px 20px; + width: 120px; border-radius: 5px; - border-radius: 0.375rem; + border-radius: ${(p: any) => p.borderRadius ?? '0.375rem'}; font-family: 'Barlow'; font-size: 0.9375rem; font-style: normal; @@ -1104,6 +1106,45 @@ export const WorkspaceOption = styled.div` } `; +export const UserStoryOption = styled.div` + position: absolute; + z-index: 2; + top: 100%; + right: -65px; + width: 80px; + height: 30px; + background: #fff; + border: 1px solid #ccc; + border-radius: 6px; + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.25); + ul { + list-style: none; + padding: 0; + margin: 0; + } + + li { + padding: 5px; + text-align: center; + cursor: pointer; + font-family: 'Barlow', sans-serif; + font-size: 0.8rem; + font-weight: 500; + line-height: 18px; + border-bottom: 0.5px solid #ccc; + transition: background-color 0.3s ease; + + &:hover { + background-color: #f0f0f0; + color: #3c3f41; + border-radius: 6px; + } + + &:last-child { + border-bottom: none; + } + } +`; export const UserStoryFields = styled.div` margin-top: 20px; `; @@ -1113,3 +1154,29 @@ export const UserStoryField = styled.div` display: flex; align-items: center; `; + +export const StyledModal = styled.div` + background: #fff; + width: 600px; + border: 2px solid dimgray; +`; + +export const ModalBody = styled.div` + margin: 20px 0 20px 40px; + width: 100%; +`; + +export const StoryButtonWrap = styled.div` + margin-right: auto; + margin-top: 10px; + display: flex; + gap: 15px; +`; + +export const ButtonGroup = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + margin-top: 20px; +`; diff --git a/src/store/main.ts b/src/store/main.ts index c38348e2..fb55522d 100644 --- a/src/store/main.ts +++ b/src/store/main.ts @@ -2619,6 +2619,30 @@ export class MainStore { } } + async deleteFeatureStory( + feature_uuid: string, + uuid: string + ): Promise { + try { + if (!uiStore.meInfo) return undefined; + const info = uiStore.meInfo; + + const r: any = await fetch(`${TribesURL}/features/${feature_uuid}/story/${uuid}`, { + method: 'DELETE', + mode: 'cors', + headers: { + 'x-jwt': info.tribe_jwt, + 'Content-Type': 'application/json' + } + }); + + return r.json(); + } catch (e) { + console.error('getFeaturesByUuid', e); + return undefined; + } + } + async addWorkspaceFeature(body: CreateFeatureInput): Promise { try { if (!uiStore.meInfo) return {};