Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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=
10 changes: 9 additions & 1 deletion public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,15 @@
},
"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",
"feedbackButton": "Send 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()}>
{t('ShellBar.feedbackButton')}
</Button>
</FormItem>
<FormItem>
<Label style={{ color: 'gray' }}>
{t('ShellBar.feedbackNotification')}
</Label>
</FormItem>
</FormGroup>
</Form>
) : (
<Label>{t('ShellBar.feedbackThanks')}</Label>
)}
</div>
</Popover>
</>
);
};
Loading