Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
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
102 changes: 3 additions & 99 deletions __tests__/Unit/Components/Tasks/TaskStatusEditMode.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('TaskStatusEditMode', () => {
<TaskStatusEditMode
task={BLOCKED_TASK}
setEditedTaskDetails={setEditedTaskDetails}
setSaveExtensionRequestStatus={jest.fn()}
/>
</Provider>
);
Expand All @@ -52,6 +53,7 @@ describe('TaskStatusEditMode', () => {
<TaskStatusEditMode
task={BLOCKED_TASK}
setEditedTaskDetails={setEditedTaskDetails}
setSaveExtensionRequestStatus={jest.fn()}
/>
</Provider>
);
Expand Down Expand Up @@ -80,6 +82,7 @@ describe('TaskStatusEditMode', () => {
<TaskStatusEditMode
task={BLOCKED_TASK}
setEditedTaskDetails={setEditedTaskDetails}
setSaveExtensionRequestStatus={jest.fn()}
/>
</Provider>
);
Expand All @@ -102,105 +105,6 @@ describe('TaskStatusEditMode', () => {

expect(allOptions).toEqual(allTaskStatus);
});

it('renders the spinner and error icon when the task update fails', async () => {
const setEditedTaskDetails = jest.fn();

updateTaskSpy.mockImplementation(() => [
jest.fn().mockImplementation(() => {
return {
unwrap: () =>
Promise.reject({
data: { message: 'Error updating task' },
}),
};
}),
{ isLoading: false },
]);

renderWithRouter(
<Provider store={store()}>
<TaskStatusEditMode
task={BLOCKED_TASK}
setEditedTaskDetails={setEditedTaskDetails}
/>
</Provider>,
{}
);

const statusSelect = screen.getByLabelText('Status:');

expect(statusSelect).toHaveValue('BLOCKED');

fireEvent.change(statusSelect, { target: { value: 'IN_PROGRESS' } });

expect(updateTaskSpy).toHaveBeenCalled();

await waitFor(
() => {
expect(screen.getByTestId('small-spinner')).toBeInTheDocument();
},
{ timeout: 2000 }
);

await waitFor(() => {
expect(screen.getByTestId('error')).toBeInTheDocument();
});
});

it('shows saved indicator and clears status after successful update', async () => {
const setEditedTaskDetails = jest.fn();
jest.useFakeTimers();

const toastModule = await import('@/helperFunctions/toast');
const toastSpy = jest.spyOn(toastModule, 'toast');

updateTaskSpy.mockImplementation(() => [
jest.fn().mockImplementation(() => {
return {
unwrap: () => Promise.resolve({ status: 'SUCCESS' }),
};
}),
{ isLoading: false },
]);

renderWithRouter(
<Provider store={store()}>
<TaskStatusEditMode
task={BLOCKED_TASK}
setEditedTaskDetails={setEditedTaskDetails}
/>
</Provider>,
{}
);

const statusSelect = screen.getByLabelText('Status:');

fireEvent.change(statusSelect, { target: { value: 'IN_PROGRESS' } });

await waitFor(
() => {
expect(screen.getByTestId('small-spinner')).toBeInTheDocument();
},
{ timeout: 1000 }
);

await waitFor(() => {
expect(screen.getByTestId('checkmark')).toBeInTheDocument();
expect(toastSpy).toHaveBeenCalledWith(
'success',
'Task status updated successfully'
);
});

jest.advanceTimersByTime(3000);

await waitFor(() => {
expect(screen.queryByTestId('checkmark')).not.toBeInTheDocument();
});

jest.useRealTimers();
});
});

describe('test beautifyStatus function', () => {
Expand Down
1 change: 1 addition & 0 deletions src/app/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const api = createApi({
'Progress_Details',
'User_Standup',
'TASK_REQUEST',
'Extension_Requests',
],
/**
* This api has endpoints injected in adjacent files,
Expand Down
15 changes: 15 additions & 0 deletions src/app/services/tasksApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import task, {
TaskRequestPayload,
TasksResponseType,
GetAllTaskParamType,
ExtensionRequestsResponse,
} from '@/interfaces/task.type';
import { api } from './api';
import { MINE_TASKS_URL, TASKS_URL } from '@/constants/url';
Expand Down Expand Up @@ -100,6 +101,19 @@ export const tasksApi = api.injectEndpoints({
},
],
}),
getSelfExtensionRequests: builder.query<
ExtensionRequestsResponse,
{ taskId: string; dev: boolean }
>({
query: ({ taskId, dev }) => ({
url: '/extension-requests/self',
params: {
taskId,
dev,
},
}),
providesTags: ['Extension_Requests'],
}),
}),
overrideExisting: true,
});
Expand All @@ -110,4 +124,5 @@ export const {
useAddTaskMutation,
useUpdateTaskMutation,
useUpdateSelfTaskMutation,
useGetSelfExtensionRequestsQuery,
} = tasksApi;
88 changes: 88 additions & 0 deletions src/components/ExtensionRequest/ExtensionRequestDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from 'react';
import { formatToRelativeTime } from './ExtensionStatusModal';

type ExtensionRequestDetailsProps = {
extensionRequests: any[];
styles: any;
getExtensionRequestDetails: (request: any, styles: any) => any[];
};

export const ExtensionRequestDetails: React.FC<ExtensionRequestDetailsProps> =
({ extensionRequests, styles, getExtensionRequestDetails }) => {
if (extensionRequests.length === 0) {
return (
<div
className={styles.extensionNoRequests}
data-testid="no-requests-message"
>
<p>
No extension requests found for this task, want to
create one?
</p>
</div>
);
}

return (
<>
{extensionRequests.map((request) => (
<div
key={request.id}
className={styles.extensionExtensionRequest}
data-testid={`extension-request-${request.id}`}
>
{getExtensionRequestDetails(request, styles).map(
(item, idx) => (
<div
key={idx}
className={styles.extensionDetailRow}
data-testid={`detail-row-${idx}`}
>
<span
className={styles.extensionLabel}
data-testid={`label-${item.testId}`}
>
{item.label}
</span>
<span
className={`${styles.extensionValue} ${
item.className || ''
}`}
data-testid={`value-${item.testId}`}
>
{item.value}
</span>
</div>
)
)}

{request.reviewedBy &&
(request.status === 'APPROVED' ||
request.status === 'DENIED') && (
<div
className={
request.status === 'APPROVED'
? styles.extensionApprovalInfo
: styles.extensionRejectionInfo
}
data-testid={
request.status === 'APPROVED'
? 'approval-info'
: 'denied-info'
}
>
Your request was{' '}
{request.status.toLowerCase()} by{' '}
{request.reviewedBy}{' '}
{request.reviewedAt
? formatToRelativeTime(
request.reviewedAt
)
: ''}
</div>
)}
</div>
))}
</>
);
};
127 changes: 127 additions & 0 deletions src/components/ExtensionRequest/ExtensionStatusModal.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
@import '../../styles/variables.scss';

.extensionModalOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba($black, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}

.extensionModal {
background-color: $white;
padding: 1.9rem;
border-radius: 0.6rem;
width: 90%;
max-width: 48rem;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 1.3rem 3.1rem rgba($black, 0.7);
font-family: 'Segoe UI', sans-serif;
font-size: 1rem;
}

.extensionModal h2 {
text-align: center;
margin-bottom: 1.9rem;
font-size: 2rem;
font-weight: 500;
color: rgba($black, 0.9);
}

.extensionDetailRow {
display: flex;
flex-wrap: wrap;
margin-bottom: 1rem;
line-height: 1.5;
}

.extensionLabel {
font-weight: bold;
width: 10rem;
color: rgba($black, 0.9);
flex-shrink: 0;
}

.extensionValue {
flex: 1;
color: rgba($black, 0.9);
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
}

.extensionApprovalInfo,
.extensionRejectionInfo {
margin-top: 0.6rem;
font-style: italic;
color: $black;
text-align: center;
}

.extensionButtonContainer {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1.9rem;
}

.extensionCloseButton,
.extensionRequestButton {
padding: 0.8rem 0;
border: none;
border-radius: 0.4rem;
cursor: pointer;
width: 40%;
font-weight: bold;
font-size: 1rem;
}

.extensionCloseButton {
background-color: $red;
color: $white;
}

.extensionRequestButton {
background-color: $green;
color: $white;
}

.extensionApproved {
font-weight: bold;
color: $green;
}

.extensionPending,
.extensionDenied {
font-weight: bold;
color: $orange;
}

.extensionModalLoading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1100;
background-color: $white;
padding: 1.5rem;
border-radius: 0.5rem;
max-width: 18.8rem;
}

.extensionNoRequests {
text-align: center;
}

.spinnerContainer {
display: flex;
justify-content: center;
width: 100%;
margin: 1rem 0;
}
Loading