1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-19 17:52:45 +02:00

chore: add project list view toggle with respective flag (#10452)

https://linear.app/unleash/issue/2-3746/add-project-list-view-toggle-with-respective-flag

Adds a project list view toggle hidden behind a feature flag:
`projectListViewToggle`.

This is already part of the persistent project list page state.

Even though the view mode switching logic is in place, this isn't really
doing anything else. We'll leave the actual visual changes (tables) for
a follow up PR.

<img width="1412" height="406" alt="image"
src="https://github.com/user-attachments/assets/793d0bd9-9874-4630-98b4-0ee364f50241"
/>
This commit is contained in:
Nuno Góis 2025-08-04 08:53:04 +01:00 committed by GitHub
parent 91f138349e
commit bd5a8539c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 81 additions and 27 deletions

View File

@ -19,6 +19,8 @@ import { ProjectArchiveLink } from './ProjectArchiveLink/ProjectArchiveLink.tsx'
import { ProjectsListHeader } from './ProjectsListHeader/ProjectsListHeader.tsx'; import { ProjectsListHeader } from './ProjectsListHeader/ProjectsListHeader.tsx';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { TablePlaceholder } from 'component/common/Table/index.ts'; import { TablePlaceholder } from 'component/common/Table/index.ts';
import { useUiFlag } from 'hooks/useUiFlag.ts';
import { ProjectsListViewToggle } from './ProjectsListViewToggle/ProjectsListViewToggle.tsx';
const StyledApiError = styled(ApiError)(({ theme }) => ({ const StyledApiError = styled(ApiError)(({ theme }) => ({
maxWidth: '500px', maxWidth: '500px',
@ -34,6 +36,7 @@ const StyledContainer = styled('div')(({ theme }) => ({
export const ProjectList = () => { export const ProjectList = () => {
const { projects, loading, error, refetch } = useProjects(); const { projects, loading, error, refetch } = useProjects();
const { isOss } = useUiConfig(); const { isOss } = useUiConfig();
const projectListViewToggleEnabled = useUiFlag('projectListViewToggle');
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
@ -126,14 +129,25 @@ export const ProjectList = () => {
<ProjectsListHeader <ProjectsListHeader
helpText='Favorite projects, projects you own, and projects you are a member of' helpText='Favorite projects, projects you own, and projects you are a member of'
actions={ actions={
<ProjectsListSort <>
sortBy={state.sortBy} {projectListViewToggleEnabled &&
setSortBy={(sortBy) => !isOss() && (
setState({ <ProjectsListViewToggle
sortBy: sortBy as typeof state.sortBy, view={state.view}
}) setView={(view) =>
} setState({ view })
/> }
/>
)}
<ProjectsListSort
sortBy={state.sortBy}
setSortBy={(sortBy) =>
setState({
sortBy: sortBy as typeof state.sortBy,
})
}
/>
</>
} }
> >
My projects My projects

View File

@ -25,6 +25,13 @@ const StyledHeaderTitle = styled('div')(({ theme }) => ({
flexGrow: 0, flexGrow: 0,
})); }));
const StyledHeaderActions = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
marginLeft: 'auto',
gap: theme.spacing(2),
}));
export const ProjectsListHeader: FC<ProjectsListHeaderProps> = ({ export const ProjectsListHeader: FC<ProjectsListHeaderProps> = ({
children, children,
helpText, helpText,
@ -36,7 +43,7 @@ export const ProjectsListHeader: FC<ProjectsListHeaderProps> = ({
{children} {children}
<HelpIcon tooltip={helpText} /> <HelpIcon tooltip={helpText} />
</StyledHeaderTitle> </StyledHeaderTitle>
{actions} <StyledHeaderActions>{actions}</StyledHeaderActions>
</StyledHeaderContainer> </StyledHeaderContainer>
); );
}; };

View File

@ -2,12 +2,6 @@ import type { FC } from 'react';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
const StyledWrapper = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'flex-end',
flex: 1,
}));
const StyledContainer = styled('div')(() => ({ const StyledContainer = styled('div')(() => ({
maxWidth: '200px', maxWidth: '200px',
width: '100%', width: '100%',
@ -35,16 +29,14 @@ export const ProjectsListSort: FC<ProjectsListSortProps> = ({
setSortBy, setSortBy,
}) => { }) => {
return ( return (
<StyledWrapper> <StyledContainer>
<StyledContainer> <GeneralSelect
<GeneralSelect fullWidth
fullWidth label='Sort by'
label='Sort by' onChange={setSortBy}
onChange={setSortBy} options={options}
options={options} value={sortBy || options[0].key}
value={sortBy || options[0].key} />
/> </StyledContainer>
</StyledContainer>
</StyledWrapper>
); );
}; };

View File

@ -0,0 +1,28 @@
import { IconButton, Tooltip } from '@mui/material';
import type { ProjectsListView } from '../hooks/useProjectsListState.js';
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
import GridViewIcon from '@mui/icons-material/GridView';
type ProjectsListViewToggleProps = {
view: ProjectsListView;
setView: (view: ProjectsListView) => void;
};
export const ProjectsListViewToggle = ({
view,
setView,
}: ProjectsListViewToggleProps) => {
const nextView = view === 'list' ? 'cards' : 'list';
return (
<Tooltip title={`Switch to ${nextView} view`} arrow>
<IconButton size='small' onClick={() => setView(nextView)}>
{nextView === 'list' ? (
<FormatListBulletedIcon />
) : (
<GridViewIcon />
)}
</IconButton>
</Tooltip>
);
};

View File

@ -7,6 +7,8 @@ import {
} from 'use-query-params'; } from 'use-query-params';
import { sortKeys } from '../ProjectsListSort/ProjectsListSort.jsx'; import { sortKeys } from '../ProjectsListSort/ProjectsListSort.jsx';
export type ProjectsListView = 'cards' | 'list';
const stateConfig = { const stateConfig = {
query: StringParam, query: StringParam,
sortBy: withDefault( sortBy: withDefault(
@ -14,6 +16,10 @@ const stateConfig = {
sortKeys[0], sortKeys[0],
) as QueryParamConfig<(typeof sortKeys)[number] | null | undefined>, ) as QueryParamConfig<(typeof sortKeys)[number] | null | undefined>,
create: StringParam, create: StringParam,
view: withDefault(
createEnumParam<ProjectsListView>(['cards', 'list']),
'cards',
) as QueryParamConfig<ProjectsListView>,
} as const; } as const;
export const useProjectsListState = () => export const useProjectsListState = () =>

View File

@ -95,6 +95,7 @@ export type UiFlags = {
reportUnknownFlags?: boolean; reportUnknownFlags?: boolean;
lifecycleGraphs?: boolean; lifecycleGraphs?: boolean;
addConfiguration?: boolean; addConfiguration?: boolean;
projectListViewToggle?: boolean;
}; };
export interface IVersionInfo { export interface IVersionInfo {

View File

@ -65,7 +65,8 @@ export type IFlagKey =
| 'timestampsInChangeRequestTimeline' | 'timestampsInChangeRequestTimeline'
| 'lifecycleGraphs' | 'lifecycleGraphs'
| 'githubAuth' | 'githubAuth'
| 'addConfiguration'; | 'addConfiguration'
| 'projectListViewToggle';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -305,6 +306,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_ADD_CONFIGURATION, process.env.UNLEASH_EXPERIMENTAL_ADD_CONFIGURATION,
false, false,
), ),
projectListViewToggle: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_PROJECT_LIST_VIEW_TOGGLE,
false,
),
}; };
export const defaultExperimentalOptions: IExperimentalOptions = { export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -61,6 +61,7 @@ process.nextTick(async () => {
timestampsInChangeRequestTimeline: true, timestampsInChangeRequestTimeline: true,
lifecycleGraphs: true, lifecycleGraphs: true,
addConfiguration: true, addConfiguration: true,
projectListViewToggle: true,
}, },
}, },
authentication: { authentication: {