diff options
Diffstat (limited to 'webui')
-rw-r--r-- | webui/src/components/Label.tsx | 41 | ||||
-rw-r--r-- | webui/src/pages/bug/Bug.tsx | 15 | ||||
-rw-r--r-- | webui/src/pages/bug/labels/LabelMenu.tsx | 356 | ||||
-rw-r--r-- | webui/src/pages/bug/labels/SetLabel.graphql | 13 | ||||
-rw-r--r-- | webui/src/pages/list/BugRow.tsx | 3 | ||||
-rw-r--r-- | webui/src/pages/list/Filter.tsx | 31 | ||||
-rw-r--r-- | webui/src/pages/list/FilterToolbar.tsx | 1 | ||||
-rw-r--r-- | webui/src/pages/list/ListLabels.graphql | 3 |
8 files changed, 424 insertions, 39 deletions
diff --git a/webui/src/components/Label.tsx b/webui/src/components/Label.tsx index 111f6d7f..13c913c9 100644 --- a/webui/src/components/Label.tsx +++ b/webui/src/components/Label.tsx @@ -1,56 +1,43 @@ import React from 'react'; +import { Chip } from '@material-ui/core'; import { common } from '@material-ui/core/colors'; -import { makeStyles } from '@material-ui/core/styles'; import { - getContrastRatio, darken, + getContrastRatio, } from '@material-ui/core/styles/colorManipulator'; -import { LabelFragment } from '../graphql/fragments.generated'; -import { Color } from 'src/gqlTypes'; +import { Color } from '../gqlTypes'; + +import { LabelFragment } from './fragments.generated'; + +const _rgb = (color: Color) => + 'rgb(' + color.R + ',' + color.G + ',' + color.B + ')'; // Minimum contrast between the background and the text color const contrastThreshold = 2.5; - // Guess the text color based on the background color const getTextColor = (background: string) => getContrastRatio(background, common.white) >= contrastThreshold ? common.white // White on dark backgrounds : common.black; // And black on light ones -const _rgb = (color: Color) => - 'rgb(' + color.R + ',' + color.G + ',' + color.B + ')'; - // Create a style object from the label RGB colors const createStyle = (color: Color) => ({ backgroundColor: _rgb(color), color: getTextColor(_rgb(color)), borderBottomColor: darken(_rgb(color), 0.2), + margin: '3px', }); -const useStyles = makeStyles((theme) => ({ - label: { - ...theme.typography.body1, - padding: '1px 6px 0.5px', - fontSize: '0.9em', - fontWeight: 500, - margin: '0.05em 1px calc(-1.5px + 0.05em)', - borderRadius: '3px', - display: 'inline-block', - borderBottom: 'solid 1.5px', - verticalAlign: 'bottom', - }, -})); - type Props = { label: LabelFragment }; function Label({ label }: Props) { - const classes = useStyles(); return ( - <span className={classes.label} style={createStyle(label.color)}> - {label.name} - </span> + <Chip + size={'small'} + label={label.name} + style={createStyle(label.color)} + ></Chip> ); } - export default Label; diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx index 25281f96..3cb48ecd 100644 --- a/webui/src/pages/bug/Bug.tsx +++ b/webui/src/pages/bug/Bug.tsx @@ -9,6 +9,7 @@ import Label from 'src/components/Label'; import { BugFragment } from './Bug.generated'; import CommentForm from './CommentForm'; import TimelineQuery from './TimelineQuery'; +import LabelMenu from './labels/LabelMenu'; /** * Css in JS Styles @@ -53,13 +54,13 @@ const useStyles = makeStyles((theme) => ({ listStyle: 'none', padding: 0, margin: 0, + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', }, label: { - marginTop: theme.spacing(1), - marginBottom: theme.spacing(1), - '& > *': { - display: 'block', - }, + marginTop: theme.spacing(0.1), + marginBottom: theme.spacing(0.1), }, noLabel: { ...theme.typography.body2, @@ -94,7 +95,9 @@ function Bug({ bug }: Props) { </IfLoggedIn> </div> <div className={classes.rightSidebar}> - <span className={classes.rightSidebarTitle}>Labels</span> + <span className={classes.rightSidebarTitle}> + <LabelMenu bug={bug} /> + </span> <ul className={classes.labelList}> {bug.labels.length === 0 && ( <span className={classes.noLabel}>None yet</span> diff --git a/webui/src/pages/bug/labels/LabelMenu.tsx b/webui/src/pages/bug/labels/LabelMenu.tsx new file mode 100644 index 00000000..8213d15b --- /dev/null +++ b/webui/src/pages/bug/labels/LabelMenu.tsx @@ -0,0 +1,356 @@ +import React, { useEffect, useRef, useState } from 'react'; + +import { IconButton } from '@material-ui/core'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import { SvgIconProps } from '@material-ui/core/SvgIcon'; +import TextField from '@material-ui/core/TextField'; +import { makeStyles, withStyles } from '@material-ui/core/styles'; +import { darken } from '@material-ui/core/styles/colorManipulator'; +import CheckIcon from '@material-ui/icons/Check'; +import SettingsIcon from '@material-ui/icons/Settings'; + +import { Color } from '../../../gqlTypes'; +import { + ListLabelsDocument, + useListLabelsQuery, +} from '../../list/ListLabels.generated'; +import { BugFragment } from '../Bug.generated'; +import { GetBugDocument } from '../BugQuery.generated'; + +import { useSetLabelMutation } from './SetLabel.generated'; + +type DropdownTuple = [string, string, Color]; + +type FilterDropdownProps = { + children: React.ReactNode; + dropdown: DropdownTuple[]; + icon?: React.ComponentType<SvgIconProps>; + hasFilter?: boolean; + itemActive: (key: string) => boolean; + onClose: () => void; + toggleLabel: (key: string, active: boolean) => void; + onNewItem: (name: string) => void; +} & React.ButtonHTMLAttributes<HTMLButtonElement>; + +const CustomTextField = withStyles((theme) => ({ + root: { + margin: '0 8px 12px 8px', + '& label.Mui-focused': { + margin: '0 2px', + color: theme.palette.text.secondary, + }, + '& .MuiInput-underline::before': { + borderBottomColor: theme.palette.divider, + }, + '& .MuiInput-underline::after': { + borderBottomColor: theme.palette.divider, + }, + }, +}))(TextField); + +const ITEM_HEIGHT = 48; + +const useStyles = makeStyles((theme) => ({ + element: { + ...theme.typography.body2, + color: theme.palette.text.secondary, + padding: theme.spacing(0, 1), + fontWeight: 400, + textDecoration: 'none', + display: 'flex', + background: 'none', + border: 'none', + }, + itemActive: { + fontWeight: 600, + color: theme.palette.text.primary, + }, + icon: { + paddingRight: theme.spacing(0.5), + }, + labelcolor: { + width: '15px', + height: '15px', + display: 'flex', + backgroundColor: 'blue', + borderRadius: '0.25rem', + marginRight: '5px', + marginLeft: '3px', + }, + labelsheader: { + display: 'flex', + flexDirection: 'row', + }, + menuRow: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + }, +})); + +const _rgb = (color: Color) => + 'rgb(' + color.R + ',' + color.G + ',' + color.B + ')'; + +// Create a style object from the label RGB colors +const createStyle = (color: Color) => ({ + backgroundColor: _rgb(color), + borderBottomColor: darken(_rgb(color), 0.2), +}); + +function FilterDropdown({ + children, + dropdown, + icon: Icon, + hasFilter, + itemActive, + onClose, + toggleLabel, + onNewItem, +}: FilterDropdownProps) { + const [open, setOpen] = useState(false); + const [filter, setFilter] = useState<string>(''); + const buttonRef = useRef<HTMLButtonElement>(null); + const searchRef = useRef<HTMLButtonElement>(null); + const classes = useStyles({ active: false }); + + useEffect(() => { + searchRef && searchRef.current && searchRef.current.focus(); + }, [filter]); + + return ( + <> + <div className={classes.labelsheader}> + Labels + <IconButton + ref={buttonRef} + onClick={() => setOpen(!open)} + className={classes.element} + > + <SettingsIcon fontSize={'small'} /> + </IconButton> + </div> + + <Menu + getContentAnchorEl={null} + ref={searchRef} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + open={open} + onClose={() => { + setOpen(false); + onClose(); + }} + onExited={() => setFilter('')} + anchorEl={buttonRef.current} + PaperProps={{ + style: { + maxHeight: ITEM_HEIGHT * 4.5, + width: '25ch', + }, + }} + > + {hasFilter && ( + <CustomTextField + onChange={(e) => { + const { value } = e.target; + setFilter(value); + }} + onKeyDown={(e) => e.stopPropagation()} + value={filter} + label={`Filter ${children}`} + /> + )} + {dropdown + .sort(function (x, y) { + // true values first + return itemActive(x[1]) === itemActive(y[1]) ? 0 : x ? -1 : 1; + }) + .filter((d) => d[1].toLowerCase().includes(filter.toLowerCase())) + .map(([key, value, color]) => ( + <MenuItem + style={{ whiteSpace: 'normal', wordBreak: 'break-all' }} + onClick={() => { + toggleLabel(key, itemActive(key)); + }} + key={key} + className={itemActive(key) ? classes.itemActive : undefined} + > + <div className={classes.menuRow}> + {itemActive(key) ? <CheckIcon fontSize={'small'} /> : null} + <div + className={classes.labelcolor} + style={createStyle(color)} + /> + {value} + </div> + </MenuItem> + ))} + {filter !== '' && + dropdown.filter((d) => d[1].toLowerCase() === filter.toLowerCase()) + .length <= 0 && ( + <MenuItem + style={{ whiteSpace: 'normal', wordBreak: 'break-all' }} + onClick={() => { + onNewItem(filter); + setFilter(''); + setOpen(false); + }} + > + Create new label '{filter}' + </MenuItem> + )} + </Menu> + </> + ); +} + +type Props = { + bug: BugFragment; +}; +function LabelMenu({ bug }: Props) { + const { data: labelsData } = useListLabelsQuery(); + const [bugLabelNames, setBugLabelNames] = useState( + bug.labels.map((l) => l.name) + ); + const [selectedLabels, setSelectedLabels] = useState( + bug.labels.map((l) => l.name) + ); + + const [setLabelMutation] = useSetLabelMutation(); + + useEffect(() => {}); + function toggleLabel(key: string, active: boolean) { + const labels: string[] = active + ? selectedLabels.filter((label) => label !== key) + : selectedLabels.concat([key]); + setSelectedLabels(labels); + console.log('toggle (selected)'); + console.log(labels); + } + + function diff(oldState: string[], newState: string[]) { + console.log('oldState / Buglabels'); + console.log(oldState); + console.log('newState / Selected'); + console.log(newState); + const added = newState.filter((x) => !oldState.includes(x)); + const removed = oldState.filter((x) => !newState.includes(x)); + return { + added: added, + removed: removed, + }; + } + + const changeBugLabels = ( + bugLabels = bug.labels.map((l) => l.name), + selectedLabel = selectedLabels + ) => { + const labels = diff(bugLabels, selectedLabel); + console.log('changeBugLabels'); + console.log(labels); + console.log('bugLabelNames'); + console.log(bugLabelNames); + if (labels.added.length > 0 || labels.removed.length > 0) { + setLabelMutation({ + variables: { + input: { + prefix: bug.id, + added: labels.added, + Removed: labels.removed, + }, + }, + refetchQueries: [ + // TODO: update the cache instead of refetching + { + query: GetBugDocument, + variables: { id: bug.id }, + }, + { + query: ListLabelsDocument, + }, + ], + awaitRefetchQueries: true, + }) + .then((res) => { + console.log(res); + setBugLabelNames(selectedLabels); + }) + .catch((e) => console.log(e)); + } + }; + + function isActive(key: string) { + return selectedLabels.includes(key); + } + + function createNewLabel(name: string) { + console.log('CREATE NEW LABEL'); + setLabelMutation({ + variables: { + input: { + prefix: bug.id, + added: [name], + }, + }, + refetchQueries: [ + // TODO: update the cache instead of refetching + { + query: GetBugDocument, + variables: { id: bug.id }, + }, + { + query: ListLabelsDocument, + }, + ], + awaitRefetchQueries: true, + }) + .then((res) => { + console.log(res); + + const tmp = selectedLabels.concat([name]); + console.log(tmp); + console.log('tmp'); + setSelectedLabels(tmp); + setBugLabelNames(bugLabelNames.concat([name])); + + changeBugLabels(bugLabelNames.concat([name]), tmp); + }) + .catch((e) => console.log('createnewLabelError' + e)); + } + + let labels: any = []; + if ( + labelsData?.repository && + labelsData.repository.validLabels && + labelsData.repository.validLabels.nodes + ) { + labels = labelsData.repository.validLabels.nodes.map((node) => [ + node.name, + node.name, + node.color, + ]); + } + + return ( + <FilterDropdown + onClose={changeBugLabels} + itemActive={isActive} + toggleLabel={toggleLabel} + dropdown={labels} + onNewItem={createNewLabel} + hasFilter + > + Labels + </FilterDropdown> + ); +} + +export default LabelMenu; diff --git a/webui/src/pages/bug/labels/SetLabel.graphql b/webui/src/pages/bug/labels/SetLabel.graphql new file mode 100644 index 00000000..44dfae11 --- /dev/null +++ b/webui/src/pages/bug/labels/SetLabel.graphql @@ -0,0 +1,13 @@ +mutation SetLabel($input: ChangeLabelInput) { + changeLabels(input: $input) { + results{ + status, + label{ + name, + color{R}, + color{G}, + color{B} + } + } + } +} diff --git a/webui/src/pages/list/BugRow.tsx b/webui/src/pages/list/BugRow.tsx index 1f5d22aa..190370b0 100644 --- a/webui/src/pages/list/BugRow.tsx +++ b/webui/src/pages/list/BugRow.tsx @@ -71,9 +71,6 @@ const useStyles = makeStyles((theme) => ({ }, labels: { paddingLeft: theme.spacing(1), - '& > *': { - display: 'inline-block', - }, }, commentCount: { fontSize: '1rem', diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx index 2e99eedf..119480e7 100644 --- a/webui/src/pages/list/Filter.tsx +++ b/webui/src/pages/list/Filter.tsx @@ -8,8 +8,11 @@ import MenuItem from '@material-ui/core/MenuItem'; import { SvgIconProps } from '@material-ui/core/SvgIcon'; import TextField from '@material-ui/core/TextField'; import { makeStyles, withStyles } from '@material-ui/core/styles'; +import { darken } from '@material-ui/core/styles/colorManipulator'; import ArrowDropDown from '@material-ui/icons/ArrowDropDown'; +import { Color } from '../../gqlTypes'; + const CustomTextField = withStyles((theme) => ({ root: { margin: '0 8px 12px 8px', @@ -99,9 +102,26 @@ const useStyles = makeStyles((theme) => ({ icon: { paddingRight: theme.spacing(0.5), }, + labelcolor: { + minWidth: '15px', + minHeight: '15px', + display: 'flex', + backgroundColor: 'blue', + borderRadius: '0.25rem', + marginRight: '5px', + marginLeft: '3px', + }, })); +const _rgb = (color: Color) => + 'rgb(' + color.R + ',' + color.G + ',' + color.B + ')'; + +// Create a style object from the label RGB colors +const createStyle = (color: Color) => ({ + backgroundColor: _rgb(color), + borderBottomColor: darken(_rgb(color), 0.2), +}); -type DropdownTuple = [string, string]; +type DropdownTuple = [string, string, Color?]; type FilterDropdownProps = { children: React.ReactNode; @@ -183,14 +203,21 @@ function FilterDropdown({ )} {dropdown .filter((d) => d[1].toLowerCase().includes(filter.toLowerCase())) - .map(([key, value]) => ( + .map(([key, value, color]) => ( <MenuItem + style={{ whiteSpace: 'normal', wordBreak: 'break-all' }} component={Link} to={to(key)} className={itemActive(key) ? classes.itemActive : undefined} onClick={() => setOpen(false)} key={key} > + {color && ( + <div + className={classes.labelcolor} + style={createStyle(color)} + /> + )} {value} </MenuItem> ))} diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index 979bf530..e109578d 100644 --- a/webui/src/pages/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -89,6 +89,7 @@ function FilterToolbar({ query, queryLocation }: Props) { labels = labelsData.repository.validLabels.nodes.map((node) => [ node.name, node.name, + node.color, ]); } diff --git a/webui/src/pages/list/ListLabels.graphql b/webui/src/pages/list/ListLabels.graphql index dcb44b67..8b2f561a 100644 --- a/webui/src/pages/list/ListLabels.graphql +++ b/webui/src/pages/list/ListLabels.graphql @@ -2,7 +2,8 @@ query ListLabels { repository { validLabels { nodes { - name + name, + color{R,G,B} } } } |