1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-08-05 13:35:27 +02:00

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,191 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useCallback, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { Button } from 'semantic-ui-react';
import { Input, Popup } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { useField, useNestedRef, useSteps } from '../../../hooks';
import DroppableTypes from '../../../constants/DroppableTypes';
import Item from './Item';
import CustomFieldGroupStep from '../CustomFieldGroupStep';
import AddCustomFieldGroupStep from '../AddCustomFieldGroupStep';
import styles from './CustomFieldGroupsStep.module.scss';
const StepTypes = {
ADD: 'ADD',
EDIT: 'EDIT',
};
const CustomFieldGroupsStep = React.memo(({ onBack }) => {
const customFieldGroups = useSelector(selectors.selectCustomFieldGroupsForCurrentBoard);
const dispatch = useDispatch();
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const [search, handleSearchChange] = useField('');
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
const filteredCustomFieldGroups = useMemo(
() =>
customFieldGroups.filter((customFieldGroup) =>
customFieldGroup.name.toLowerCase().includes(cleanSearch),
),
[customFieldGroups, cleanSearch],
);
const [searchFieldRef, handleSearchFieldRef] = useNestedRef('inputRef');
const handleCreate = useCallback(
(data) => {
dispatch(entryActions.createCustomFieldGroupInCurrentBoard(data));
},
[dispatch],
);
const handleDragEnd = useCallback(
({ draggableId, source, destination }) => {
if (!destination || source.index === destination.index) {
return;
}
dispatch(entryActions.moveCustomFieldGroup(draggableId, destination.index));
},
[dispatch],
);
const handleAddClick = useCallback(() => {
openStep(StepTypes.ADD);
}, [openStep]);
const handleEdit = useCallback(
(id) => {
openStep(StepTypes.EDIT, {
id,
});
},
[openStep],
);
useEffect(() => {
searchFieldRef.current.focus({
preventScroll: true,
});
}, [searchFieldRef]);
if (step) {
switch (step.type) {
case StepTypes.ADD:
return (
<AddCustomFieldGroupStep
onCreate={handleCreate}
onBack={handleBack}
onClose={handleBack}
/>
);
case StepTypes.EDIT: {
const currentCustomFieldGroup = customFieldGroups.find(
(customFieldGroup) => customFieldGroup.id === step.params.id,
);
if (currentCustomFieldGroup) {
return (
<CustomFieldGroupStep
id={currentCustomFieldGroup.id}
onBack={handleBack}
onClose={handleBack}
/>
);
}
openStep(null);
break;
}
default:
}
}
return (
<>
<Popup.Header onBack={onBack}>
{t('common.customFieldGroups', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Input
fluid
ref={handleSearchFieldRef}
value={search}
placeholder={t('common.searchCustomFieldGroups')}
maxLength={128}
icon="search"
onChange={handleSearchChange}
/>
{filteredCustomFieldGroups.length > 0 && (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="customFieldGroups" type={DroppableTypes.CUSTOM_FIELD_GROUP}>
{({ innerRef, droppableProps, placeholder }) => (
<div
{...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
ref={innerRef}
className={styles.items}
>
{filteredCustomFieldGroups.map((customFieldGroup, index) => (
<Item
key={customFieldGroup.id}
id={customFieldGroup.id}
index={index}
onEdit={handleEdit}
/>
))}
{placeholder}
</div>
)}
</Droppable>
<Droppable
droppableId="customFieldGroups:hack"
type={DroppableTypes.CUSTOM_FIELD_GROUP}
>
{({ innerRef, droppableProps, placeholder }) => (
<div
{...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
ref={innerRef}
className={styles.droppableHack}
>
{placeholder}
</div>
)}
</Droppable>
</DragDropContext>
)}
<Button
fluid
content={t('action.addCustomFieldGroup')}
className={styles.actionButton}
onClick={handleAddClick}
/>
</Popup.Content>
</>
);
});
CustomFieldGroupsStep.propTypes = {
onBack: PropTypes.func,
};
CustomFieldGroupsStep.defaultProps = {
onBack: undefined,
};
export default CustomFieldGroupsStep;

View file

@ -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) {
.actionButton {
background: transparent;
box-shadow: none;
color: #6b808c;
font-weight: normal;
margin-top: 8px;
padding: 6px 11px;
text-align: left;
text-decoration: underline;
transition: background 0.3s ease;
&:hover {
background: #e9e9e9;
}
}
.droppableHack {
display: none;
position: fixed;
}
.items {
margin-top: 8px;
max-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
@supports (-moz-appearance: none) {
scrollbar-color: rgba(0, 0, 0, 0.32) transparent;
scrollbar-width: thin;
}
&::-webkit-scrollbar {
width: 9px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-clip: padding-box;
border-left: 0.25em transparent solid;
border-radius: 3px;
}
}
}

View file

@ -0,0 +1,61 @@
/*!
* 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 ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Draggable } from 'react-beautiful-dnd';
import { Button } from 'semantic-ui-react';
import selectors from '../../../selectors';
import styles from './Item.module.scss';
const Item = React.memo(({ id, index, onEdit }) => {
const selectCustomFieldGroupById = useMemo(() => selectors.makeSelectCustomFieldGroupById(), []);
const customFieldGroup = useSelector((state) => selectCustomFieldGroupById(state, id));
const handleEditClick = useCallback(() => {
onEdit(id);
}, [id, onEdit]);
return (
<Draggable draggableId={id} index={index} isDragDisabled={!customFieldGroup.isPersisted}>
{({ innerRef, draggableProps, dragHandleProps }, { isDragging }) => {
const contentNode = (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...draggableProps} ref={innerRef} className={styles.wrapper}>
<span
{...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
className={styles.name}
>
{customFieldGroup.name}
</span>
<Button
icon="pencil"
size="small"
floated="right"
disabled={!customFieldGroup.isPersisted}
className={styles.editButton}
onClick={handleEditClick}
/>
</div>
);
return isDragging ? ReactDOM.createPortal(contentNode, document.body) : contentNode;
}}
</Draggable>
);
});
Item.propTypes = {
id: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
onEdit: PropTypes.func.isRequired,
};
export default Item;

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) {
.editButton {
background: transparent;
box-shadow: none;
flex: 0 0 auto;
font-weight: normal;
padding: 8px 10px;
text-decoration: underline;
&:hover {
background: #e9e9e9;
}
}
.name {
background: rgba(9, 30, 66, 0.04);
border-radius: 3px;
color: #17394d;
flex: 1 1 auto;
font-size: 14px;
overflow: hidden;
padding: 8px 32px 8px 10px;
position: relative;
text-overflow: ellipsis;
}
.wrapper {
display: flex;
margin-bottom: 4px;
max-width: 280px;
white-space: nowrap;
}
}

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