diff --git a/webapp/src/components/sidebar_right/sidebar_right.jsx b/webapp/src/components/sidebar_right/sidebar_right.jsx index 567527929..16e2c4900 100644 --- a/webapp/src/components/sidebar_right/sidebar_right.jsx +++ b/webapp/src/components/sidebar_right/sidebar_right.jsx @@ -5,9 +5,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import Scrollbars from 'react-custom-scrollbars-2'; +import {makeStyleFromTheme} from 'mattermost-redux/utils/theme_utils'; + import {RHSStates} from '../../constants'; import GithubItems from './github_items'; +import SortDropdown from './sort_dropdown'; export function renderView(props) { return ( @@ -81,6 +84,15 @@ export default class SidebarRight extends React.PureComponent { }).isRequired, }; + constructor(props) { + super(props); + this.state = {sortBy: 'created'}; + } + + handleSortChange = (selectedOption) => { + this.setState({sortBy: selectedOption ? selectedOption.value : 'created'}); + }; + componentDidMount() { if (this.props.yourPrs && this.props.rhsState === RHSStates.PRS) { this.props.actions.getYourPrsDetails(mapGithubItemListToPrList(this.props.yourPrs)); @@ -145,6 +157,25 @@ export default class SidebarRight extends React.PureComponent { break; } + // Sort items by selected criteria + // Notification threads (UNREADS) don't have created_at, so fall back + // to updated_at when created_at is undefined. + const {sortBy} = this.state; + const sortedItems = (githubItems || []).slice().sort((a, b) => { + const dateA = sortBy === 'updated' ? a.updated_at : (a.created_at || a.updated_at); + const dateB = sortBy === 'updated' ? b.updated_at : (b.created_at || b.updated_at); + return new Date(dateB) - new Date(dateA); + }); + + // Build sort options + const sortOptions = [ + {value: 'created', label: 'Recently Created'}, + {value: 'updated', label: 'Recently Updated'}, + ]; + const selectedSortOption = sortOptions.find((opt) => opt.value === sortBy) || sortOptions[0]; + + const style = getStyle(this.props.theme); + return ( {title} +
+ +
{ + return { + sectionHeader: { + padding: '15px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + flexWrap: 'wrap', + }, + sortDropdownContainer: { + marginLeft: '8px', + width: 'fit-content', + fontSize: '12px', + }, + }; +}); \ No newline at end of file diff --git a/webapp/src/components/sidebar_right/sort_dropdown.jsx b/webapp/src/components/sidebar_right/sort_dropdown.jsx new file mode 100644 index 000000000..041eb9111 --- /dev/null +++ b/webapp/src/components/sidebar_right/sort_dropdown.jsx @@ -0,0 +1,315 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import {Overlay} from 'react-bootstrap'; +import {ChevronDownIcon, ChevronUpIcon, CheckIcon} from '@primer/octicons-react'; + +import {makeStyleFromTheme, changeOpacity} from 'mattermost-redux/utils/theme_utils'; + +export default class SortDropdown extends React.PureComponent { + static propTypes = { + value: PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + }).isRequired, + onChange: PropTypes.func.isRequired, + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + })).isRequired, + theme: PropTypes.object.isRequired, + ariaLabel: PropTypes.string, + className: PropTypes.string, + }; + + static defaultProps = { + ariaLabel: 'Sort items', + className: '', + }; + + constructor(props) { + super(props); + this.state = {isOpen: false, hoveredOption: null, isTriggerFocused: false}; + this.toggleRef = React.createRef(); + } + + handleTriggerFocus = () => { + this.setState({isTriggerFocused: true}); + }; + + handleTriggerBlur = () => { + this.setState({isTriggerFocused: false}); + }; + + handleToggle = () => { + this.setState(({isOpen}) => ({isOpen: !isOpen})); + }; + + handleClose = () => { + this.setState({isOpen: false, hoveredOption: null}); + }; + + handleOptionClick = (option) => { + this.props.onChange(option); + this.handleClose(); + }; + + handleKeyDown = (event) => { + const {options, value} = this.props; + const currentIndex = options.findIndex((opt) => opt.value === value.value); + let newIndex = currentIndex; + + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + newIndex = (currentIndex + 1) % options.length; + break; + case 'ArrowUp': + event.preventDefault(); + newIndex = ((currentIndex - 1) + options.length) % options.length; + break; + case 'Enter': + case ' ': + event.preventDefault(); + this.handleToggle(); + return; + case 'Escape': + this.handleClose(); + return; + default: + return; + } + + if (newIndex !== currentIndex) { + this.props.onChange(options[newIndex]); + } + }; + + render() { + const {value, options, theme, ariaLabel, className} = this.props; + const {isOpen, hoveredOption, isTriggerFocused} = this.state; + const style = getStyle(theme); + + const trigger = ( + + ); + + const overlay = ( + +
+
    + {options.map((option) => { + const isSelected = option.value === value.value; + const isHovered = option.value === hoveredOption; + return ( +
  • this.handleOptionClick(option)} + onMouseEnter={() => this.setState({hoveredOption: option.value})} + onMouseLeave={() => this.setState({hoveredOption: null})} + onFocus={() => this.setState({hoveredOption: option.value})} + onBlur={() => this.setState({hoveredOption: null})} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.handleOptionClick(option); + } + }} + tabIndex={isSelected ? 0 : -1} + > + + +
  • + ); + })} +
+
+
+ ); + + return ( +
+ {trigger} + {overlay} +
+ ); + } +} + +const getStyle = makeStyleFromTheme((theme) => { + const textColor = theme.centerChannelColor; + const bgColor = theme.centerChannelBg; + + const text56 = changeOpacity(textColor, 0.56); + const text8 = changeOpacity(textColor, 0.08); + + return { + container: { + display: 'inline-flex', + alignItems: 'center', + }, + + // Select trigger — "Sort by: Recently Created" + // font: Open Sans 12px/16px semibold, color rgba(61,60,64,0.56) + trigger: { + display: 'inline-flex', + alignItems: 'center', + gap: '4px', + padding: 0, + border: 'none', + borderRadius: 0, + background: 'transparent', + color: text56, + fontFamily: '"Open Sans", sans-serif', + fontSize: '12px', + fontStyle: 'normal', + fontWeight: 600, + lineHeight: '16px', + textAlign: 'center', + cursor: 'pointer', + whiteSpace: 'nowrap', + }, + triggerFocused: { + outline: `2px solid ${textColor}`, + outlineOffset: '2px', + }, + triggerText: { + display: 'inline', + }, + chevronContainer: { + display: 'inline-flex', + alignItems: 'center', + flexShrink: 0, + }, + chevron: { + color: text56, + }, + + // Popover — display:inline-flex, padding:8px 0, flex-direction:column, align-items:flex-start + // border-radius:4px, border:1px solid rgba(61,60,64,0.08), background:#FFF + // box-shadow: Elevation 4 = 0 8px 24px 0 rgba(0,0,0,0.12) + popover: { + position: 'relative', + display: 'inline-flex', + padding: '8px 0', + flexDirection: 'column', + alignItems: 'flex-start', + background: bgColor, + border: `1px solid ${text8}`, + borderRadius: '4px', + boxShadow: '0 8px 24px 0 rgba(0, 0, 0, 0.12)', + minWidth: '200px', + zIndex: 9999, + marginTop: '4px', + }, + optionsList: { + listStyle: 'none', + margin: 0, + padding: 0, + maxHeight: '300px', + overflowY: 'auto', + }, + option: { + height: '32px', + display: 'flex', + alignItems: 'center', + padding: '0 20px', + cursor: 'pointer', + whiteSpace: 'nowrap', + transition: 'background 0.1s ease', + }, + optionHover: { + background: text8, + outline: 'none', + }, + optionSelected: { + background: textColor, + }, + optionLabel: { + display: 'inline-flex', + alignItems: 'center', + gap: '12px', + padding: '0 4px', + }, + optionIcon: { + color: 'transparent', + flexShrink: 0, + }, + optionIconSelected: { + color: bgColor, + flexShrink: 0, + }, + optionText: { + fontSize: '12px', + fontWeight: 400, + color: textColor, + }, + optionTextSelected: { + fontSize: '12px', + fontWeight: 400, + color: bgColor, + }, + }; +});