feat: Version 2

Closes #627, closes #1047
This commit is contained in:
Maksim Eltyshev
2025-05-10 02:09:06 +02:00
parent ad7fb51cfa
commit 2ee1166747
1557 changed files with 76832 additions and 47042 deletions

View File

@@ -0,0 +1,69 @@
/*!
* 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 { useTranslation } from 'react-i18next';
import { Tab } from 'semantic-ui-react';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { useClosableModal } from '../../../hooks';
import GeneralPane from './GeneralPane';
import PreferencesPane from './PreferencesPane';
import NotificationsPane from './NotificationsPane';
const BoardSettingsModal = React.memo(() => {
const openPreferences = useSelector(
(state) => selectors.selectCurrentModal(state).params.openPreferences,
);
const dispatch = useDispatch();
const [t] = useTranslation();
const handleClose = useCallback(() => {
dispatch(entryActions.closeModal());
}, [dispatch]);
const [ClosableModal] = useClosableModal();
const panes = [
{
menuItem: t('common.general', {
context: 'title',
}),
render: () => <GeneralPane />,
},
{
menuItem: t('common.preferences', {
context: 'title',
}),
render: () => <PreferencesPane />,
},
{
menuItem: t('common.notifications', {
context: 'title',
}),
render: () => <NotificationsPane />,
},
];
return (
<ClosableModal closeIcon size="small" centered={false} onClose={handleClose}>
<ClosableModal.Content>
<Tab
menu={{
secondary: true,
pointing: true,
}}
panes={panes}
defaultActiveIndex={openPreferences ? 1 : undefined}
/>
</ClosableModal.Content>
</ClosableModal>
);
});
export default BoardSettingsModal;

View File

@@ -0,0 +1,75 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { dequal } from 'dequal';
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Form, Input } from 'semantic-ui-react';
import selectors from '../../../../selectors';
import entryActions from '../../../../entry-actions';
import { useForm, useNestedRef } from '../../../../hooks';
import styles from './EditInformation.module.scss';
const EditInformation = React.memo(() => {
const selectBoardById = useMemo(() => selectors.makeSelectBoardById(), []);
const boardId = useSelector((state) => selectors.selectCurrentModal(state).params.id);
const board = useSelector((state) => selectBoardById(state, boardId));
const dispatch = useDispatch();
const [t] = useTranslation();
const defaultData = useMemo(
() => ({
name: board.name,
}),
[board.name],
);
const [data, handleFieldChange] = useForm(() => ({
name: '',
...defaultData,
}));
const cleanData = useMemo(
() => ({
...data,
name: data.name.trim(),
}),
[data],
);
const [nameFieldRef, handleNameFieldRef] = useNestedRef('inputRef');
const handleSubmit = useCallback(() => {
if (!cleanData.name) {
nameFieldRef.current.select();
return;
}
dispatch(entryActions.updateBoard(boardId, cleanData));
}, [boardId, dispatch, cleanData, nameFieldRef]);
return (
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.title')}</div>
<Input
fluid
ref={handleNameFieldRef}
name="name"
value={data.name}
maxLength={128}
className={styles.field}
onChange={handleFieldChange}
/>
<Button positive disabled={dequal(cleanData, defaultData)} content={t('action.save')} />
</Form>
);
});
export default EditInformation;

View File

@@ -0,0 +1,17 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}
}

View File

@@ -0,0 +1,64 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Divider, Header, Tab } from 'semantic-ui-react';
import selectors from '../../../../selectors';
import entryActions from '../../../../entry-actions';
import { usePopupInClosableContext } from '../../../../hooks';
import EditInformation from './EditInformation';
import ConfirmationStep from '../../../common/ConfirmationStep';
import styles from './GeneralPane.module.scss';
const GeneralPane = React.memo(() => {
const selectBoardById = useMemo(() => selectors.makeSelectBoardById(), []);
const boardId = useSelector((state) => selectors.selectCurrentModal(state).params.id);
const board = useSelector((state) => selectBoardById(state, boardId));
const dispatch = useDispatch();
const [t] = useTranslation();
const handleDeleteConfirm = useCallback(() => {
dispatch(entryActions.deleteBoard(boardId));
}, [boardId, dispatch]);
const ConfirmationPopup = usePopupInClosableContext(ConfirmationStep);
return (
<Tab.Pane attached={false} className={styles.wrapper}>
<EditInformation />
<Divider horizontal section>
<Header as="h4">
{t('common.dangerZone', {
context: 'title',
})}
</Header>
</Divider>
<div className={styles.action}>
<ConfirmationPopup
title="common.deleteBoard"
content="common.areYouSureYouWantToDeleteThisBoard"
buttonContent="action.deleteBoard"
typeValue={board.name}
typeContent="common.typeTitleToConfirm"
onConfirm={handleDeleteConfirm}
>
<Button className={styles.actionButton}>
{t(`action.deleteBoard`, {
context: 'title',
})}
</Button>
</ConfirmationPopup>
</div>
</Tab.Pane>
);
});
export default GeneralPane;

View File

@@ -0,0 +1,38 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.action {
border: none;
border-radius: 0.28571429rem;
display: inline-block;
height: 36px;
overflow: hidden;
position: relative;
transition: background 0.3s ease;
width: 100%;
&:hover {
background: #e9e9e9;
}
}
.actionButton {
background: transparent;
color: #6b808c;
font-weight: normal;
height: 36px;
line-height: 24px;
padding: 6px 12px;
text-align: left;
text-decoration: underline;
width: 100%;
}
.wrapper {
border: none;
box-shadow: none;
}
}

View 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 GeneralPane from './GeneralPane';
export default GeneralPane;

View File

@@ -0,0 +1,44 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Tab } from 'semantic-ui-react';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import NotificationServices from '../../notification-services/NotificationServices';
import styles from './NotificationsPane.module.scss';
const NotificationsPane = React.memo(() => {
const selectNotificationServiceIdsByBoardId = useMemo(
() => selectors.makeSelectNotificationServiceIdsByBoardId(),
[],
);
const boardId = useSelector((state) => selectors.selectCurrentModal(state).params.id);
const notificationServiceIds = useSelector((state) =>
selectNotificationServiceIdsByBoardId(state, boardId),
);
const dispatch = useDispatch();
const handleCreate = useCallback(
(data) => {
dispatch(entryActions.createNotificationServiceInBoard(boardId, data));
},
[boardId, dispatch],
);
return (
<Tab.Pane attached={false} className={styles.wrapper}>
<NotificationServices ids={notificationServiceIds} onCreate={handleCreate} />
</Tab.Pane>
);
});
export default NotificationsPane;

View File

@@ -0,0 +1,11 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.wrapper {
border: none;
box-shadow: none;
}
}

View File

@@ -0,0 +1,65 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Radio, Segment } from 'semantic-ui-react';
import selectors from '../../../../selectors';
import entryActions from '../../../../entry-actions';
import SelectCardType from '../../../cards/SelectCardType';
import styles from './DefaultCardType.module.scss';
const DefaultCardType = React.memo(() => {
const selectBoardById = useMemo(() => selectors.makeSelectBoardById(), []);
const boardId = useSelector((state) => selectors.selectCurrentModal(state).params.id);
const board = useSelector((state) => selectBoardById(state, boardId));
const dispatch = useDispatch();
const [t] = useTranslation();
const handleSelect = useCallback(
(defaultCardType) => {
dispatch(
entryActions.updateBoard(boardId, {
defaultCardType,
}),
);
},
[boardId, dispatch],
);
const handleToggleChange = useCallback(
(_, { name: fieldName, checked }) => {
dispatch(
entryActions.updateBoard(boardId, {
[fieldName]: checked,
}),
);
},
[boardId, dispatch],
);
return (
<>
<SelectCardType value={board.defaultCardType} onSelect={handleSelect} />
<Segment basic className={styles.settings}>
<Radio
toggle
name="limitCardTypesToDefaultOne"
checked={board.limitCardTypesToDefaultOne}
label={t('common.limitCardTypesToDefaultOne')}
className={styles.radio}
onChange={handleToggleChange}
/>
</Segment>
</>
);
});
export default DefaultCardType;

View File

@@ -0,0 +1,19 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.radio {
margin-bottom: 16px;
width: 100%;
&:last-child {
margin-bottom: 0;
}
}
.settings {
margin: 0 0 8px;
}
}

View File

@@ -0,0 +1,63 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Icon, Menu } from 'semantic-ui-react';
import selectors from '../../../../selectors';
import entryActions from '../../../../entry-actions';
import { BoardViews } from '../../../../constants/Enums';
import { BoardViewIcons } from '../../../../constants/Icons';
import styles from './DefaultView.module.scss';
const DESCRIPTION_BY_VIEW = {
[BoardViews.KANBAN]: 'common.visualTaskManagementWithLists',
[BoardViews.GRID]: 'common.dynamicAndUnevenlySpacedLayout',
[BoardViews.LIST]: 'common.sequentialDisplayOfCards',
};
const DefaultView = React.memo(() => {
const selectBoardById = useMemo(() => selectors.makeSelectBoardById(), []);
const boardId = useSelector((state) => selectors.selectCurrentModal(state).params.id);
const board = useSelector((state) => selectBoardById(state, boardId));
const dispatch = useDispatch();
const [t] = useTranslation();
const handleSelectClick = useCallback(
(_, { value: defaultView }) => {
dispatch(
entryActions.updateBoard(boardId, {
defaultView,
}),
);
},
[boardId, dispatch],
);
return (
<Menu secondary vertical className={styles.menu}>
{[BoardViews.KANBAN, BoardViews.GRID, BoardViews.LIST].map((view) => (
<Menu.Item
key={view}
value={view}
active={view === board.defaultView}
className={styles.menuItem}
onClick={handleSelectClick}
>
<Icon name={BoardViewIcons[view]} className={styles.menuItemIcon} />
<div className={styles.menuItemTitle}>{t(`common.${view}`)}</div>
<p className={styles.menuItemDescription}>{t(DESCRIPTION_BY_VIEW[view])}</p>
</Menu.Item>
))}
</Menu>
);
});
export default DefaultView;

View File

@@ -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) {
.menu {
margin: 0 auto 8px;
width: 100%;
}
.menuItem:last-child {
margin-bottom: 0;
}
.menuItemDescription {
opacity: 0.5;
}
.menuItemIcon {
float: left;
margin: 0 0.35714286em 0 0;
}
.menuItemTitle {
margin-bottom: 8px;
}
}

View File

@@ -0,0 +1,50 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Radio, Segment } from 'semantic-ui-react';
import selectors from '../../../../selectors';
import entryActions from '../../../../entry-actions';
import styles from './Others.module.scss';
const Others = React.memo(() => {
const selectBoardById = useMemo(() => selectors.makeSelectBoardById(), []);
const boardId = useSelector((state) => selectors.selectCurrentModal(state).params.id);
const board = useSelector((state) => selectBoardById(state, boardId));
const dispatch = useDispatch();
const [t] = useTranslation();
const handleChange = useCallback(
(_, { name: fieldName, checked }) => {
dispatch(
entryActions.updateBoard(boardId, {
[fieldName]: checked,
}),
);
},
[boardId, dispatch],
);
return (
<Segment basic>
<Radio
toggle
name="alwaysDisplayCardCreator"
checked={board.alwaysDisplayCardCreator}
label={t('common.alwaysDisplayCardCreator')}
className={styles.radio}
onChange={handleChange}
/>
</Segment>
);
});
export default Others;

View File

@@ -0,0 +1,15 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.radio {
margin-bottom: 16px;
width: 100%;
&:last-child {
margin-bottom: 0;
}
}
}

View File

@@ -0,0 +1,49 @@
/*!
* 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 { useTranslation } from 'react-i18next';
import { Divider, Header, Tab } from 'semantic-ui-react';
import DefaultView from './DefaultView';
import DefaultCardType from './DefaultCardType';
import Others from './Others';
import styles from './PreferencesPane.module.scss';
const PreferencesPane = React.memo(() => {
const [t] = useTranslation();
return (
<Tab.Pane attached={false} className={styles.wrapper}>
<Divider horizontal className={styles.firstDivider}>
<Header as="h4">
{t('common.defaultView', {
context: 'title',
})}
</Header>
</Divider>
<DefaultView />
<Divider horizontal>
<Header as="h4">
{t('common.defaultCardType', {
context: 'title',
})}
</Header>
</Divider>
<DefaultCardType />
<Divider horizontal>
<Header as="h4">
{t('common.others', {
context: 'title',
})}
</Header>
</Divider>
<Others />
</Tab.Pane>
);
});
export default PreferencesPane;

View File

@@ -0,0 +1,15 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.firstDivider {
margin-top: 0;
}
.wrapper {
border: none;
box-shadow: none;
}
}

View 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 PreferencesPane from './PreferencesPane';
export default PreferencesPane;

View 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 BoardSettingsModal from './BoardSettingsModal';
export default BoardSettingsModal;