mirror of
https://github.com/plankanban/planka.git
synced 2025-12-25 01:11:49 +03:00
157
client/src/components/projects/AddProjectModal/AddProjectModal.jsx
Executable file
157
client/src/components/projects/AddProjectModal/AddProjectModal.jsx
Executable file
@@ -0,0 +1,157 @@
|
||||
/*!
|
||||
* 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 } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { Button, Form, Header, Icon, TextArea } from 'semantic-ui-react';
|
||||
import { usePopup } from '../../../lib/popup';
|
||||
import { Input } from '../../../lib/custom-ui';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { useClosableModal, useForm, useNestedRef } from '../../../hooks';
|
||||
import { isModifierKeyPressed } from '../../../utils/event-helpers';
|
||||
import { ProjectTypes } from '../../../constants/Enums';
|
||||
import { ProjectTypeIcons } from '../../../constants/Icons';
|
||||
import SelectTypeStep from './SelectTypeStep';
|
||||
|
||||
import styles from './AddProjectModal.module.scss';
|
||||
|
||||
const AddProjectModal = React.memo(() => {
|
||||
const defaultType = useSelector(
|
||||
(state) => selectors.selectCurrentModal(state).params.defaultType,
|
||||
);
|
||||
|
||||
const { data: defaultData, isSubmitting } = useSelector(selectors.selectProjectCreateForm);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [data, handleFieldChange, setData] = useForm(() => ({
|
||||
name: '',
|
||||
description: '',
|
||||
type: ProjectTypes.PRIVATE,
|
||||
...defaultData,
|
||||
...(defaultType && {
|
||||
type: defaultType,
|
||||
}),
|
||||
}));
|
||||
|
||||
const [nameFieldRef, handleNameFieldRef] = useNestedRef('inputRef');
|
||||
|
||||
const submit = useCallback(() => {
|
||||
const cleanData = {
|
||||
...data,
|
||||
name: data.name.trim(),
|
||||
description: data.description.trim() || null,
|
||||
};
|
||||
|
||||
if (!cleanData.name) {
|
||||
nameFieldRef.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(entryActions.createProject(cleanData));
|
||||
}, [dispatch, data, nameFieldRef]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(entryActions.closeModal());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
submit();
|
||||
}, [submit]);
|
||||
|
||||
const handleDescriptionKeyDown = useCallback(
|
||||
(event) => {
|
||||
if (isModifierKeyPressed(event) && event.key === 'Enter') {
|
||||
submit();
|
||||
}
|
||||
},
|
||||
[submit],
|
||||
);
|
||||
|
||||
const handleTypeSelect = useCallback(
|
||||
(type) => {
|
||||
setData((prevData) => ({
|
||||
...prevData,
|
||||
type,
|
||||
}));
|
||||
},
|
||||
[setData],
|
||||
);
|
||||
|
||||
const [ClosableModal, , activateClosable, deactivateClosable] = useClosableModal();
|
||||
|
||||
const handleSelectTypeClose = useCallback(() => {
|
||||
deactivateClosable();
|
||||
nameFieldRef.current.focus();
|
||||
}, [deactivateClosable, nameFieldRef]);
|
||||
|
||||
useEffect(() => {
|
||||
nameFieldRef.current.focus();
|
||||
}, [nameFieldRef]);
|
||||
|
||||
const SelectTypePopup = usePopup(SelectTypeStep, {
|
||||
onOpen: activateClosable,
|
||||
onClose: handleSelectTypeClose,
|
||||
});
|
||||
|
||||
return (
|
||||
<ClosableModal basic closeIcon size="tiny" onClose={handleClose}>
|
||||
<ClosableModal.Content>
|
||||
<Header inverted size="huge">
|
||||
{t('common.createProject', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Header>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div className={styles.text}>{t('common.title')}</div>
|
||||
<Input
|
||||
fluid
|
||||
inverted
|
||||
ref={handleNameFieldRef}
|
||||
name="name"
|
||||
value={data.name}
|
||||
maxLength={128}
|
||||
readOnly={isSubmitting}
|
||||
className={styles.field}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
<div className={styles.text}>{t('common.description')}</div>
|
||||
<TextArea
|
||||
as={TextareaAutosize}
|
||||
name="description"
|
||||
value={data.description}
|
||||
maxLength={1024}
|
||||
minRows={2}
|
||||
spellCheck={false}
|
||||
className={styles.field}
|
||||
onKeyDown={handleDescriptionKeyDown}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
<Button
|
||||
inverted
|
||||
color="green"
|
||||
icon="checkmark"
|
||||
content={t('action.createProject')}
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<SelectTypePopup value={data.type} onSelect={handleTypeSelect}>
|
||||
<Button type="button" className={styles.selectTypeButton}>
|
||||
<Icon name={ProjectTypeIcons[data.type]} className={styles.selectTypeButtonIcon} />
|
||||
{t(`common.${data.type}`)}
|
||||
</Button>
|
||||
</SelectTypePopup>
|
||||
</Form>
|
||||
</ClosableModal.Content>
|
||||
</ClosableModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default AddProjectModal;
|
||||
@@ -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) {
|
||||
.field {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.selectTypeButton {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: #6b808c;
|
||||
float: right;
|
||||
font-weight: normal;
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
padding: 6px 11px;
|
||||
text-align: left;
|
||||
text-decoration: underline;
|
||||
transition: none;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: rgba(246, 225, 189, 0.08);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.selectTypeButtonIcon {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*!
|
||||
* 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 { useTranslation } from 'react-i18next';
|
||||
import { Icon, Menu } from 'semantic-ui-react';
|
||||
import { Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import { ProjectTypes } from '../../../constants/Enums';
|
||||
import { ProjectTypeIcons } from '../../../constants/Icons';
|
||||
|
||||
import styles from './SelectTypeStep.module.scss';
|
||||
|
||||
const DESCRIPTION_BY_TYPE = {
|
||||
[ProjectTypes.PRIVATE]: 'common.forPersonalProjects',
|
||||
[ProjectTypes.SHARED]: 'common.forTeamBasedProjects',
|
||||
};
|
||||
|
||||
const SelectTypeStep = React.memo(({ value, onSelect, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleSelectClick = useCallback(
|
||||
(_, { value: nextValue }) => {
|
||||
if (nextValue !== value) {
|
||||
onSelect(nextValue);
|
||||
}
|
||||
|
||||
onClose();
|
||||
},
|
||||
[value, onSelect, onClose],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.selectType', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
{[ProjectTypes.PRIVATE, ProjectTypes.SHARED].map((type) => (
|
||||
<Menu.Item
|
||||
key={type}
|
||||
value={type}
|
||||
active={type === value}
|
||||
className={styles.menuItem}
|
||||
onClick={handleSelectClick}
|
||||
>
|
||||
<Icon name={ProjectTypeIcons[type]} className={styles.menuItemIcon} />
|
||||
<div className={styles.menuItemTitle}>{t(`common.${type}`)}</div>
|
||||
<p className={styles.menuItemDescription}>{t(DESCRIPTION_BY_TYPE[type])}</p>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
SelectTypeStep.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SelectTypeStep;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
8
client/src/components/projects/AddProjectModal/index.js
Normal file
8
client/src/components/projects/AddProjectModal/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 AddProjectModal from './AddProjectModal';
|
||||
|
||||
export default AddProjectModal;
|
||||
45
client/src/components/projects/Project/Project.jsx
Executable file
45
client/src/components/projects/Project/Project.jsx
Executable file
@@ -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
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import ModalTypes from '../../../constants/ModalTypes';
|
||||
import ProjectSettingsModal from '../ProjectSettingsModal';
|
||||
import Boards from '../../boards/Boards';
|
||||
import BoardSettingsModal from '../../boards/BoardSettingsModal';
|
||||
|
||||
import styles from './Project.module.scss';
|
||||
|
||||
const Project = React.memo(() => {
|
||||
const modal = useSelector(selectors.selectCurrentModal);
|
||||
|
||||
let modalNode = null;
|
||||
if (modal) {
|
||||
switch (modal.type) {
|
||||
case ModalTypes.PROJECT_SETTINGS:
|
||||
modalNode = <ProjectSettingsModal />;
|
||||
|
||||
break;
|
||||
case ModalTypes.BOARD_SETTINGS:
|
||||
modalNode = <BoardSettingsModal />;
|
||||
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.wrapper}>
|
||||
<Boards />
|
||||
</div>
|
||||
{modalNode}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Project;
|
||||
14
client/src/components/projects/Project/Project.module.scss
Normal file
14
client/src/components/projects/Project/Project.module.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.wrapper {
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 10px 20px 0;
|
||||
}
|
||||
}
|
||||
8
client/src/components/projects/Project/index.js
Executable file
8
client/src/components/projects/Project/index.js
Executable 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 Project from './Project';
|
||||
|
||||
export default Project;
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import React, { useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import { ProjectBackgroundTypes } from '../../../constants/Enums';
|
||||
|
||||
import styles from './ProjectBackground.module.scss';
|
||||
import globalStyles from '../../../styles.module.scss';
|
||||
|
||||
const ProjectBackground = React.memo(() => {
|
||||
const selectBackgroundImageById = useMemo(() => selectors.makeSelectBackgroundImageById(), []);
|
||||
|
||||
const { backgroundImageId, backgroundType, backgroundGradient } = useSelector(
|
||||
selectors.selectCurrentProject,
|
||||
);
|
||||
|
||||
const backgroundImageUrl = useSelector((state) => {
|
||||
if (!backgroundType || backgroundType !== ProjectBackgroundTypes.IMAGE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const backgroundImage = selectBackgroundImageById(state, backgroundImageId);
|
||||
|
||||
if (!backgroundImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return backgroundImage.url;
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.wrapper,
|
||||
backgroundType === ProjectBackgroundTypes.GRADIENT &&
|
||||
globalStyles[`background${upperFirst(camelCase(backgroundGradient))}`],
|
||||
)}
|
||||
style={{
|
||||
background: backgroundImageUrl && `url("${backgroundImageUrl}") center / cover`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default ProjectBackground;
|
||||
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
@@ -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 ProjectBackground from './ProjectBackground';
|
||||
|
||||
export default ProjectBackground;
|
||||
188
client/src/components/projects/ProjectCard/ProjectCard.jsx
Normal file
188
client/src/components/projects/ProjectCard/ProjectCard.jsx
Normal file
@@ -0,0 +1,188 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Icon } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import Paths from '../../../constants/Paths';
|
||||
import { ProjectBackgroundTypes } from '../../../constants/Enums';
|
||||
import UserAvatar from '../../users/UserAvatar';
|
||||
|
||||
import styles from './ProjectCard.module.scss';
|
||||
import globalStyles from '../../../styles.module.scss';
|
||||
|
||||
const Sizes = {
|
||||
SMALL: 'small',
|
||||
LARGE: 'large',
|
||||
};
|
||||
|
||||
const ProjectCard = React.memo(
|
||||
({ id, size, isActive, withDescription, withTypeIndicator, withFavoriteButton, className }) => {
|
||||
const selectProjectById = useMemo(() => selectors.makeSelectProjectById(), []);
|
||||
|
||||
const selectFirstBoardIdByProjectId = useMemo(
|
||||
() => selectors.makeSelectFirstBoardIdByProjectId(),
|
||||
[],
|
||||
);
|
||||
|
||||
const selectNotificationsTotalByProjectId = useMemo(
|
||||
() => selectors.makeSelectNotificationsTotalByProjectId(),
|
||||
[],
|
||||
);
|
||||
|
||||
const selectProjectManagerById = useMemo(() => selectors.makeSelectProjectManagerById(), []);
|
||||
const selectBackgroundImageById = useMemo(() => selectors.makeSelectBackgroundImageById(), []);
|
||||
|
||||
const project = useSelector((state) => selectProjectById(state, id));
|
||||
const firstBoardId = useSelector((state) => selectFirstBoardIdByProjectId(state, id));
|
||||
|
||||
const notificationsTotal = useSelector((state) =>
|
||||
selectNotificationsTotalByProjectId(state, id),
|
||||
);
|
||||
|
||||
const ownerProjectManager = useSelector(
|
||||
(state) =>
|
||||
project.ownerProjectManagerId &&
|
||||
selectProjectManagerById(state, project.ownerProjectManagerId),
|
||||
);
|
||||
|
||||
const backgroundImageUrl = useSelector((state) => {
|
||||
if (!project.backgroundType || project.backgroundType !== ProjectBackgroundTypes.IMAGE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const backgroundImage = selectBackgroundImageById(state, project.backgroundImageId);
|
||||
|
||||
if (!backgroundImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return backgroundImage.thumbnailUrls.outside360;
|
||||
});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleToggleFavoriteClick = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateProject(project.id, {
|
||||
isFavorite: !project.isFavorite,
|
||||
}),
|
||||
);
|
||||
}, [project, dispatch]);
|
||||
|
||||
const withSidebar = withTypeIndicator || (withFavoriteButton && !project.isHidden);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
styles.wrapper,
|
||||
styles[`wrapper${upperFirst(size)}`],
|
||||
project.isHidden && styles.wrapperHidden,
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
to={
|
||||
firstBoardId
|
||||
? Paths.BOARDS.replace(':id', firstBoardId)
|
||||
: Paths.PROJECTS.replace(':id', id)
|
||||
}
|
||||
className={styles.content}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.cover,
|
||||
project.backgroundType === ProjectBackgroundTypes.GRADIENT &&
|
||||
globalStyles[`background${upperFirst(camelCase(project.backgroundGradient))}`],
|
||||
)}
|
||||
style={{
|
||||
background: backgroundImageUrl && `url("${backgroundImageUrl}") center / cover`,
|
||||
}}
|
||||
/>
|
||||
{notificationsTotal > 0 && (
|
||||
<span className={styles.notifications}>{notificationsTotal}</span>
|
||||
)}
|
||||
<div
|
||||
className={classNames(styles.information, withSidebar && styles.informationWithSidebar)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.title,
|
||||
isActive !== undefined && styles.titleActivatable,
|
||||
isActive && styles.titleActive,
|
||||
)}
|
||||
>
|
||||
{project.name}
|
||||
</div>
|
||||
{withDescription && project.description && (
|
||||
<div className={styles.description}>{project.description}</div>
|
||||
)}
|
||||
</div>
|
||||
{withTypeIndicator && (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.typeIndicator,
|
||||
ownerProjectManager && styles.typeIndicatorWithUser,
|
||||
)}
|
||||
>
|
||||
{ownerProjectManager ? (
|
||||
<UserAvatar id={ownerProjectManager.userId} size="small" />
|
||||
) : (
|
||||
<Icon
|
||||
fitted
|
||||
name="group"
|
||||
className={classNames(styles.icon, styles.typeIndicatorIcon)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
{withFavoriteButton && !project.isHidden && (
|
||||
<Button
|
||||
className={classNames(
|
||||
styles.favoriteButton,
|
||||
!project.isFavorite && styles.favoriteButtonAppearable,
|
||||
)}
|
||||
onClick={handleToggleFavoriteClick}
|
||||
>
|
||||
<Icon
|
||||
fitted
|
||||
name={project.isFavorite ? 'star' : 'star outline'}
|
||||
className={classNames(styles.icon, styles.favoriteButtonIcon)}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ProjectCard.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
size: PropTypes.oneOf(Object.values(Sizes)),
|
||||
isActive: PropTypes.bool,
|
||||
withDescription: PropTypes.bool,
|
||||
withTypeIndicator: PropTypes.bool,
|
||||
withFavoriteButton: PropTypes.bool,
|
||||
className: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
ProjectCard.defaultProps = {
|
||||
size: Sizes.LARGE,
|
||||
isActive: undefined,
|
||||
withDescription: false,
|
||||
withTypeIndicator: false,
|
||||
withFavoriteButton: false,
|
||||
};
|
||||
|
||||
export default ProjectCard;
|
||||
@@ -0,0 +1,196 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.content {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.cover {
|
||||
background: #555;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: filter 0.2s ease;
|
||||
|
||||
&::after {
|
||||
background: #000;
|
||||
bottom: 0;
|
||||
content: "";
|
||||
left: 0;
|
||||
opacity: 0.3;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
border-left: 1px solid;
|
||||
display: -webkit-box;
|
||||
font-weight: lighter;
|
||||
hyphens: auto;
|
||||
left: 0;
|
||||
line-height: 1.2;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
transform: translate(-40px, 0);
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.favoriteButton {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
height: 36px;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
width: 36px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.favoriteButtonAppearable {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.information {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
background: #eb5a46;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
padding: 0px 6px;
|
||||
position: absolute;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.title {
|
||||
bottom: 0;
|
||||
display: -webkit-box;
|
||||
hyphens: auto;
|
||||
left: 0;
|
||||
line-height: 1.1;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
word-break: break-word;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.titleActivatable {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.titleActive {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.typeIndicator {
|
||||
bottom: 12px;
|
||||
margin-top: auto;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.typeIndicatorIcon {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.typeIndicatorWithUser {
|
||||
bottom: 12px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.cover {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
|
||||
.description {
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.favoriteButtonAppearable {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.titleActivatable {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapperHidden {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
|
||||
.wrapperSmall {
|
||||
.notifications {
|
||||
font-size: 10px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.title {
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
margin: 0 12px 10px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapperLarge {
|
||||
.description {
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 3;
|
||||
margin: 24px 20px 0 20px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.informationWithSidebar {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
margin: 0 20px 14px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
client/src/components/projects/ProjectCard/index.js
Normal file
8
client/src/components/projects/ProjectCard/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 ProjectCard from './ProjectCard';
|
||||
|
||||
export default ProjectCard;
|
||||
@@ -0,0 +1,84 @@
|
||||
/*!
|
||||
* 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 } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
|
||||
import styles from './AddImageZone.module.scss';
|
||||
|
||||
const AddImageZone = React.memo(({ children, onCreate }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleDropAccepted = useCallback(
|
||||
(files) => {
|
||||
onCreate(files[0]);
|
||||
},
|
||||
[onCreate],
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
accept: {
|
||||
'image/*': [],
|
||||
},
|
||||
multiple: false,
|
||||
noClick: true,
|
||||
noKeyboard: true,
|
||||
onDropAccepted: handleDropAccepted,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handlePaste = (event) => {
|
||||
if (!event.clipboardData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = event.clipboardData.files[0];
|
||||
|
||||
if (file) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
onCreate(file);
|
||||
return;
|
||||
}
|
||||
|
||||
const item = event.clipboardData.items[0];
|
||||
|
||||
if (!item || !item.type.startsWith('image/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.kind === 'file') {
|
||||
onCreate(item.getAsFile());
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('paste', handlePaste);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('paste', handlePaste);
|
||||
};
|
||||
}, [onCreate]);
|
||||
|
||||
return (
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
<div {...getRootProps()}>
|
||||
{isDragActive && <div className={styles.dropzone}>{t('common.dropFileToUpload')}</div>}
|
||||
{children}
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<input {...getInputProps()} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
AddImageZone.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AddImageZone;
|
||||
@@ -0,0 +1,20 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.dropzone {
|
||||
background: white;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
height: 100%;
|
||||
line-height: 30px;
|
||||
opacity: 0.7;
|
||||
padding-top: 200px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
z-index: 2001;
|
||||
}
|
||||
}
|
||||
@@ -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 React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Tab } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../../selectors';
|
||||
import entryActions from '../../../../entry-actions';
|
||||
import { ProjectBackgroundTypes } from '../../../../constants/Enums';
|
||||
import Gradients from './Gradients';
|
||||
import Images from './Images';
|
||||
import AddImageZone from './AddImageZone';
|
||||
|
||||
import styles from './BackgroundPane.module.scss';
|
||||
|
||||
const TITLE_BY_TYPE = {
|
||||
[ProjectBackgroundTypes.GRADIENT]: 'common.gradients',
|
||||
[ProjectBackgroundTypes.IMAGE]: 'common.uploadedImages',
|
||||
};
|
||||
|
||||
const BackgroundPane = React.memo(() => {
|
||||
const { backgroundType: currentType } = useSelector(selectors.selectCurrentProject);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [activeType, setActiveType] = useState(
|
||||
() => currentType || ProjectBackgroundTypes.GRADIENT,
|
||||
);
|
||||
|
||||
const handleImageCreate = useCallback(
|
||||
(file) => {
|
||||
dispatch(
|
||||
entryActions.createBackgroundImageInCurrentProject({
|
||||
file,
|
||||
}),
|
||||
);
|
||||
|
||||
setActiveType(ProjectBackgroundTypes.IMAGE);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleActiveTypeChange = useCallback((_, { value }) => {
|
||||
setActiveType(value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||
<AddImageZone onCreate={handleImageCreate}>
|
||||
<Button.Group fluid basic className={styles.activeTypeButtonGroup}>
|
||||
{[ProjectBackgroundTypes.GRADIENT, ProjectBackgroundTypes.IMAGE].map((type) => (
|
||||
<Button
|
||||
key={type}
|
||||
type="button"
|
||||
value={type}
|
||||
active={type === activeType}
|
||||
onClick={handleActiveTypeChange}
|
||||
>
|
||||
{t(TITLE_BY_TYPE[type])}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
{activeType === ProjectBackgroundTypes.GRADIENT && <Gradients />}
|
||||
{activeType === ProjectBackgroundTypes.IMAGE && <Images />}
|
||||
</AddImageZone>
|
||||
</Tab.Pane>
|
||||
);
|
||||
});
|
||||
|
||||
export default BackgroundPane;
|
||||
@@ -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) {
|
||||
.activeTypeButtonGroup {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*!
|
||||
* 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 BACKGROUND_GRADIENTS from '../../../../../constants/BackgroundGradients';
|
||||
import Item from './Item';
|
||||
|
||||
import styles from './Gradients.module.scss';
|
||||
|
||||
const Gradients = React.memo(() => (
|
||||
<div className={styles.wrapper}>
|
||||
{BACKGROUND_GRADIENTS.map((backgroundGradient) => (
|
||||
<Item key={backgroundGradient} name={backgroundGradient} />
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
|
||||
export default Gradients;
|
||||
@@ -0,0 +1,39 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.wrapper {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
|
||||
@supports (-moz-appearance: none) {
|
||||
scrollbar-color: rgba(0, 0, 0, 0.32) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
&:after {
|
||||
clear: both;
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
|
||||
&::-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Button, Label } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../../../selectors';
|
||||
import entryActions from '../../../../../entry-actions';
|
||||
import { ProjectBackgroundTypes } from '../../../../../constants/Enums';
|
||||
|
||||
import styles from './Item.module.scss';
|
||||
import globalStyles from '../../../../../styles.module.scss';
|
||||
|
||||
const Item = React.memo(({ name }) => {
|
||||
const isActive = useSelector((state) => {
|
||||
const { backgroundType, backgroundGradient } = selectors.selectCurrentProject(state);
|
||||
return backgroundType === ProjectBackgroundTypes.GRADIENT && name === backgroundGradient;
|
||||
});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentProject(
|
||||
isActive
|
||||
? {
|
||||
backgroundType: null,
|
||||
backgroundGradient: null,
|
||||
}
|
||||
: {
|
||||
backgroundType: ProjectBackgroundTypes.GRADIENT,
|
||||
backgroundGradient: name,
|
||||
},
|
||||
),
|
||||
);
|
||||
}, [name, isActive, dispatch]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
className={classNames(
|
||||
styles.wrapper,
|
||||
globalStyles[`background${upperFirst(camelCase(name))}`],
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{isActive && (
|
||||
<Label
|
||||
corner="left"
|
||||
size="mini"
|
||||
icon={{
|
||||
name: 'checkmark',
|
||||
color: 'grey',
|
||||
inverted: true,
|
||||
}}
|
||||
className={styles.label}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
Item.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Item;
|
||||
@@ -0,0 +1,25 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.label {
|
||||
border-color: rgba(29, 46, 63, 0.8);
|
||||
|
||||
i {
|
||||
cursor: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
height: 74px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Gradients from './Gradients';
|
||||
|
||||
export default Gradients;
|
||||
@@ -0,0 +1,76 @@
|
||||
/*!
|
||||
* 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 { Button, Icon, Label } from 'semantic-ui-react';
|
||||
|
||||
import { usePopupInClosableContext } from '../../../../hooks';
|
||||
import ConfirmationStep from '../../../common/ConfirmationStep';
|
||||
|
||||
import styles from './Image.module.scss';
|
||||
|
||||
const Image = React.memo(({ url, isActive, onSelect, onDeselect, onDelete }) => {
|
||||
const handleClick = useCallback(() => {
|
||||
if (isActive) {
|
||||
onDeselect();
|
||||
} else {
|
||||
onSelect();
|
||||
}
|
||||
}, [isActive, onSelect, onDeselect]);
|
||||
|
||||
const ConfirmationPopup = usePopupInClosableContext(ConfirmationStep);
|
||||
|
||||
return (
|
||||
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
|
||||
jsx-a11y/no-static-element-interactions */
|
||||
<div
|
||||
className={styles.wrapper}
|
||||
style={{
|
||||
background: `url("${url}") center / cover`,
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{isActive && (
|
||||
<Label
|
||||
corner="left"
|
||||
size="mini"
|
||||
icon={{
|
||||
name: 'checkmark',
|
||||
color: 'grey',
|
||||
inverted: true,
|
||||
}}
|
||||
className={styles.label}
|
||||
/>
|
||||
)}
|
||||
{onDelete && (
|
||||
<ConfirmationPopup
|
||||
title="common.deleteBackgroundImage"
|
||||
content="common.areYouSureYouWantToDeleteThisBackgroundImage"
|
||||
buttonContent="action.deleteBackgroundImage"
|
||||
onConfirm={onDelete}
|
||||
>
|
||||
<Button className={styles.deleteButton}>
|
||||
<Icon fitted name="trash alternate" size="small" />
|
||||
</Button>
|
||||
</ConfirmationPopup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Image.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onDeselect: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func,
|
||||
};
|
||||
|
||||
Image.defaultProps = {
|
||||
onDelete: undefined,
|
||||
};
|
||||
|
||||
export default Image;
|
||||
@@ -0,0 +1,43 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.deleteButton {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: none;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
min-height: 24px;
|
||||
opacity: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.label {
|
||||
border-color: rgba(29, 46, 63, 0.8);
|
||||
|
||||
i {
|
||||
cursor: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
height: 74px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
|
||||
.deleteButton {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, useEffect, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
import { FilePicker } from '../../../../../lib/custom-ui';
|
||||
|
||||
import selectors from '../../../../../selectors';
|
||||
import entryActions from '../../../../../entry-actions';
|
||||
import Item from './Item';
|
||||
|
||||
import styles from './Images.module.scss';
|
||||
|
||||
const Images = React.memo(() => {
|
||||
const backgroundImageIds = useSelector(selectors.selectBackgroundImageIdsForCurrentProject);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const fieldRef = useRef(null);
|
||||
|
||||
const handleFileSelect = useCallback(
|
||||
(file) => {
|
||||
dispatch(
|
||||
entryActions.createBackgroundImageInCurrentProject({
|
||||
file,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fieldRef.current.focus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.images}>
|
||||
{backgroundImageIds.map((backgroundImageId) => (
|
||||
<Item key={backgroundImageId} id={backgroundImageId} />
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<div className={styles.action}>
|
||||
<FilePicker accept="image/*" onSelect={handleFileSelect}>
|
||||
<Button
|
||||
ref={fieldRef}
|
||||
content={t('action.uploadNewImage', {
|
||||
context: 'title',
|
||||
})}
|
||||
className={styles.actionButton}
|
||||
/>
|
||||
</FilePicker>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Images;
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
: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;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
background: transparent;
|
||||
color: #6b808c;
|
||||
font-weight: normal;
|
||||
height: 36px;
|
||||
line-height: 24px;
|
||||
padding: 6px 12px;
|
||||
text-align: left;
|
||||
text-decoration: underline;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.images {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
max-height: 60vh;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Loader } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../../../selectors';
|
||||
import entryActions from '../../../../../entry-actions';
|
||||
import { ProjectBackgroundTypes } from '../../../../../constants/Enums';
|
||||
import Image from '../Image';
|
||||
|
||||
import styles from './Item.module.scss';
|
||||
|
||||
const Item = React.memo(({ id }) => {
|
||||
const selectBackgroundImageById = useMemo(() => selectors.makeSelectBackgroundImageById(), []);
|
||||
|
||||
const backgroundImage = useSelector((state) => selectBackgroundImageById(state, id));
|
||||
|
||||
const isActive = useSelector((state) => {
|
||||
const { backgroundType, backgroundImageId } = selectors.selectCurrentProject(state);
|
||||
return backgroundType === ProjectBackgroundTypes.IMAGE && id === backgroundImageId;
|
||||
});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleSelect = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentProject({
|
||||
backgroundType: ProjectBackgroundTypes.IMAGE,
|
||||
backgroundImageId: id,
|
||||
}),
|
||||
);
|
||||
}, [id, dispatch]);
|
||||
|
||||
const handleDeselect = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentProject({
|
||||
backgroundType: null,
|
||||
backgroundImageId: null,
|
||||
}),
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
dispatch(entryActions.deleteBackgroundImage(id));
|
||||
}, [id, dispatch]);
|
||||
|
||||
if (!backgroundImage.isPersisted) {
|
||||
return (
|
||||
<div className={styles.wrapperSubmitting}>
|
||||
<Loader inverted />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
url={backgroundImage.thumbnailUrls.outside360}
|
||||
isActive={isActive}
|
||||
onSelect={handleSelect}
|
||||
onDeselect={handleDeselect}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Item.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Item;
|
||||
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.wrapperSubmitting {
|
||||
background: rgba(9, 30, 66, 0.04);
|
||||
border-radius: 3px;
|
||||
height: 74px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@@ -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 Images from './Images';
|
||||
|
||||
export default Images;
|
||||
@@ -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 BackgroundPane from './BackgroundPane';
|
||||
|
||||
export default BackgroundPane;
|
||||
@@ -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 from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Icon, Tab } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import { usePopupInClosableContext } from '../../../hooks';
|
||||
import BaseCustomFieldGroupChip from '../../base-custom-field-groups/BaseCustomFieldGroupChip';
|
||||
import BaseCustomFieldGroupStep from '../../base-custom-field-groups/BaseCustomFieldGroupStep';
|
||||
import AddBaseCustomFieldGroupStep from '../../base-custom-field-groups/AddBaseCustomFieldGroupStep';
|
||||
|
||||
import styles from './BaseCustomFieldGroupsPane.module.scss';
|
||||
|
||||
const BaseCustomFieldGroupsPane = React.memo(() => {
|
||||
const baseCustomFieldGroupIds = useSelector(
|
||||
selectors.selectBaseCustomFieldGroupIdsForCurrentProject,
|
||||
);
|
||||
|
||||
const BaseCustomFieldGroupPopup = usePopupInClosableContext(BaseCustomFieldGroupStep);
|
||||
const AddBaseCustomFieldGroupPopup = usePopupInClosableContext(AddBaseCustomFieldGroupStep);
|
||||
|
||||
return (
|
||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||
<div className={styles.attachments}>
|
||||
{baseCustomFieldGroupIds.map((baseCustomFieldGroupId) => (
|
||||
<span key={baseCustomFieldGroupId} className={styles.attachment}>
|
||||
<BaseCustomFieldGroupPopup id={baseCustomFieldGroupId}>
|
||||
<BaseCustomFieldGroupChip id={baseCustomFieldGroupId} />
|
||||
</BaseCustomFieldGroupPopup>
|
||||
</span>
|
||||
))}
|
||||
<AddBaseCustomFieldGroupPopup>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(styles.attachment, styles.addAttachmentButton)}
|
||||
>
|
||||
<Icon name="plus" size="small" className={styles.addAttachmentButtonIcon} />
|
||||
</button>
|
||||
</AddBaseCustomFieldGroupPopup>
|
||||
</div>
|
||||
</Tab.Pane>
|
||||
);
|
||||
});
|
||||
|
||||
export default BaseCustomFieldGroupsPane;
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.addAttachmentButton {
|
||||
background: rgba(9, 30, 66, 0.04);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: #6b808c;
|
||||
cursor: pointer;
|
||||
line-height: 20px;
|
||||
padding: 6px 14px;
|
||||
transition: background 0.3s ease;
|
||||
vertical-align: top;
|
||||
|
||||
&:hover {
|
||||
background: rgba(9, 30, 66, 0.08);
|
||||
color: #17394d;
|
||||
}
|
||||
}
|
||||
|
||||
.addAttachmentButtonIcon {
|
||||
margin: 0 -4.3px;
|
||||
}
|
||||
|
||||
.attachment {
|
||||
display: inline-block;
|
||||
margin: 0 4px 4px 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.attachments {
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
margin: 0 8px 8px 0;
|
||||
max-width: 100%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*!
|
||||
* 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 TextareaAutosize from 'react-textarea-autosize';
|
||||
import { Button, Form, Input, TextArea } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../../selectors';
|
||||
import entryActions from '../../../../entry-actions';
|
||||
import { useForm, useNestedRef } from '../../../../hooks';
|
||||
import { isModifierKeyPressed } from '../../../../utils/event-helpers';
|
||||
|
||||
import styles from './EditInformation.module.scss';
|
||||
|
||||
const EditInformation = React.memo(() => {
|
||||
const project = useSelector(selectors.selectCurrentProject);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const defaultData = useMemo(
|
||||
() => ({
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
}),
|
||||
[project.name, project.description],
|
||||
);
|
||||
|
||||
const [data, handleFieldChange] = useForm(() => ({
|
||||
name: '',
|
||||
...defaultData,
|
||||
description: defaultData.description || '',
|
||||
}));
|
||||
|
||||
const cleanData = useMemo(
|
||||
() => ({
|
||||
...data,
|
||||
name: data.name.trim(),
|
||||
description: data.description.trim() || null,
|
||||
}),
|
||||
[data],
|
||||
);
|
||||
|
||||
const [nameFieldRef, handleNameFieldRef] = useNestedRef('inputRef');
|
||||
|
||||
const submit = useCallback(() => {
|
||||
if (!cleanData.name) {
|
||||
nameFieldRef.current.select();
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(entryActions.updateCurrentProject(cleanData));
|
||||
}, [dispatch, cleanData, nameFieldRef]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
submit();
|
||||
}, [submit]);
|
||||
|
||||
const handleDescriptionKeyDown = useCallback(
|
||||
(event) => {
|
||||
if (isModifierKeyPressed(event) && event.key === 'Enter') {
|
||||
submit();
|
||||
}
|
||||
},
|
||||
[submit],
|
||||
);
|
||||
|
||||
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}
|
||||
/>
|
||||
<div className={styles.text}>{t('common.description')}</div>
|
||||
<TextArea
|
||||
as={TextareaAutosize}
|
||||
name="description"
|
||||
value={data.description}
|
||||
maxLength={1024}
|
||||
minRows={2}
|
||||
spellCheck={false}
|
||||
className={styles.field}
|
||||
onKeyDown={handleDescriptionKeyDown}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
<Button positive disabled={dequal(cleanData, defaultData)} content={t('action.save')} />
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
export default EditInformation;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*!
|
||||
* 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 { Button, Divider, Header, Radio, 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 project = useSelector(selectors.selectCurrentProject);
|
||||
|
||||
const hasBoards = useSelector(
|
||||
(state) => selectors.selectBoardIdsForCurrentProject(state).length > 0,
|
||||
);
|
||||
|
||||
const canEdit = useSelector(selectors.selectIsCurrentUserManagerForCurrentProject);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleToggleChange = useCallback(
|
||||
(_, { name: fieldName, checked }) => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentProject({
|
||||
[fieldName]: checked,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleDeleteConfirm = useCallback(() => {
|
||||
dispatch(entryActions.deleteCurrentProject());
|
||||
}, [dispatch]);
|
||||
|
||||
const ConfirmationPopup = usePopupInClosableContext(ConfirmationStep);
|
||||
|
||||
return (
|
||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||
{canEdit && (
|
||||
<>
|
||||
<EditInformation />
|
||||
<Divider horizontal section>
|
||||
<Header as="h4">
|
||||
{t('common.display', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Header>
|
||||
</Divider>
|
||||
</>
|
||||
)}
|
||||
<Radio
|
||||
toggle
|
||||
name="isHidden"
|
||||
checked={project.isHidden}
|
||||
label={t('common.hideFromProjectListAndFavorites')}
|
||||
className={styles.radio}
|
||||
onChange={handleToggleChange}
|
||||
/>
|
||||
{canEdit && (
|
||||
<>
|
||||
<Divider horizontal section>
|
||||
<Header as="h4">
|
||||
{t('common.dangerZone', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Header>
|
||||
</Divider>
|
||||
<div className={styles.action}>
|
||||
<ConfirmationPopup
|
||||
title="common.deleteProject"
|
||||
content="common.areYouSureYouWantToDeleteThisProject"
|
||||
buttonContent="action.deleteProject"
|
||||
onConfirm={handleDeleteConfirm}
|
||||
>
|
||||
<Button disabled={hasBoards} className={styles.actionButton}>
|
||||
{hasBoards
|
||||
? t('common.deleteAllBoardsToBeAbleToDeleteThisProject')
|
||||
: t('action.deleteProject', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Button>
|
||||
</ConfirmationPopup>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Tab.Pane>
|
||||
);
|
||||
});
|
||||
|
||||
export default GeneralPane;
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
: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%;
|
||||
|
||||
&:has(:enabled):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%;
|
||||
}
|
||||
|
||||
.radio {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,71 @@
|
||||
/*!
|
||||
* 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 { Button, Divider, Header, Tab } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { usePopupInClosableContext } from '../../../hooks';
|
||||
import ConfirmationStep from '../../common/ConfirmationStep';
|
||||
import ProjectManagers from '../../project-managers/ProjectManagers';
|
||||
|
||||
import styles from './ManagersPane.module.scss';
|
||||
|
||||
const ManagersPane = React.memo(() => {
|
||||
// TODO: rename?
|
||||
const isShared = useSelector(
|
||||
(state) => !selectors.selectCurrentProject(state).ownerProjectManagerId,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleMakeSharedConfirm = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentProject({
|
||||
ownerProjectManagerId: null,
|
||||
}),
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const ConfirmationPopup = usePopupInClosableContext(ConfirmationStep);
|
||||
|
||||
return (
|
||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||
<ProjectManagers />
|
||||
{!isShared && (
|
||||
<>
|
||||
<Divider horizontal section>
|
||||
<Header as="h4">
|
||||
{t('common.dangerZone', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Header>
|
||||
</Divider>
|
||||
<div className={styles.action}>
|
||||
<ConfirmationPopup
|
||||
title="common.makeProjectShared"
|
||||
content="common.areYouSureYouWantToMakeThisProjectShared"
|
||||
buttonType="positive"
|
||||
buttonContent="action.makeProjectShared"
|
||||
onConfirm={handleMakeSharedConfirm}
|
||||
>
|
||||
<Button className={styles.actionButton}>
|
||||
{t('action.makeProjectShared', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Button>
|
||||
</ConfirmationPopup>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Tab.Pane>
|
||||
);
|
||||
});
|
||||
|
||||
export default ManagersPane;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } 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 ManagersPane from './ManagersPane';
|
||||
import BackgroundPane from './BackgroundPane';
|
||||
import BaseCustomFieldGroupsPane from './BaseCustomFieldGroupsPane';
|
||||
|
||||
import styles from './ProjectSettingsModal.module.scss';
|
||||
|
||||
const ProjectSettingsModal = React.memo(() => {
|
||||
const withManagablePanes = useSelector(selectors.selectIsCurrentUserManagerForCurrentProject);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(entryActions.closeModal());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleTabChange = useCallback((_, { activeIndex }) => {
|
||||
setActiveTabIndex(activeIndex);
|
||||
}, []);
|
||||
|
||||
const [ClosableModal] = useClosableModal();
|
||||
|
||||
const panes = [
|
||||
{
|
||||
menuItem: t('common.general', {
|
||||
context: 'title',
|
||||
}),
|
||||
render: () => <GeneralPane />,
|
||||
},
|
||||
{
|
||||
menuItem: t('common.managers', {
|
||||
context: 'title',
|
||||
}),
|
||||
render: () => <ManagersPane />,
|
||||
},
|
||||
];
|
||||
|
||||
if (withManagablePanes) {
|
||||
panes.push(
|
||||
{
|
||||
menuItem: t('common.background', {
|
||||
context: 'title',
|
||||
}),
|
||||
render: () => <BackgroundPane />,
|
||||
},
|
||||
{
|
||||
menuItem: t('common.baseCustomFields', {
|
||||
context: 'title',
|
||||
}),
|
||||
render: () => <BaseCustomFieldGroupsPane />,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const isBackgroundPaneActive = withManagablePanes && activeTabIndex === 2;
|
||||
|
||||
return (
|
||||
<ClosableModal
|
||||
closeIcon
|
||||
size="small"
|
||||
centered={false}
|
||||
dimmer={isBackgroundPaneActive && { className: styles.dimmerTransparent }}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<ClosableModal.Content>
|
||||
<Tab
|
||||
menu={{
|
||||
secondary: true,
|
||||
pointing: true,
|
||||
}}
|
||||
panes={panes}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
</ClosableModal.Content>
|
||||
</ClosableModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ProjectSettingsModal;
|
||||
@@ -0,0 +1,10 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.dimmerTransparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
@@ -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 ProjectSettingsModal from './ProjectSettingsModal';
|
||||
|
||||
export default ProjectSettingsModal;
|
||||
Reference in New Issue
Block a user