mirror of
https://github.com/plankanban/planka.git
synced 2025-12-27 01:11:50 +03:00
47
client/src/components/boards/BoardActions/BoardActions.jsx
Normal file
47
client/src/components/boards/BoardActions/BoardActions.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import Filters from './Filters';
|
||||
import RightSide from './RightSide';
|
||||
import BoardMemberships from '../../board-memberships/BoardMemberships';
|
||||
|
||||
import styles from './BoardActions.module.scss';
|
||||
|
||||
const BoardActions = React.memo(() => {
|
||||
const withMemberships = useSelector((state) => {
|
||||
const boardMemberships = selectors.selectMembershipsForCurrentBoard(state);
|
||||
|
||||
if (boardMemberships.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return selectors.selectIsCurrentUserManagerForCurrentProject(state);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.actions}>
|
||||
{withMemberships && (
|
||||
<div className={styles.action}>
|
||||
<BoardMemberships />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.action}>
|
||||
<Filters />
|
||||
</div>
|
||||
<div className={classNames(styles.action, styles.actionRightSide)}>
|
||||
<RightSide />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default BoardActions;
|
||||
@@ -0,0 +1,45 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.action {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.actionRightSide {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-ms-overflow-style: none;
|
||||
padding: 15px 0;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
219
client/src/components/boards/BoardActions/Filters.jsx
Normal file
219
client/src/components/boards/BoardActions/Filters.jsx
Normal file
@@ -0,0 +1,219 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from 'semantic-ui-react';
|
||||
import { useDidUpdate } from '../../../lib/hooks';
|
||||
import { usePopup } from '../../../lib/popup';
|
||||
import { Input } from '../../../lib/custom-ui';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { useNestedRef } from '../../../hooks';
|
||||
import UserAvatar from '../../users/UserAvatar';
|
||||
import BoardMembershipsStep from '../../board-memberships/BoardMembershipsStep';
|
||||
import LabelChip from '../../labels/LabelChip';
|
||||
import LabelsStep from '../../labels/LabelsStep';
|
||||
|
||||
import styles from './Filters.module.scss';
|
||||
|
||||
const Filters = React.memo(() => {
|
||||
const board = useSelector(selectors.selectCurrentBoard);
|
||||
const userIds = useSelector(selectors.selectFilterUserIdsForCurrentBoard);
|
||||
const labelIds = useSelector(selectors.selectFilterLabelIdsForCurrentBoard);
|
||||
const currentUserId = useSelector(selectors.selectCurrentUserId);
|
||||
|
||||
const withCurrentUserSelector = useSelector(
|
||||
(state) => !!selectors.selectCurrentUserMembershipForCurrentBoard(state),
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const [search, setSearch] = useState(board.search);
|
||||
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
||||
|
||||
const debouncedSearch = useMemo(
|
||||
() =>
|
||||
debounce((nextSearch) => {
|
||||
dispatch(entryActions.searchInCurrentBoard(nextSearch));
|
||||
}, 400),
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const [searchFieldRef, handleSearchFieldRef] = useNestedRef('inputRef');
|
||||
|
||||
const cancelSearch = useCallback(() => {
|
||||
debouncedSearch.cancel();
|
||||
setSearch('');
|
||||
dispatch(entryActions.searchInCurrentBoard(''));
|
||||
searchFieldRef.current.blur();
|
||||
}, [dispatch, debouncedSearch, searchFieldRef]);
|
||||
|
||||
const handleUserSelect = useCallback(
|
||||
(userId) => {
|
||||
dispatch(entryActions.addUserToFilterInCurrentBoard(userId));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleCurrentUserSelect = useCallback(() => {
|
||||
dispatch(entryActions.addUserToFilterInCurrentBoard(currentUserId));
|
||||
}, [currentUserId, dispatch]);
|
||||
|
||||
const handleUserDeselect = useCallback(
|
||||
(userId) => {
|
||||
dispatch(entryActions.removeUserFromFilterInCurrentBoard(userId));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleUserClick = useCallback(
|
||||
({
|
||||
currentTarget: {
|
||||
dataset: { id: userId },
|
||||
},
|
||||
}) => {
|
||||
dispatch(entryActions.removeUserFromFilterInCurrentBoard(userId));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleLabelSelect = useCallback(
|
||||
(labelId) => {
|
||||
dispatch(entryActions.addLabelToFilterInCurrentBoard(labelId));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleLabelDeselect = useCallback(
|
||||
(labelId) => {
|
||||
dispatch(entryActions.removeLabelFromFilterInCurrentBoard(labelId));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleLabelClick = useCallback(
|
||||
({
|
||||
currentTarget: {
|
||||
dataset: { id: labelId },
|
||||
},
|
||||
}) => {
|
||||
dispatch(entryActions.removeLabelFromFilterInCurrentBoard(labelId));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
(_, { value }) => {
|
||||
setSearch(value);
|
||||
debouncedSearch(value);
|
||||
},
|
||||
[debouncedSearch],
|
||||
);
|
||||
|
||||
const handleSearchFocus = useCallback(() => {
|
||||
setIsSearchFocused(true);
|
||||
}, []);
|
||||
|
||||
const handleSearchKeyDown = useCallback(
|
||||
(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
cancelSearch();
|
||||
}
|
||||
},
|
||||
[cancelSearch],
|
||||
);
|
||||
|
||||
const handleSearchBlur = useCallback(() => {
|
||||
setIsSearchFocused(false);
|
||||
}, []);
|
||||
|
||||
const handleCancelSearchClick = useCallback(() => {
|
||||
cancelSearch();
|
||||
}, [cancelSearch]);
|
||||
|
||||
useDidUpdate(() => {
|
||||
setSearch(board.search);
|
||||
}, [board.search]);
|
||||
|
||||
const BoardMembershipsPopup = usePopup(BoardMembershipsStep);
|
||||
const LabelsPopup = usePopup(LabelsStep);
|
||||
|
||||
const isSearchActive = search || isSearchFocused;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={styles.filter}>
|
||||
<BoardMembershipsPopup
|
||||
currentUserIds={userIds}
|
||||
title="common.filterByMembers"
|
||||
onUserSelect={handleUserSelect}
|
||||
onUserDeselect={handleUserDeselect}
|
||||
>
|
||||
<button type="button" className={styles.filterButton}>
|
||||
<span className={styles.filterTitle}>{`${t('common.members')}:`}</span>
|
||||
{userIds.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>}
|
||||
</button>
|
||||
</BoardMembershipsPopup>
|
||||
{userIds.length === 0 && withCurrentUserSelector && (
|
||||
<button type="button" className={styles.filterButton} onClick={handleCurrentUserSelect}>
|
||||
<span className={styles.filterLabel}>
|
||||
<Icon fitted name="target" className={styles.filterLabelIcon} />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{userIds.map((userId) => (
|
||||
<span key={userId} className={styles.filterItem}>
|
||||
<UserAvatar id={userId} size="tiny" onClick={handleUserClick} />
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<span className={styles.filter}>
|
||||
<LabelsPopup
|
||||
currentIds={labelIds}
|
||||
title="common.filterByLabels"
|
||||
onSelect={handleLabelSelect}
|
||||
onDeselect={handleLabelDeselect}
|
||||
>
|
||||
<button type="button" className={styles.filterButton}>
|
||||
<span className={styles.filterTitle}>{`${t('common.labels')}:`}</span>
|
||||
{labelIds.length === 0 && <span className={styles.filterLabel}>{t('common.all')}</span>}
|
||||
</button>
|
||||
</LabelsPopup>
|
||||
{labelIds.map((labelId) => (
|
||||
<span key={labelId} className={styles.filterItem}>
|
||||
<LabelChip id={labelId} size="small" onClick={handleLabelClick} />
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<span className={styles.filter}>
|
||||
<Input
|
||||
ref={handleSearchFieldRef}
|
||||
value={search}
|
||||
placeholder={t('common.searchCards')}
|
||||
maxLength={128}
|
||||
icon={
|
||||
isSearchActive ? (
|
||||
<Icon link name="cancel" onClick={handleCancelSearchClick} />
|
||||
) : (
|
||||
'search'
|
||||
)
|
||||
}
|
||||
className={classNames(styles.search, !isSearchActive && styles.searchInactive)}
|
||||
onFocus={handleSearchFocus}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
onChange={handleSearchChange}
|
||||
onBlur={handleSearchBlur}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Filters;
|
||||
@@ -0,0 +1,91 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.filter {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.filterButton {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterItem {
|
||||
display: inline-block;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
margin-right: 4px;
|
||||
max-width: 190px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.filterLabel {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 2px 8px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
}
|
||||
|
||||
.filterLabelIcon {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.filterTitle {
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 2px 12px;
|
||||
}
|
||||
|
||||
.search {
|
||||
height: 30px;
|
||||
margin: 0 12px;
|
||||
transition: width 0.2s ease;
|
||||
width: 280px;
|
||||
|
||||
@media only screen and (width < 768px) {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.searchInactive {
|
||||
color: #fff;
|
||||
height: 24px;
|
||||
width: 220px;
|
||||
|
||||
input {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
border: none;
|
||||
color: #fff !important;
|
||||
font-size: 12px;
|
||||
|
||||
&::placeholder {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
client/src/components/boards/BoardActions/RightSide/ActionsStep.jsx
Executable file
167
client/src/components/boards/BoardActions/RightSide/ActionsStep.jsx
Executable file
@@ -0,0 +1,167 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon, Menu } from 'semantic-ui-react';
|
||||
import { Popup } from '../../../../lib/custom-ui';
|
||||
|
||||
import selectors from '../../../../selectors';
|
||||
import entryActions from '../../../../entry-actions';
|
||||
import { useSteps } from '../../../../hooks';
|
||||
import { BoardContexts, BoardMembershipRoles } from '../../../../constants/Enums';
|
||||
import { BoardContextIcons } from '../../../../constants/Icons';
|
||||
import ConfirmationStep from '../../../common/ConfirmationStep';
|
||||
import CustomFieldGroupsStep from '../../../custom-field-groups/CustomFieldGroupsStep';
|
||||
|
||||
import styles from './ActionsStep.module.scss';
|
||||
|
||||
const StepTypes = {
|
||||
EMPTY_TRASH: 'EMPTY_TRASH',
|
||||
CUSTOM_FIELD_GROUPS: 'CUSTOM_FIELD_GROUPS',
|
||||
};
|
||||
|
||||
const ActionsStep = React.memo(({ onClose }) => {
|
||||
const board = useSelector(selectors.selectCurrentBoard);
|
||||
|
||||
const { withSubscribe, withTrashEmptier, withCustomFieldGroups } = useSelector((state) => {
|
||||
const isManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
|
||||
const boardMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
|
||||
let isMember = false;
|
||||
let isEditor = false;
|
||||
|
||||
if (boardMembership) {
|
||||
isMember = true;
|
||||
isEditor = boardMembership.role === BoardMembershipRoles.EDITOR;
|
||||
}
|
||||
|
||||
return {
|
||||
withSubscribe: isMember, // TODO: rename?
|
||||
withTrashEmptier: board.context === BoardContexts.TRASH && (isManager || isEditor),
|
||||
withCustomFieldGroups: isEditor,
|
||||
};
|
||||
}, shallowEqual);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
|
||||
const handleToggleSubscriptionClick = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentBoard({
|
||||
isSubscribed: !board.isSubscribed,
|
||||
}),
|
||||
);
|
||||
|
||||
onClose();
|
||||
}, [onClose, board.isSubscribed, dispatch]);
|
||||
|
||||
const handleSelectContextClick = useCallback(
|
||||
(_, { value: context }) => {
|
||||
dispatch(entryActions.updateContextInCurrentBoard(context));
|
||||
onClose();
|
||||
},
|
||||
[onClose, dispatch],
|
||||
);
|
||||
|
||||
const handleEmptyTrashConfirm = useCallback(() => {
|
||||
dispatch(entryActions.clearTrashListInCurrentBoard());
|
||||
onClose();
|
||||
}, [onClose, dispatch]);
|
||||
|
||||
const handleEmptyTrashClick = useCallback(() => {
|
||||
openStep(StepTypes.EMPTY_TRASH);
|
||||
}, [openStep]);
|
||||
|
||||
const handleCustomFieldsClick = useCallback(() => {
|
||||
openStep(StepTypes.CUSTOM_FIELD_GROUPS);
|
||||
}, [openStep]);
|
||||
|
||||
if (step) {
|
||||
switch (step.type) {
|
||||
case StepTypes.EMPTY_TRASH:
|
||||
return (
|
||||
<ConfirmationStep
|
||||
title="common.emptyTrash"
|
||||
content="common.areYouSureYouWantToEmptyTrash"
|
||||
buttonContent="action.emptyTrash"
|
||||
onConfirm={handleEmptyTrashConfirm}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
case StepTypes.CUSTOM_FIELD_GROUPS:
|
||||
return <CustomFieldGroupsStep onBack={handleBack} onClose={onClose} />;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.boardActions', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
{withSubscribe && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleToggleSubscriptionClick}>
|
||||
<Icon
|
||||
name={board.isSubscribed ? 'bell slash outline' : 'bell outline'}
|
||||
className={styles.menuItemIcon}
|
||||
/>
|
||||
{t(board.isSubscribed ? 'action.unsubscribe' : 'action.subscribe', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{withCustomFieldGroups && (
|
||||
<Menu.Item className={styles.menuItem} onClick={handleCustomFieldsClick}>
|
||||
<Icon name="sticky note outline" className={styles.menuItemIcon} />
|
||||
{t('common.customFields', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
)}
|
||||
{withTrashEmptier && (
|
||||
<>
|
||||
{(withSubscribe || withCustomFieldGroups) && <hr className={styles.divider} />}
|
||||
<Menu.Item className={styles.menuItem} onClick={handleEmptyTrashClick}>
|
||||
<Icon name="trash alternate outline" className={styles.menuItemIcon} />
|
||||
{t('action.emptyTrash', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
{(withSubscribe || withTrashEmptier) && <hr className={styles.divider} />}
|
||||
{[BoardContexts.BOARD, BoardContexts.ARCHIVE, BoardContexts.TRASH].map((context) => (
|
||||
<Menu.Item
|
||||
key={context}
|
||||
value={context}
|
||||
active={context === board.context}
|
||||
className={styles.menuItem}
|
||||
onClick={handleSelectContextClick}
|
||||
>
|
||||
<Icon name={BoardContextIcons[context]} className={styles.menuItemIcon} />
|
||||
{t(`common.${context}`)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</>
|
||||
</Menu>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ActionsStep.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ActionsStep;
|
||||
@@ -0,0 +1,28 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.divider {
|
||||
background: #eee;
|
||||
border: 0;
|
||||
height: 1px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
margin: -7px -12px -5px;
|
||||
width: calc(100% + 24px);
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
margin: 0;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.menuItemIcon {
|
||||
float: left;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Icon } from 'semantic-ui-react';
|
||||
import { usePopup } from '../../../../lib/popup';
|
||||
|
||||
import selectors from '../../../../selectors';
|
||||
import entryActions from '../../../../entry-actions';
|
||||
import { BoardContexts, BoardViews } from '../../../../constants/Enums';
|
||||
import { BoardContextIcons, BoardViewIcons } from '../../../../constants/Icons';
|
||||
import ActionsStep from './ActionsStep';
|
||||
|
||||
import styles from './RightSide.module.scss';
|
||||
|
||||
const RightSide = React.memo(() => {
|
||||
const board = useSelector(selectors.selectCurrentBoard);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleSelectViewClick = useCallback(
|
||||
({ currentTarget: { value: view } }) => {
|
||||
dispatch(entryActions.updateViewInCurrentBoard(view));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const ActionsPopup = usePopup(ActionsStep);
|
||||
|
||||
const views = [BoardViews.GRID, BoardViews.LIST];
|
||||
if (board.context === BoardContexts.BOARD) {
|
||||
views.unshift(BoardViews.KANBAN);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.action}>
|
||||
<div className={styles.buttonGroup}>
|
||||
{views.map((view) => (
|
||||
<button
|
||||
key={view}
|
||||
type="button"
|
||||
value={view}
|
||||
disabled={view === board.view}
|
||||
className={styles.button}
|
||||
onClick={handleSelectViewClick}
|
||||
>
|
||||
<Icon fitted name={BoardViewIcons[view]} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.action}>
|
||||
<ActionsPopup>
|
||||
<button type="button" className={styles.button}>
|
||||
<Icon fitted name={BoardContextIcons[board.context]} />
|
||||
</button>
|
||||
</ActionsPopup>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default RightSide;
|
||||
@@ -0,0 +1,53 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.action:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: rgba(0, 0, 0, 0.24);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
outline: none;
|
||||
padding: 5px 12px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
.button {
|
||||
border-radius: 0;
|
||||
|
||||
&:enabled {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import RightSide from './RightSide';
|
||||
|
||||
export default RightSide;
|
||||
8
client/src/components/boards/BoardActions/index.js
Normal file
8
client/src/components/boards/BoardActions/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import BoardActions from './BoardActions';
|
||||
|
||||
export default BoardActions;
|
||||
Reference in New Issue
Block a user