Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ API_BACKEND_URL=
# Replace this value with a strong, randomly generated string (at least 32 characters).
# Example for generation in Node.js: require('crypto').randomBytes(32).toString('hex')
COOKIE_SECRET=

FEEDBACK_SLACK_URL=
FEEDBACK_URL_LINK=
9 changes: 8 additions & 1 deletion public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,14 @@
},
"ShellBar": {
"betaButtonDescription": "This web app is currently in Beta, and may not ready for productive use. We're actively improving the experience and would love your feedback — your input helps shape the future of the app!",
"signOutButton": "Sign Out"
"signOutButton": "Sign Out",
"feedbackMessageLabel": "Message",
"feedbackRatingLabel": "Rating",
"feedbackHeader": "Your feedback",
"feedbackButtonInfo": "Give us your feedback",
"feedbackPlaceholder": "Please let us know what you think about our application",
"feedbackNotification": "*Slack notification with your email address will be shared with our Operations Team. If you have a special Feature request in mind, please create here.",
"feedbackThanks": "Thank you for your feedback!"
},
"CreateProjectDialog": {
"toastMessage": "Project creation triggered. The list will refresh automatically once completed."
Expand Down
2 changes: 2 additions & 0 deletions server/config/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const schema = {
POST_LOGIN_REDIRECT: { type: "string" },
COOKIE_SECRET: { type: "string" },
API_BACKEND_URL: { type: "string" },
FEEDBACK_SLACK_URL: { type: "string" },
FEEDBACK_URL_LINK: { type: "string" },

// System variables
NODE_ENV: { type: "string", enum: ["development", "production"] },
Expand Down
35 changes: 35 additions & 0 deletions server/routes/feedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import fetch from 'node-fetch';
import fp from "fastify-plugin";

async function feedbackRoute(fastify) {
const { FEEDBACK_SLACK_URL } = fastify.config;

fastify.post('/feedback', async (request, reply) => {
const { message, rating, user, environment } = request.body;

if (!message || !rating || !user || !environment) {
return reply.status(400).send({ error: 'Missing required fields' });
}

try {
const res = await fetch(FEEDBACK_SLACK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message, rating, user, environment }),
});

if (!res.ok) {
return reply.status(500).send({ error: 'Slack API error' });
}
return reply.send({ message: res, });
} catch (err) {
fastify.log.error('Slack error:', err);
return reply.status(500).send({ error: 'Request failed' });
}
});
}

export default fp(feedbackRoute);

190 changes: 188 additions & 2 deletions src/components/Core/ShellBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,59 @@ import {
Avatar,
Button,
ButtonDomRef,
Form,
FormGroup,
FormItem,
Icon,
Label,
List,
ListItemStandard,
Popover,
PopoverDomRef,
RatingIndicator,
ShellBar,
ShellBarDomRef,
ShellBarItem,
ShellBarItemDomRef,
TextArea,
TextAreaDomRef,
Ui5CustomEvent,
} from '@ui5/webcomponents-react';
import { useAuth } from '../../spaces/onboarding/auth/AuthContext.tsx';
import { RefObject, useEffect, useRef, useState } from 'react';
import {
Dispatch,
RefObject,
SetStateAction,
useEffect,
useRef,
useState,
} from 'react';
import { ShellBarProfileClickEventDetail } from '@ui5/webcomponents-fiori/dist/ShellBar.js';
import PopoverPlacement from '@ui5/webcomponents/dist/types/PopoverPlacement.js';
import { useTranslation } from 'react-i18next';
import { generateInitialsForEmail } from '../Helper/generateInitialsForEmail.ts';
import styles from './ShellBar.module.css';
import { ThemingParameters } from '@ui5/webcomponents-react-base';
import { ShellBarItemClickEventDetail } from '@ui5/webcomponents-fiori/dist/ShellBarItem.js';
import { t } from 'i18next';

type UI5RatingIndicatorElement = HTMLElement & { value: number };

export function ShellBarComponent() {
const auth = useAuth();
const profilePopoverRef = useRef<PopoverDomRef>(null);
const betaPopoverRef = useRef<PopoverDomRef>(null);
const feedbackPopoverRef = useRef<PopoverDomRef>(null);
const [profilePopoverOpen, setProfilePopoverOpen] = useState(false);
const [betaPopoverOpen, setBetaPopoverOpen] = useState(false);
const [feedbackPopoverOpen, setFeedbackPopoverOpen] = useState(false);
const [rating, setRating] = useState(0);
const [feedbackMessage, setFeedbackMessage] = useState('');
const [feedbackSent, setFeedbackSent] = useState(false);
const betaButtonRef = useRef<ButtonDomRef>(null);

const { user } = useAuth();

const onProfileClick = (
e: Ui5CustomEvent<ShellBarDomRef, ShellBarProfileClickEventDetail>,
) => {
Expand All @@ -42,6 +69,45 @@ export function ShellBarComponent() {
}
};

const onFeedbackClick = (
e: Ui5CustomEvent<ShellBarItemDomRef, ShellBarItemClickEventDetail>,
) => {
feedbackPopoverRef.current!.opener = e.detail.targetRef;
setFeedbackPopoverOpen(!feedbackPopoverOpen);
};

const onFeedbackMessageChange = (
event: Ui5CustomEvent<
TextAreaDomRef,
{ value: string; previousValue: string }
>,
) => {
const newValue = event.target.value;
setFeedbackMessage(newValue);
};

async function onFeedbackSent() {
const payload = {
message: feedbackMessage,
rating: rating.toString(),
user: user?.email,
environment: window.location.hostname,
};
try {
await fetch('/api/feedback', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
} catch (err) {
console.log(err);
} finally {
setFeedbackSent(true);
}
}

useEffect(() => {
const shellbar = document.querySelector('ui5-shellbar');
const el = shellbar?.shadowRoot?.querySelector(
Expand Down Expand Up @@ -82,7 +148,13 @@ export function ShellBarComponent() {
</div>
}
onProfileClick={onProfileClick}
/>
>
<ShellBarItem
icon="feedback"
text={t('ShellBar.feedbackNotification')}
onClick={onFeedbackClick}
/>
</ShellBar>

<ProfilePopover
open={profilePopoverOpen}
Expand All @@ -94,6 +166,17 @@ export function ShellBarComponent() {
setOpen={setBetaPopoverOpen}
popoverRef={betaPopoverRef}
/>
<FeedbackPopover
open={feedbackPopoverOpen}
setOpen={setFeedbackPopoverOpen}
popoverRef={feedbackPopoverRef}
setRating={setRating}
rating={rating}
feedbackMessage={feedbackMessage}
feedbackSent={feedbackSent}
onFeedbackSent={onFeedbackSent}
onFeedbackMessageChange={onFeedbackMessageChange}
/>
</>
);
}
Expand Down Expand Up @@ -163,3 +246,106 @@ const BetaPopover = ({
</Popover>
);
};

const FeedbackPopover = ({
open,
setOpen,
popoverRef,
setRating,
rating,
onFeedbackSent,
feedbackMessage,
onFeedbackMessageChange,
feedbackSent,
}: {
open: boolean;
setOpen: (arg0: boolean) => void;
popoverRef: RefObject<PopoverDomRef | null>;
setRating: Dispatch<SetStateAction<number>>;
rating: number;
onFeedbackSent: () => void;
feedbackMessage: string;
onFeedbackMessageChange: (
event: Ui5CustomEvent<
TextAreaDomRef,
{
value: string;
previousValue: string;
}
>,
) => void;
feedbackSent: boolean;
}) => {
const { t } = useTranslation();

const onRatingChange = (
event: Event & { target: UI5RatingIndicatorElement },
) => {
setRating(event.target.value);
};

return (
<>
<Popover
ref={popoverRef}
placement={PopoverPlacement.Bottom}
open={open}
onClose={() => setOpen(false)}
>
<div
style={{
padding: '1rem',
width: '250px',
}}
>
{!feedbackSent ? (
<Form headerText={t('ShellBar.feedbackHeader')}>
<FormGroup>
<FormItem
labelContent={
<Label style={{ color: 'black' }}>
{t('ShellBar.feedbackRatingLabel')}
</Label>
}
>
<RatingIndicator
value={rating}
max={5}
onChange={onRatingChange}
/>
</FormItem>
<FormItem
className="formAlignLabelStart"
labelContent={
<Label style={{ color: 'black' }}>
{t('ShellBar.feedbackMessageLabel')}
</Label>
}
>
<TextArea
value={feedbackMessage}
placeholder={t('ShellBar.feedbackPlaceholder')}
rows={5}
onInput={onFeedbackMessageChange}
/>
</FormItem>
<FormItem>
<Button design="Emphasized" onClick={() => onFeedbackSent()}>
Send Feedback
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing translation

</Button>
</FormItem>
<FormItem>
<Label style={{ color: 'gray' }}>
{t('ShellBar.feedbackNotification')}
</Label>
</FormItem>
</FormGroup>
</Form>
) : (
<Label>{t('ShellBar.feedbackThanks')}</Label>
)}
</div>
</Popover>
</>
);
};
Loading