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

[MI-2035]: Integrated unlinking project from details page #33

Merged
merged 13 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from 12 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
7 changes: 7 additions & 0 deletions server/plugin/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ func (p *Plugin) handleGetAllLinkedProjects(w http.ResponseWriter, r *http.Reque
return
}

w.Header().Add("Content-Type", "application/json")

if projectList == nil {
_, _ = w.Write([]byte("[]"))
return
}

response, err := json.Marshal(projectList)
if err != nil {
p.API.LogError(constants.ErrorFetchProjectList, "Error", err.Error())
Expand Down
36 changes: 20 additions & 16 deletions webapp/src/components/buttons/iconButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {Button} from 'react-bootstrap';

import Tooltip from 'components/tooltip';

import {onPressingEnterKey} from 'utils';

import './styles.scss';

type IconColor = 'danger'
Expand All @@ -15,21 +17,23 @@ type IconButtonProps = {
onClick?: () => void
}

const IconButton = ({tooltipText, iconClassName, extraClass = '', iconColor, onClick}: IconButtonProps) => {
return (
<Tooltip tooltipContent={tooltipText}>
<Button
variant='outline-danger'
className={`plugin-btn button-wrapper btn-icon ${extraClass}`}
onClick={onClick}
>
<i
className={iconClassName}
aria-hidden='true'
/>
</Button>
</Tooltip>
);
};
const IconButton = ({tooltipText, iconClassName, extraClass = '', iconColor, onClick}: IconButtonProps) => (
<Tooltip tooltipContent={tooltipText}>
<Button
variant='outline-danger'
className={`plugin-btn button-wrapper btn-icon ${extraClass}`}
onClick={onClick}
aria-label={tooltipText}
role='button'
tabIndex={0}
onKeyDown={(event) => onPressingEnterKey(event, () => onClick?.())}
>
<i
className={iconClassName}
aria-hidden='true'
/>
</Button>
</Tooltip>
);

export default IconButton;
72 changes: 54 additions & 18 deletions webapp/src/components/emptyState/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,55 @@ type EmptyStatePropTypes = {
icon?: DisplayIcon;
}

const EmptyState = ({title, subTitle, buttonText, buttonAction}: EmptyStatePropTypes) => (
// TODO: UI to be changed
const EmptyState = ({title, subTitle, buttonText, buttonAction, icon = 'folder'}: EmptyStatePropTypes) => (
<div className='no-data d-flex'>
<div className='d-flex flex-column align-items-center'>
<div className='no-data__icon d-flex justify-content-center align-items-center'>
<svg
width='32px'
height='32px'
viewBox='0 0 32 32'
>
<path d='M0 11.865l2.995-3.953 11.208-4.557v-3.292l9.828 7.188-20.078 3.896v10.969l-3.953-1.141zM32 5.932v19.536l-7.672 6.531-12.401-4.073v4.073l-7.974-9.885 20.078 2.396v-17.26z'/>
</svg>
{

// TODO: use SVGWrapper component
icon === 'azure' && (
<svg
width='36'
height='36'
viewBox='0 0 36 36'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M4.449 12.4965L27.033 8.1045L15.978 0V3.7155L3.3705 8.8485L0 13.3065V23.571L4.449 24.855V12.4965ZM13.416 31.407L27.3705 36L36 28.638V6.618L27.0345 8.1045V27.5565L4.449 24.855L13.416 36V31.407Z'
fill='#8E8E8E'
/>
</svg>
)
}
{
icon === 'folder' && (
<svg
width='48'
height='40'
viewBox='0 0 48 40'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M2 21.1112V35.2223C2 36.8792 3.34314 38.2223 5 38.2223H36.3333C37.9902 38.2223 39.3333 36.8792 39.3333 35.2223V21.1112M2 21.1112V11.6667C2 10.0099 3.34315 8.66675 5 8.66675H13.5361C14.1284 8.66675 14.7074 8.84206 15.2002 9.1706L20.6887 12.8296C21.1815 13.1581 21.7605 13.3334 22.3528 13.3334H36.3333C37.9902 13.3334 39.3333 14.6766 39.3333 16.3334V21.1112M2 21.1112H39.3333'
stroke='#8E8E8E'
strokeWidth='3.5'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M9.77783 2H16.9917C18.3737 2 19.7248 2.40907 20.8746 3.17565L24.3477 5.49102C25.4976 6.2576 26.8486 6.66667 28.2306 6.66667H39.0001C42.8661 6.66667 46.0001 9.80067 46.0001 13.6667V28.4444'
stroke='#8E8E8E'
strokeWidth='3.5'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
)
}
</div>
<p className='no-data__title'>{title}</p>
{subTitle && (
Expand All @@ -37,16 +75,14 @@ const EmptyState = ({title, subTitle, buttonText, buttonAction}: EmptyStatePropT

</>
)}
{
buttonText && buttonAction && (
<button
onClick={buttonAction}
className='plugin-btn no-data__btn btn btn-primary'
>
{buttonText}
</button>
)
}
{buttonText && buttonAction && (
<button
onClick={buttonAction}
className='plugin-btn no-data__btn btn btn-primary'
>
{buttonText}
</button>
)}
</div>
</div>
);
Expand Down
5 changes: 3 additions & 2 deletions webapp/src/components/modal/confirmationModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ type ConfirmationModalProps = {
title: string
description: string
confirmBtnText: string
onHide: () => void
onHide?: () => void
onConfirm?: () => void
isLoading?: boolean
}

const ConfirmationModal = ({isOpen, title, confirmBtnText, description, onHide, onConfirm}: ConfirmationModalProps) => (
const ConfirmationModal = ({isOpen, title, confirmBtnText, description, onHide, onConfirm, isLoading}: ConfirmationModalProps) => (
<Modal
show={isOpen}
title={title}
onHide={onHide}
onConfirm={onConfirm}
confirmAction={true}
confirmBtnText={confirmBtnText}
loading={isLoading}
>
<p>{description}</p>
</Modal>
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/components/modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import ModalSubTitleAndError from './subComponents/modalSubtitleAndError';

type ModalProps = {
show: boolean;
onHide: () => void;
onHide?: () => void;
showCloseIconInHeader?: boolean;
children?: JSX.Element;
title?: string | JSX.Element;
Expand Down
9 changes: 5 additions & 4 deletions webapp/src/containers/LinkModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useCallback, useEffect, useState} from 'react';
import React, {useEffect, useState} from 'react';
import {useDispatch} from 'react-redux';

import Input from 'components/inputField';
Expand Down Expand Up @@ -50,7 +50,8 @@ const LinkModal = () => {
setProjectDetails({...projectDetails, project: (e.target as HTMLInputElement).value});
};

const onConfirm = useCallback(() => {
// Handles on confirming link project
const onConfirm = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you remove the useCallback?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need any caching here even when the callback dependency which would be errorState here is changed we need to perform all the logic present inside this func

so, I don't think it worth using that, what you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand. If the dependency errorState is not changing, then why do we need to perform all the logic inside the function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad, I thought the submit button stays disabled when fields are invalidated so it will be enabled only when errors are resolved and that way we would be performing the logic only when the button is clicked this implies error fields are resolved

added callback, but we can enhance our UI to keep submit button disabled if there is any error in the form and no change is made

@sooraj-shukla you can also do this as this is a good practice, it's useless to keep the button enabled when form already have validation errors

const errorStateChanges: LinkPayload = {
organization: '',
project: '',
Expand All @@ -71,9 +72,9 @@ const LinkModal = () => {

// Make POST api request
linkTask(projectDetails);
}, [errorState]);
};

// Make POST API request to link a project
// Make POST api request to link a project
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
const linkTask = async (payload: LinkPayload) => {
const createTaskRequest = await usePlugin.makeApiRequest(plugin_constants.pluginApiServiceConfigs.createLink.apiServiceName, payload);
if (createTaskRequest) {
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/containers/Rhs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const Rhs = (): JSX.Element => {
{
isUserAccountConnected() && (
getprojectDetailsState(state).projectID ?
<ProjectDetails title={getprojectDetailsState(state).projectName}/> :
<ProjectDetails {...getprojectDetailsState(state)}/> :
<ProjectList/>)
}
</div>
Expand Down
48 changes: 41 additions & 7 deletions webapp/src/containers/Rhs/projectDetails/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import React, {useEffect} from 'react';
import React, {useEffect, useState} from 'react';
import {useDispatch} from 'react-redux';

import SubscriptionCard from 'components/card/subscription';
import IconButton from 'components/buttons/iconButton';
import BackButton from 'components/buttons/backButton';
import ConfirmationModal from 'components/modal/confirmationModal';

import usePluginApi from 'hooks/usePluginApi';
import {resetProjectDetails} from 'reducers/projectDetails';

import plugin_constants from 'plugin_constants';

// TODO: dummy data, remove later
const data: SubscriptionDetails[] = [
{
Expand All @@ -26,31 +30,61 @@ const data: SubscriptionDetails[] = [
},
];

type ProjectDetailsProps = {
title: string
}
const ProjectDetails = (projectDetails: ProjectDetails) => {
// State variables
const [showConfirmationModal, setShowConfirmationModal] = useState(false);

const ProjectDetails = ({title}: ProjectDetailsProps) => {
// Hooks
const dispatch = useDispatch();
const usePlugin = usePluginApi();

const handleResetProjectDetails = () => {
dispatch(resetProjectDetails());
};

/**
* Opens a confirmation modal to confirm unlinking a project
*/
const handleUnlinkProject = () => {
setShowConfirmationModal(true);
};

// Handles unlinking a project
const handleConfirmUnlinkProject = async () => {
const unlinkProjectStatus = await usePlugin.makeApiRequest(plugin_constants.pluginApiServiceConfigs.unlinkProject.apiServiceName, projectDetails);

if (unlinkProjectStatus) {
handleResetProjectDetails();
setShowConfirmationModal(false);
}
};

// Reset the state when the component is unmounted
useEffect(() => {
return handleResetProjectDetails();
return () => {
handleResetProjectDetails();
};
}, []);

return (
<>
<ConfirmationModal
isOpen={showConfirmationModal}
onHide={() => setShowConfirmationModal(false)}
onConfirm={handleConfirmUnlinkProject}
isLoading={usePlugin.getApiState(plugin_constants.pluginApiServiceConfigs.unlinkProject.apiServiceName, projectDetails).isLoading}
confirmBtnText='Unlink'
description={`Are you sure you want to unlink ${projectDetails?.projectName}?`}
title='Confirm Project Unlink'
/>
<BackButton onClick={handleResetProjectDetails}/>
<div className='d-flex'>
<p className='rhs-title'>{title}</p>
<p className='rhs-title'>{projectDetails.projectName}</p>
<IconButton
tooltipText='Unlink project'
iconClassName='fa fa-chain-broken'
extraClass='project-details-unlink-button unlink-button'
onClick={handleUnlinkProject}
/>
</div>
<div className='bottom-divider'>
Expand Down
34 changes: 34 additions & 0 deletions webapp/src/containers/Rhs/projectList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,40 @@ const ProjectList = () => {
)
)
}
{
usePlugin.getApiState(plugin_constants.pluginApiServiceConfigs.getAllLinkedProjectsList.apiServiceName).isSuccess && (
data && data?.length > 0 ? (
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
<>
{
data?.map((item: ProjectDetails) => (
manojmalik20 marked this conversation as resolved.
Show resolved Hide resolved
avas27JTG marked this conversation as resolved.
Show resolved Hide resolved
<ProjectCard
onProjectTitleClick={handleProjectTitleClick}
projectDetails={item}
key={item.projectID}
handleUnlinkProject={handleUnlinkProject}
/>
),
)
}
<div className='rhs-project-list-wrapper'>
<button
onClick={handleOpenLinkProjectModal}
className='plugin-btn no-data__btn btn btn-primary project-list-btn'
>
{'Link new project'}
</button>
</div>
</>
) : (
<EmptyState
title='No Project Linked'
subTitle={{text: 'Link a project by clicking the button below'}}
buttonText='Link new project'
buttonAction={handleOpenLinkProjectModal}
/>
)
)
}
</>
);
};
Expand Down
1 change: 1 addition & 0 deletions webapp/src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default class Hooks {
// const args = splitArgs(commandTrimmed);
return Promise.resolve({});
}

return Promise.resolve({
message,
args: contextArgs,
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const getProjectLinkModalArgs = (str: string): LinkPayload => {
};
};

export const onPressingEnterKey = (event: Event | undefined, func: () => void) => {
export const onPressingEnterKey = (event: React.KeyboardEvent<HTMLButtonElement> | undefined, func: () => void) => {
if (event instanceof KeyboardEvent && event.key !== 'Enter' && event.key !== ' ') {
return;
}
Expand Down