Skip to content

Commit

Permalink
Feature/feedback form (#2085)
Browse files Browse the repository at this point in the history
* first skeleton for the feedback component

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* star component

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* include feedback button

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* send data to heap through feedback

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* remove unused prop and component

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* add title for feedback button

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* mood board

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* update with final design

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* include messages

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* move to Feedback wrapper instead

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* add local storage to track first time

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* add key for Mood component

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* draft version of local storage

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* update styling

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* break the title into 2 lines

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* move mood to its own component

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* include the release note

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* fix eslint error

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* Separate feedback to feedback-button and feedback-form to suit the logic

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* tidy up code

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* remove unused icon and components

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* add transition for feedback-form and button

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* include new message for cancel event

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* update localStorage name

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* move feedback to config

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* update text area colour

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* update font-size for text area and hover state for button

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* update font for place holder in text area

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* update font for place holder in text area in feedback

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* re-run the test

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* include classnames for icons

Signed-off-by: Huong Nguyen <huongg1409@gmail>

* test ci fail with main

Signed-off-by: Jitendra Gundaniya <jitendra_gundaniya@mckinsey.com>

* config removed

Signed-off-by: Jitendra Gundaniya <jitendra_gundaniya@mckinsey.com>

* config var added

Signed-off-by: Jitendra Gundaniya <jitendra_gundaniya@mckinsey.com>

* include localStorage from config

Signed-off-by: Huong Nguyen <huongg1409@gmail>

---------

Signed-off-by: Huong Nguyen <huongg1409@gmail>
Signed-off-by: Jitendra Gundaniya <jitendra_gundaniya@mckinsey.com>
Co-authored-by: Huong Nguyen <huongg1409@gmail>
Co-authored-by: Jitendra Gundaniya <jitendra_gundaniya@mckinsey.com>
  • Loading branch information
3 people authored Sep 18, 2024
1 parent 8e622c0 commit 490416d
Show file tree
Hide file tree
Showing 15 changed files with 505 additions and 4 deletions.
2 changes: 1 addition & 1 deletion RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Please follow the established format:
- Slice a pipeline functionality. (#2036)

## Bug fixes and other changes

- Add feedback component for slicing pipeline. (#2085)
- Fixes design issues in metadata panel. (#2009)
- Fix missing run command in metadata panel for task nodes. (#2055)
- Add `UnavailableDataset` as a default dataset for `--lite` mode. (#2083)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,4 @@
"not op_mini all"
],
"snyk": true
}
}
17 changes: 17 additions & 0 deletions src/components/feedback-button/feedback-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import classnames from 'classnames';

import './feedback-button.scss';

export const FeedbackButton = ({ onClick, visible, title }) => {
return (
<button
className={classnames('feedback-button', {
'feedback-button--visible': visible,
})}
onClick={onClick}
>
{title}
</button>
);
};
21 changes: 21 additions & 0 deletions src/components/feedback-button/feedback-button.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.feedback-button {
background-color: #007bff;
border: none;
color: white;
cursor: pointer;
font-size: 16px;
height: 40px;
line-height: 40px;
opacity: 0;
position: fixed;
right: -96px;
text-align: center;
top: 50%;
padding: 0 16px;
transform: translateY(-50%) rotate(90deg);
}

.feedback-button--visible {
opacity: 1;
transition: opacity 0.5s ease-in;
}
110 changes: 110 additions & 0 deletions src/components/feedback-form/feedback-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useState, useEffect } from 'react';
import classnames from 'classnames';
import Button from '../ui/button';
import CloseIcon from '../icons/close';
import { Mood } from '../mood/mood';
import { getHeap } from '../../tracking';
import { getDataTestAttribute } from '../../utils/get-data-test-attribute';
import { loadLocalStorage, saveLocalStorage } from '../../store/helpers';
import { localStorageFeedbackSeen } from '../../config';

import './feedback-form.scss';

export const FeedbackForm = ({ hideForm, title, usageContext }) => {
const [formStatus, setFormStatus] = useState('active'); // 'active', 'submitted', 'cancelled'
const [activeMood, setActiveMood] = useState(null);
const [feedbackText, setFeedbackText] = useState('');

const handleFormAction = (action) => {
setFormStatus(action);

const timer = setTimeout(() => {
updateLocalStorageUsageContext(false);
hideForm();
}, 4000);

return () => clearTimeout(timer);
};

const updateLocalStorageUsageContext = (value) => {
const existingData = loadLocalStorage(localStorageFeedbackSeen) || {};
existingData[usageContext] = value;
saveLocalStorage(localStorageFeedbackSeen, existingData);
};

useEffect(() => {
if (formStatus === 'submitted' || formStatus === 'cancelled') {
const timer = setTimeout(() => {
setFormStatus('active');
setActiveMood(null);
setFeedbackText('');
}, 4000);

return () => clearTimeout(timer);
}
}, [formStatus]);

const handleFormSubmit = (e) => {
e.preventDefault();
handleFormAction('submitted');
getHeap().track(getDataTestAttribute(usageContext, 'feedback-form'), {
rating: activeMood,
feedback: feedbackText,
});
};

const getMessages = () => {
if (formStatus === 'submitted') {
return 'Thank you for sharing feedback!';
}
if (formStatus === 'cancelled') {
return (
<>
You can provide feedback any time by using
<br />
the feedback button in the sliced view.
</>
);
}
};

if (formStatus === 'submitted' || formStatus === 'cancelled') {
return (
<div className="feedback-form--wrapper feedback-form--message">
{getMessages()}
</div>
);
} else {
return (
<div
className={classnames('feedback-form--wrapper', {
'feedback-form--wrapper-no-text-area': activeMood === null,
})}
>
<div
className="feedback-form--close-icon"
onClick={() => handleFormAction('cancelled')}
>
<CloseIcon />
</div>
<h2 className="feedback-form--title">{title}</h2>
<div className="feedback-form">
<Mood selectedMood={activeMood} onClick={setActiveMood} />
{activeMood !== null && (
<>
<textarea
className="feedback-form--text-area"
value={feedbackText}
onChange={(event) => setFeedbackText(event.target.value)}
placeholder="How can we improve this feature?"
/>
<Button type="submit" onClick={handleFormSubmit}>
Submit feedback
</Button>
</>
)}
</div>
</div>
);
}
};
120 changes: 120 additions & 0 deletions src/components/feedback-form/feedback-form.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
@use '../../styles/variables' as colors;

@keyframes fadeIn {
from {
opacity: 0;
}

to {
opacity: 1;
}
}

.kui-theme--dark {
--feedback-form-bg: #{colors.$white-0};
--feedback-form-content: #{colors.$slate-0};
--feedback-form-title: #{colors.$black-900};
--feedback-form--text-area--background: #{colors.$slate-200};
--feedback-form--text-area-placeholder--color: #{colors.$black-200};
}

.kui-theme--light {
--feedback-form-bg: #{colors.$black-900};
--feedback-form-content: #{colors.$white-0};
--feedback-form-title: #{colors.$white-0};
--feedback-form--text-area--background: #{colors.$white-0};
--feedback-form--text-area-placeholder--color: #{colors.$black-0};
}

.feedback-form--wrapper {
animation: fadeIn 0.5s ease-in;
background-color: var(--feedback-form-bg);
border-radius: 8px 0 0 8px;
min-width: 312px;
padding: 24px 24px 36px;
position: fixed;
right: 0;
top: 320px;

.button__btn {
margin-top: 12px;
}
}

.feedback-form--wrapper-no-text-area {
padding-bottom: 68px;
}

.feedback-form--text-area::placeholder {
color: var(--feedback-form--text-area-placeholder--color);
margin: auto; /* Centers the placeholder text vertically */
text-align: center;
font-family: Inter, sans-serif;
}

.feedback-form--text-area {
align-items: center;
background-color: transparent;
border: 1px solid grey;
color: var(--feedback-form-title);
display: flex;
font-size: 14px;
font-family: Inter, sans-serif;
height: 104px;
margin-top: 68px;
padding: 8px 12px;
resize: none;
text-align: left;
width: 364px;
}

.feedback-form--title {
color: var(--feedback-form-title);
font-size: 18px;
font-style: normal;
font-weight: 400;
line-height: 28px;
margin-top: 12px;
text-align: center;
margin-bottom: 24px;
}

.feedback-form--close-icon {
cursor: pointer;
height: 24px;
margin-left: auto;
width: 24px;

path {
fill: var(--feedback-form-title);
}
}

.feedback-form {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;

.button__btn {
border: 1px solid var(--feedback-form-title);
color: var(--feedback-form-title);
margin-top: 36px;
padding: 8px 12px;

&:hover {
background-color: var(--feedback-form-title);
color: var(--feedback-form-bg);
}
}
}

.feedback-form--message {
color: var(--feedback-form-title);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
padding: 24px 36px;
width: auto;
}
56 changes: 54 additions & 2 deletions src/components/flowchart/flowchart.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,25 @@ import { getDataTestAttribute } from '../../utils/get-data-test-attribute';
import Tooltip from '../ui/tooltip';
import { SlicedPipelineActionBar } from '../sliced-pipeline-action-bar/sliced-pipeline-action-bar';
import { SlicedPipelineNotification } from '../sliced-pipeline-notification/sliced-pipeline-notification';
import { FeedbackButton } from '../feedback-button/feedback-button';
import { FeedbackForm } from '../feedback-form/feedback-form';
import { loadLocalStorage } from '../../store/helpers';
import { localStorageFeedbackSeen } from '../../config';

import './styles/flowchart.scss';

export const feedbacks = {
slicingPipeline: {
formTitle: [
'How satisfied are you with',
<br key="1" />,
'pipeline slicing?',
],
buttonTittle: 'Feedback for pipeline slicing',
usageContext: 'slicing-pipeline',
},
};

/**
* Display a pipeline flowchart, mostly rendered with D3
*/
Expand All @@ -64,6 +81,8 @@ export class FlowChart extends Component {
range: [],
},
showSlicingNotification: false,
resetSlicingPipelineBtnClicked: false,
showFeedbackForm: false,
};
this.onViewChange = this.onViewChange.bind(this);
this.onViewChangeEnd = this.onViewChangeEnd.bind(this);
Expand Down Expand Up @@ -804,12 +823,25 @@ export class FlowChart extends Component {
visibleSlicing,
} = this.props;
const { outerWidth = 0, outerHeight = 0 } = chartSize;
const { showSlicingNotification } = this.state;
const {
showSlicingNotification,
resetSlicingPipelineBtnClicked,
showFeedbackForm,
} = this.state;

// Counts the nodes in the slicedPipeline array, excludes any modularPipeline Id
const numberOfNodesInSlicedPipeline = slicedPipeline.filter(
(id) => !modularPipelineIds.includes(id)
).length;

const isFirstTimeFeedbackAfterResetSlicing =
resetSlicingPipelineBtnClicked &&
loadLocalStorage(localStorageFeedbackSeen)['slicing-pipeline'] ===
undefined;

const seenSlicingFeedbackBefore =
loadLocalStorage(localStorageFeedbackSeen)['slicing-pipeline'] === false;

return (
<div
className="pipeline-flowchart kedro"
Expand Down Expand Up @@ -871,6 +903,22 @@ export class FlowChart extends Component {
})}
ref={this.layerNamesRef}
/>
<FeedbackButton
onClick={() => this.setState({ showFeedbackForm: true })}
title={feedbacks.slicingPipeline.buttonTittle}
visible={
isSlicingPipelineApplied &&
seenSlicingFeedbackBefore &&
!showFeedbackForm
}
/>
{(isFirstTimeFeedbackAfterResetSlicing || showFeedbackForm) && (
<FeedbackForm
hideForm={() => this.setState({ showFeedbackForm: false })}
title={feedbacks.slicingPipeline.formTitle}
usageContext={feedbacks.slicingPipeline.usageContext}
/>
)}
{showSlicingNotification && visibleSlicing && (
<SlicedPipelineNotification
notification={
Expand All @@ -879,14 +927,18 @@ export class FlowChart extends Component {
visibleSidebar={visibleSidebar}
/>
)}

{numberOfNodesInSlicedPipeline > 0 && runCommand.length > 0 && (
<div ref={this.slicedPipelineActionBarRef}>
<SlicedPipelineActionBar
chartSize={chartSize}
displayMetadataPanel={Boolean(clickedNode)}
isSlicingPipelineApplied={isSlicingPipelineApplied}
onApplySlicingPipeline={() => onApplySlice(true)}
onResetSlicingPipeline={this.resetSlicedPipeline}
onResetSlicingPipeline={() => {
this.resetSlicedPipeline();
this.setState({ resetSlicingPipelineBtnClicked: true });
}}
ref={this.slicedPipelineActionBarRef}
runCommand={runCommand}
slicedPipelineLength={numberOfNodesInSlicedPipeline}
Expand Down
Loading

0 comments on commit 490416d

Please sign in to comment.