Skip to content

Commit

Permalink
feat: Mobile-responsive Insight Viewer
Browse files Browse the repository at this point in the history
  • Loading branch information
baumandm committed Jan 24, 2022
1 parent 6533088 commit 9bec6f5
Show file tree
Hide file tree
Showing 10 changed files with 585 additions and 524 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,4 @@ npm run license:thirdparty

This project is available under the Apache 2.0 License.

Copyright 2020-2021 Expedia, Inc.
Copyright 2020-2022 Expedia, Inc.
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,29 @@ import { Flex, HStack, Skeleton, VStack } from '@chakra-ui/react';
export const InsightSkeleton = () => {
return (
<>
<Flex direction="column" justify="stretch" flexGrow={2}>
{/* Desktop-only */}
<Flex direction="column" justify="stretch" display={{ base: 'none', md: 'flex' }}>
{/* Header */}
<Flex direction="column" align="stretch" p="0.5rem">
<HStack spacing={0} height="40px" align="stretch">
{/* Item type icon */}
<Skeleton boxSize="40px" mr="0.5rem" />

{/* Item name */}
<Skeleton size="lg" w="33%" />

{/* Spacer */}
<Flex flexGrow={1} />
<Skeleton width="60px" mr="0.5rem" />
<Skeleton width="60px" mr="0.5rem" />
<Skeleton width="40px" mr="0.5rem" />
<Skeleton width="40px" mr="0.5rem" />
<Skeleton width="60px" mr="0.5rem" />

<HStack spacing="0.5rem" height="40px" align="stretch">
<Skeleton width="40px" />
<Skeleton width="40px" />
<Skeleton width="65px" />
<Skeleton width="65px" />
<Skeleton width="65px" />
<Skeleton width="40px" />
<Skeleton width="60px" />
</HStack>
</HStack>
</Flex>

Expand Down Expand Up @@ -72,13 +81,60 @@ export const InsightSkeleton = () => {
<Skeleton width="30%" height="1.5rem" mt="2rem" />
<Skeleton height="1.5rem" mt="0.5rem" />

<HStack spacing="1rem" height="40px" mt="2rem">
<Skeleton boxSize="40px" />
<Skeleton boxSize="40px" />
<HStack spacing="1rem" height="60px" mt="2rem">
<Skeleton boxSize="60px" />
<Skeleton boxSize="60px" />
</HStack>

<Skeleton width="30%" height="1.5rem" mt="2rem" />
<Skeleton height="1.5rem" mt="0.5rem" />

<Skeleton width="30%" height="1.5rem" mt="2rem" />
<Skeleton height="1.5rem" mt="0.5rem" />
</Flex>
</Flex>
</Flex>

{/* Mobile-only */}
<VStack align="stretch" justify="stretch" display={{ base: 'flex', md: 'none' }}>
{/* Header */}
<VStack spacing="0.5rem" align="stretch" flexGrow={1} overflow="hidden" p="0.5rem">
<HStack spacing="1rem" height="40px" flexGrow={1}>
<Skeleton height="32px" flexGrow={1} />
<Skeleton height="32px" flexGrow={1} />
</HStack>

<HStack spacing={0} height="40px" align="stretch">
{/* Item type icon */}
<Skeleton boxSize="40px" mr="0.5rem" />

{/* Item name */}
<Skeleton size="lg" flexGrow={1} />
</HStack>

<HStack spacing="0.5rem" height="40px" flexGrow={1} justify="space-between">
<Skeleton width="65px" height="40px" />
<Skeleton width="65px" height="40px" />
<Skeleton width="65px" height="40px" />
<Skeleton width="40px" height="40px" />
</HStack>
</VStack>

{/* Insight */}
<VStack spacing="0.5rem" align="stretch" flexGrow={1} overflow="hidden" p="0.5rem" pt="2rem">
<Skeleton mt="1rem" height="2rem" width="30%" />
<Skeleton height="1.5rem" />
<Skeleton height="1.5rem" />
<Skeleton height="1.5rem" />
<Skeleton height="1.5rem" width="74%" />
<Flex h="2rem" />
<Skeleton mt="1rem" height="2rem" width="30%" />
<Skeleton height="1.5rem" />
<Skeleton height="1.5rem" />
<Skeleton height="1.5rem" />
<Skeleton height="1.5rem" width="60%" />
</VStack>
</VStack>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/**
* Copyright 2022 Expedia, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
Box,
Button,
HStack,
Icon,
IconButton,
Menu,
MenuButton,
MenuDivider,
MenuGroup,
MenuItem,
MenuList,
useDisclosure,
useToast
} from '@chakra-ui/react';
import { useSelector } from 'react-redux';
import { Link as RouterLink } from 'react-router-dom';
import titleize from 'titleize';
import { gql, useMutation } from 'urql';

import { InsightCollaboratorsModal } from '../../../../../../components/insight-collaborators-modal/insight-collaborators-modal';
import { LikeButton } from '../../../../../../components/like-button/like-button';
import { LikedByTooltip } from '../../../../../../components/liked-by-tooltip/liked-by-tooltip';
import { NumberIconButton } from '../../../../../../components/number-icon-button/number-icon-button';
import { Insight, User } from '../../../../../../models/generated/graphql';
import { iconFactory, iconFactoryAs } from '../../../../../../shared/icon-factory';
import { RootState } from '../../../../../../store/store';
import { CloneDialog } from '../clone-dialog/clone-dialog';
import { DeleteDialog } from '../delete-dialog/delete-dialog';

const SYNC_INSIGHT_MUTATION = gql`
mutation SyncInsight($insightId: ID!) {
syncInsight(insightId: $insightId) {
id
}
}
`;

interface Props {
insight: Insight;
isExport: boolean;
nextInsight?: Pick<Insight, 'id' | 'name' | 'fullName' | 'itemType'>;
previousInsight?: Pick<Insight, 'id' | 'name' | 'fullName' | 'itemType'>;
onClone: () => Promise<boolean>;
onDelete: () => Promise<boolean>;
onFetchLikedBy: (insightId?: string) => Promise<User[]>;
onLike: (liked: boolean) => Promise<boolean>;
}

export const ActionBar = ({
insight,
nextInsight,
previousInsight,
isExport,
onClone,
onDelete,
onFetchLikedBy,
onLike
}: Props) => {
const { loggedIn } = useSelector((state: RootState) => state.user);

const toast = useToast();
const [, sync] = useMutation(SYNC_INSIGHT_MUTATION);

const syncInsight = async () => {
const result = await sync({
insightId: insight.id
});

if (result.error) {
toast({
position: 'bottom-right',
title: 'Unable to sync.',
status: 'error',
duration: 9000,
isClosable: true
});
return;
}

toast({
position: 'bottom-right',
title: 'Insight synced.',
status: 'success',
duration: 3000,
isClosable: true
});
};

// Collaborators modal
const { isOpen: isCollaboratorsOpen, onOpen: onCollaboratorsOpen, onClose: onCollaboratorsClose } = useDisclosure();

// Clone dialog
const { isOpen: isCloneOpen, onOpen: onCloneOpen, onClose: onCloneClose } = useDisclosure();

// Delete dialog
const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure();

// This indicates that the upstream repository is missing
// We can still show the cached insight, but with a warning message
const isMissing = insight.repository.isMissing;

// If the resource is missing we can't edit or clone.
const canEdit = !isMissing;
const canClone = !isMissing;

const likeLabel = insight.viewerHasLiked ? 'Unlike this Insight' : 'Like this Insight';

return (
<>
<HStack spacing="0" justify={{ base: 'space-between', md: 'unset' }}>
<RouterLink to={`/${insight.itemType}/${insight.fullName}/discuss`}>
<NumberIconButton label="Discussion" icon={iconFactoryAs('comments')} number={insight.commentCount} />
</RouterLink>

<LikedByTooltip
label={likeLabel}
likeCount={insight.likeCount}
onFetchLikedBy={() => onFetchLikedBy(insight.id)}
>
<LikeButton liked={insight.viewerHasLiked} label={likeLabel} onLike={onLike} likeCount={insight.likeCount} />
</LikedByTooltip>

<RouterLink to={`/activities/${encodeURIComponent(`insight:${insight.fullName} activityType:VIEW_INSIGHT`)}`}>
<NumberIconButton label="Views" icon={iconFactoryAs('views')} number={insight.viewCount + 1} />
</RouterLink>

<Menu>
<MenuButton as={Box} display="inline-block">
<IconButton variant="ghost" aria-label="Additional commands" icon={iconFactoryAs('optionsMenu')} />
</MenuButton>
<MenuList>
<MenuItem onClick={onCloneOpen} isDisabled={!canClone}>
<Icon as={iconFactory('clone')} mr="0.5rem" />
Clone {titleize(insight.itemType)}
</MenuItem>

<MenuDivider />

<RouterLink to={`/${insight.itemType}/${insight.fullName}/activity`}>
<MenuItem>
<Icon as={iconFactory('activities')} mr="0.5rem" />
Activity
</MenuItem>
</RouterLink>
<RouterLink to={`/${insight.itemType}/${insight.fullName}/json`}>
<MenuItem>
<Icon as={iconFactory('json')} mr="0.5rem" />
View JSON
</MenuItem>
</RouterLink>

<MenuDivider />

<MenuItem onClick={syncInsight}>
<Icon as={iconFactory('sync')} mr="0.5rem" />
Sync Now
</MenuItem>

{loggedIn && (
<>
<MenuDivider />
<MenuGroup title="Admin">
<MenuItem onClick={onCollaboratorsOpen} isDisabled={insight.viewerPermission !== 'ADMIN'}>
<Icon as={iconFactory('permissions')} mr="0.5rem" />
Collaborators
</MenuItem>

<MenuItem
onClick={onDeleteOpen}
isDisabled={!insight.repository.isMissing && insight.viewerPermission !== 'ADMIN'}
>
<Icon as={iconFactory('trash')} mr="0.5rem" />
Delete {titleize(insight.itemType)}
</MenuItem>
</MenuGroup>
</>
)}
</MenuList>
</Menu>
</HStack>

<RouterLink to={`/${insight.itemType}/${insight.fullName}/edit`}>
<Button width={{ base: '100%', md: 'unset' }} variant="frost" isDisabled={!canEdit}>
Edit
</Button>
</RouterLink>

<CloneDialog insight={insight} isOpen={isCloneOpen} onClone={onClone} onClose={onCloneClose} />

<DeleteDialog insight={insight} isOpen={isDeleteOpen} onDelete={onDelete} onClose={onDeleteClose} />

<InsightCollaboratorsModal insightId={insight.id} isOpen={isCollaboratorsOpen} onClose={onCollaboratorsClose} />
</>
);
};

0 comments on commit 9bec6f5

Please sign in to comment.