Skip to content
Draft
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
5 changes: 5 additions & 0 deletions src/common/Pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import Profile from 'src/pages/Profile';
import Template from 'src/pages/Template';
import Templates from 'src/pages/Templates';
import Video from 'src/pages/Video';
import VideoShared from 'src/pages/VideoShared';
import Videos from 'src/pages/Videos';
import { Redirect } from './Redirect';

Expand Down Expand Up @@ -111,6 +112,10 @@ const Pages = () => {
path={`/${langPrefix}/campaigns/:campaignId/videos/:videoId`}
element={<Video />}
/>
<Route
path={`/${langPrefix}/campaigns/:campaignId/videos/:videoId/shared`}
element={<VideoShared />}
/>
<Route
path={`/${langPrefix}/plans/:planId`}
element={<Plan />}
Expand Down
144 changes: 144 additions & 0 deletions src/pages/VideoShared/Actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { LG, Skeleton, Tag, XL } from '@appquality/unguess-design-system';
import { useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { appTheme } from 'src/app/theme';
import { ReactComponent as ClockIcon } from 'src/assets/icons/time-icon.svg';
import { capitalizeFirstLetter } from 'src/common/capitalizeFirstLetter';
import { getDeviceIcon } from 'src/common/components/BugDetail/Meta';
import { Divider } from 'src/common/components/divider';
import { Meta } from 'src/common/components/Meta';
import { Pipe } from 'src/common/components/Pipe';
import {
useGetVideosByVidObservationsQuery,
useGetVideosByVidQuery,
} from 'src/features/api';
import styled from 'styled-components';
import { formatDuration } from '../Videos/utils/formatDuration';
import { getSeverityTagsByVideoCount } from '../Videos/utils/getSeverityTagsWithCount';
import { NoObservations } from './components/NoObservations';
import { Observation } from './components/Observation';
import { SentimentOverview } from './components/SentimentOverview';

const Container = styled.div`
display: flex;
flex-direction: column;
position: sticky;
top: 0;
width: 100%;
background-color: white;
height: 100vh;
padding: ${({ theme }) => theme.space.md} ${({ theme }) => theme.space.md};
overflow-y: auto;
border-left: 1px solid ${({ theme }) => theme.palette.grey[200]};
scroll-behavior: smooth;
`;
const MetaContainer = styled.div`
display: flex;
align-items: center;
margin-top: ${({ theme }) => theme.space.sm};
margin-bottom: ${({ theme }) => theme.space.xs};
`;

const ObservationsCountWrapper = styled.div`
display: flex;
align-items: center;
flex-wrap: wrap;
row-gap: ${({ theme }) => theme.space.xxs};
margin-top: ${({ theme }) => theme.space.sm};
`;

const Actions = () => {
const { videoId } = useParams();
const refScroll = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const {
data: video,
isFetching: isFetchingVideo,
isLoading: isLoadingVideo,
isError: isErrorVideo,
} = useGetVideosByVidQuery({
vid: videoId || '',
});

const {
data: observations,
isFetching: isFetchingObservations,
isLoading: isLoadingObservations,
isError: isErrorObservations,
} = useGetVideosByVidObservationsQuery({
vid: videoId || '',
});

const severities = observations
? getSeverityTagsByVideoCount(observations)
: [];

if (!video || isErrorVideo) return null;
if (!observations || isErrorObservations) return null;

if (isFetchingVideo || isLoadingVideo) return <Skeleton />;
if (isFetchingObservations || isLoadingObservations) return <Skeleton />;

return (
<Container ref={refScroll}>
<XL isBold style={{ marginTop: 130 }}>
{video.tester.name}
</XL>
<MetaContainer>
<Meta size="medium">Tester ID: {video.tester.id}</Meta>
<Pipe />
{video.tester.device && (
<Tag hue="white" style={{ textTransform: 'capitalize' }}>
<Tag.Avatar>{getDeviceIcon(video.tester.device.type)}</Tag.Avatar>
{video.tester.device.type}
</Tag>
)}
{video.duration && (
<Tag hue="white" style={{ fontSize: appTheme.fontSizes.sm }}>
<Tag.Avatar>
<ClockIcon />
</Tag.Avatar>
{formatDuration(video.duration)}
</Tag>
)}
</MetaContainer>
<Divider />
<SentimentOverview />
<div style={{ padding: `${appTheme.space.md} 0` }}>
<LG isBold data-qa="tagging_tool_page_title_observations">
{t('__OBSERVATIONS_DRAWER_TOTAL')}: {observations.length}
</LG>
{observations && severities && severities.length > 0 && (
<ObservationsCountWrapper>
{severities.map((severity) => (
<Meta
size="large"
color={severity.style}
secondaryText={severity.count}
>
{capitalizeFirstLetter(severity.name)}
</Meta>
))}
</ObservationsCountWrapper>
)}
</div>
{observations && observations.length ? (
observations.map((observation) => (
<Observation
refScroll={refScroll}
key={observation.id}
observation={observation}
{...(video.transcript && {
transcript: video.transcript,
})}
/>
))
) : (
<NoObservations />
)}
</Container>
);
};

export default Actions;
24 changes: 24 additions & 0 deletions src/pages/VideoShared/Content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Col, Grid, Row } from '@appquality/unguess-design-system';
import { LayoutWrapper } from 'src/common/components/LayoutWrapper';
import Actions from './Actions';
import { VideoPlayer } from './components/Player';
import { VideoContextProvider } from './context/VideoContext';

const VideoPageContent = () => (
<VideoContextProvider>
<LayoutWrapper isNotBoxed style={{ padding: 0 }}>
<Grid gutters="xxl">
<Row>
<Col lg={8} style={{ margin: 0, paddingRight: 0 }}>
<VideoPlayer />
</Col>
<Col lg={4} style={{ margin: 0, paddingLeft: 0 }}>
<Actions />
</Col>
</Row>
</Grid>
</LayoutWrapper>
</VideoContextProvider>
);

export default VideoPageContent;
77 changes: 77 additions & 0 deletions src/pages/VideoShared/components/ConfirmDeleteModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
Button,
Modal,
ModalClose,
useToast,
Notification,
} from '@appquality/unguess-design-system';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { useDeleteVideosByVidObservationsAndOidMutation } from 'src/features/api';

export const ConfirmDeleteModal = ({
observationId,
setIsConfirmationModalOpen,
}: {
observationId: number;
setIsConfirmationModalOpen: (isOpen: boolean) => void;
}) => {
const { videoId } = useParams();
const { t } = useTranslation();
const { addToast } = useToast();
const [deleteObservation] = useDeleteVideosByVidObservationsAndOidMutation();

const onQuit = () => {
setIsConfirmationModalOpen(false);
};

const onContinue = async () => {
deleteObservation({ vid: videoId || '', oid: observationId.toString() })
.unwrap()
.then(() => {
addToast(
({ close }) => (
<Notification
onClose={close}
type="success"
message={t(
'__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_DELETE_TOAST_SUCCESS'
)}
closeText={t('__TOAST_CLOSE_TEXT')}
isPrimary
/>
),
{ placement: 'top' }
);
setIsConfirmationModalOpen(false);
})
.catch(() => {});
};

return (
<Modal onClose={onQuit}>
<Modal.Header isDanger>
{t('__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_DELETE_MODAL_HEADER_TITLE')}
</Modal.Header>
<Modal.Body>
{t('__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_DELETE_MODAL_BODY_TEXT')}
</Modal.Body>
<Modal.Footer>
<Button
style={{ paddingRight: 20 }}
isDanger
isLink
onClick={onContinue}
>
{t(
'__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_DELETE_MODAL_CONTINUE_BUTTON'
)}
</Button>
<Button isPrimary isAccent onClick={onQuit}>
{t('__VIDEO_PAGE_ACTIONS_OBSERVATION_FORM_DELETE_MODAL_QUIT_BUTTON')}
</Button>
</Modal.Footer>
<ModalClose onClick={onQuit} />
</Modal>
);
};
20 changes: 20 additions & 0 deletions src/pages/VideoShared/components/NoObservations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Paragraph, XXL } from '@appquality/unguess-design-system';
import { useTranslation } from 'react-i18next';
import { appTheme } from 'src/app/theme';
import { ReactComponent as EmptyImage } from 'src/assets/empty-observations.svg';

const NoObservations = () => {
const { t } = useTranslation();

return (
<>
<EmptyImage />
<XXL isBold style={{ marginTop: appTheme.space.md }}>
{t('__VIDEO_PAGE_NO_OBSERVATIONS_TITLE')}
</XXL>
<Paragraph>{t('__VIDEO_PAGE_NO_OBSERVATIONS')}</Paragraph>
</>
);
};

export { NoObservations };
Loading
Loading