Skip to content

Commit

Permalink
feat: favorite feature and project (#2582)
Browse files Browse the repository at this point in the history
## About the changes
Add an ability to star a toggle from it's overiew.

Co-authored-by: sjaanus <sellinjaanus@gmail.com>
  • Loading branch information
Tymek and sjaanus committed Dec 2, 2022
1 parent a232119 commit 79e96fd
Show file tree
Hide file tree
Showing 16 changed files with 370 additions and 247 deletions.
@@ -0,0 +1,52 @@
import React, { VFC } from 'react';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { IconButton } from '@mui/material';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
import {
Star as StarIcon,
StarBorder as StarBorderIcon,
} from '@mui/icons-material';

interface IFavoriteIconButtonProps {
onClick: (event?: any) => void;
isFavorite: boolean;
size?: 'medium' | 'large';
}

export const FavoriteIconButton: VFC<IFavoriteIconButtonProps> = ({
onClick,
isFavorite,
size = 'large',
}) => {
return (
<IconButton size={size} data-loading sx={{ mr: 1 }} onClick={onClick}>
<ConditionallyRender
condition={isFavorite}
show={
<StarIcon
color="primary"
sx={{
fontSize: theme =>
size === 'medium'
? theme.spacing(2)
: theme.spacing(3),
}}
/>
}
elseShow={
<StarBorderIcon
sx={{
fontSize: theme =>
size === 'medium'
? theme.spacing(2)
: theme.spacing(3),
}}
/>
}
/>
</IconButton>
);
};
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState, VFC } from 'react';
import { useCallback, useEffect, useMemo, useState, VFC } from 'react';
import { Link, useMediaQuery, useTheme } from '@mui/material';
import { Link as RouterLink, useSearchParams } from 'react-router-dom';
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
Expand Down Expand Up @@ -50,7 +50,7 @@ export const FeatureToggleListTable: VFC = () => {
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
const { features = [], loading } = useFeatures();
const { features = [], loading, refetchFeatures } = useFeatures();
const [searchParams, setSearchParams] = useSearchParams();
const [initialState] = useState(() => ({
sortBy: [
Expand All @@ -73,6 +73,17 @@ export const FeatureToggleListTable: VFC = () => {
const [searchValue, setSearchValue] = useState(initialState.globalFilter);
const { favorite, unfavorite } = useFavoriteFeaturesApi();
const { uiConfig } = useUiConfig();
const onFavorite = useCallback(
async (feature: any) => {
if (feature?.favorite) {
await unfavorite(feature.project, feature.name);
} else {
await favorite(feature.project, feature.name);
}
refetchFeatures();
},
[favorite, refetchFeatures, unfavorite]
);

const columns = useMemo(
() => [
Expand All @@ -89,17 +100,7 @@ export const FeatureToggleListTable: VFC = () => {
Cell: ({ row: { original: feature } }: any) => (
<FavoriteIconCell
value={feature?.favorite}
onClick={() =>
feature?.favorite
? unfavorite(
feature.project,
feature.name
)
: favorite(
feature.project,
feature.name
)
}
onClick={() => onFavorite(feature)}
/>
),
maxWidth: 50,
Expand Down
Expand Up @@ -20,7 +20,7 @@ export const useStyles = makeStyles()(theme => ({
display: 'flex',
},
innerContainer: {
padding: '1rem 2rem',
padding: theme.spacing(2, 4, 2, 2),
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
Expand Down
40 changes: 36 additions & 4 deletions frontend/src/component/feature/FeatureView/FeatureView.tsx
@@ -1,6 +1,13 @@
import { Tab, Tabs, useMediaQuery } from '@mui/material';
import React, { useState } from 'react';
import { Archive, FileCopy, Label, WatchLater } from '@mui/icons-material';
import { IconButton, Tab, Tabs, useMediaQuery } from '@mui/material';
import React, { useCallback, useState } from 'react';
import {
Archive,
FileCopy,
Label,
WatchLater,
Star as StarIcon,
StarBorder as StarBorderIcon,
} from '@mui/icons-material';
import {
Link,
Route,
Expand Down Expand Up @@ -29,18 +36,23 @@ import AddTagDialog from './FeatureOverview/AddTagDialog/AddTagDialog';
import { FeatureStatusChip } from 'component/common/FeatureStatusChip/FeatureStatusChip';
import { FeatureNotFound } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureArchiveDialog } from '../../common/FeatureArchiveDialog/FeatureArchiveDialog';
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
import { DraftBanner } from 'component/changeRequest/DraftBanner/DraftBanner';
import { MainLayout } from 'component/layout/MainLayout/MainLayout';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';

export const FeatureView = () => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { refetch: projectRefetch } = useProject(projectId);
const { favorite, unfavorite } = useFavoriteFeaturesApi();
const { refetchFeature } = useFeature(projectId, featureId);
const { isChangeRequestConfiguredInAnyEnv } =
useChangeRequestsEnabled(projectId);
const { uiConfig } = useUiConfig();

const [openTagDialog, setOpenTagDialog] = useState(false);
const [showDelDialog, setShowDelDialog] = useState(false);
Expand Down Expand Up @@ -85,6 +97,15 @@ export const FeatureView = () => {
return <FeatureNotFound />;
}

const onFavorite = async () => {
if (feature?.favorite) {
await unfavorite(projectId, feature.name);
} else {
await favorite(projectId, feature.name);
}
refetchFeature();
};

return (
<MainLayout
ref={ref}
Expand All @@ -101,6 +122,17 @@ export const FeatureView = () => {
<div className={styles.header}>
<div className={styles.innerContainer}>
<div className={styles.toggleInfoContainer}>
<ConditionallyRender
condition={Boolean(
uiConfig?.flags?.favorites
)}
show={() => (
<FavoriteIconButton
onClick={onFavorite}
isFavorite={feature?.favorite}
/>
)}
/>
<h1
className={styles.featureViewHeader}
data-loading
Expand Down
27 changes: 24 additions & 3 deletions frontend/src/component/project/Project/Project.tsx
Expand Up @@ -7,7 +7,7 @@ import { styled, Tab, Tabs } from '@mui/material';
import { Delete, Edit } from '@mui/icons-material';
import useToast from 'hooks/useToast';
import useQueryParams from 'hooks/useQueryParams';
import { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { ProjectAccess } from '../ProjectAccess/ProjectAccess';
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
import { ProjectFeaturesArchive } from './ProjectFeaturesArchive/ProjectFeaturesArchive';
Expand All @@ -29,6 +29,8 @@ import { MainLayout } from 'component/layout/MainLayout/MainLayout';
import { ProjectChangeRequests } from '../../changeRequest/ProjectChangeRequests/ProjectChangeRequests';
import { ProjectSettings } from './ProjectSettings/ProjectSettings';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { FavoriteIconButton } from '../../common/FavoriteIconButton/FavoriteIconButton';
import { useFavoriteProjectsApi } from '../../../hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi';

const StyledDiv = styled('div')(() => ({
display: 'flex',
Expand All @@ -52,17 +54,18 @@ const StyledText = styled(StyledTitle)(({ theme }) => ({
const Project = () => {
const projectId = useRequiredPathParam('projectId');
const params = useQueryParams();
const { project, loading } = useProject(projectId);
const { project, loading, refetch } = useProject(projectId);
const ref = useLoading(loading);
const { setToastData } = useToast();
const { classes: styles } = useStyles();
const navigate = useNavigate();
const { pathname } = useLocation();
const { isOss } = useUiConfig();
const { isOss, uiConfig } = useUiConfig();
const basePath = `/projects/${projectId}`;
const projectName = project?.name || projectId;
const { isChangeRequestConfiguredInAnyEnv, isChangeRequestFlagEnabled } =
useChangeRequestsEnabled(projectId);
const { favorite, unfavorite } = useFavoriteProjectsApi();

const [showDelDialog, setShowDelDialog] = useState(false);

Expand Down Expand Up @@ -144,6 +147,15 @@ const Project = () => {
/* eslint-disable-next-line */
}, []);

const onFavorite = async () => {
if (project?.favorite) {
await unfavorite(projectId);
} else {
await favorite(projectId);
}
refetch();
};

return (
<MainLayout
ref={ref}
Expand All @@ -155,6 +167,15 @@ const Project = () => {
>
<div className={styles.header}>
<div className={styles.innerContainer}>
<ConditionallyRender
condition={Boolean(uiConfig?.flags?.favorites)}
show={() => (
<FavoriteIconButton
onClick={onFavorite}
isFavorite={project?.favorite}
/>
)}
/>
<h2 className={styles.title}>
<div>
<StyledName data-loading>{projectName}</StyledName>
Expand Down
Expand Up @@ -2,7 +2,7 @@ import { makeStyles } from 'tss-react/mui';

export const useStyles = makeStyles()(theme => ({
projectCard: {
padding: '1rem',
padding: theme.spacing(1, 2, 2, 2),
width: '220px',
height: '204px',
display: 'flex',
Expand All @@ -22,7 +22,6 @@ export const useStyles = makeStyles()(theme => ({
header: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
},
title: {
fontWeight: 'normal',
Expand Down Expand Up @@ -54,6 +53,8 @@ export const useStyles = makeStyles()(theme => ({
},
actionsBtn: {
transform: 'translateX(15px)',
marginLeft: 'auto',
marginRight: theme.spacing(1),
},
icon: {
color: theme.palette.grey[700],
Expand Down
31 changes: 29 additions & 2 deletions frontend/src/component/project/ProjectCard/ProjectCard.tsx
Expand Up @@ -14,8 +14,11 @@ import {
import AccessContext from 'contexts/AccessContext';
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton';
import { DeleteProjectDialogue } from '../Project/DeleteProject/DeleteProjectDialogue';
import { ConditionallyRender } from '../../common/ConditionallyRender/ConditionallyRender';

interface IProjectCardProps {
name: string;
Expand All @@ -24,6 +27,7 @@ interface IProjectCardProps {
memberCount: number;
id: string;
onHover: () => void;
isFavorite?: boolean;
}

export const ProjectCard = ({
Expand All @@ -33,13 +37,16 @@ export const ProjectCard = ({
memberCount,
onHover,
id,
isFavorite = false,
}: IProjectCardProps) => {
const { classes } = useStyles();
const { hasAccess } = useContext(AccessContext);
const { isOss } = useUiConfig();
const { isOss, uiConfig } = useUiConfig();
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
const [showDelDialog, setShowDelDialog] = useState(false);
const navigate = useNavigate();
const { favorite, unfavorite } = useFavoriteProjectsApi();
const { refetch } = useProjects();

const handleClick = (event: React.SyntheticEvent) => {
event.preventDefault();
Expand All @@ -49,9 +56,29 @@ export const ProjectCard = ({
const canDeleteProject =
hasAccess(DELETE_PROJECT, id) && id !== DEFAULT_PROJECT_ID;

const onFavorite = async (e: Event) => {
e.preventDefault();
if (isFavorite) {
await unfavorite(id);
} else {
await favorite(id);
}
refetch();
};

return (
<Card className={classes.projectCard} onMouseEnter={onHover}>
<div className={classes.header} data-loading>
<ConditionallyRender
condition={Boolean(uiConfig?.flags?.favorites)}
show={() => (
<FavoriteIconButton
onClick={onFavorite}
isFavorite={isFavorite}
size="medium"
/>
)}
/>
<h2 className={classes.title}>{name}</h2>

<PermissionIconButton
Expand Down

0 comments on commit 79e96fd

Please sign in to comment.