Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a6fc40804 | ||
|
|
42092efcd2 | ||
|
|
5c91bddfe7 | ||
|
|
fd8f40f1b0 | ||
|
|
5b64d465e2 | ||
|
|
212fb8ac8c | ||
|
|
cece2254d7 | ||
|
|
c940805f14 | ||
|
|
052ab7f653 | ||
|
|
5a38805588 | ||
|
|
2dbbead2e9 | ||
|
|
039eee0aac | ||
|
|
07867fc0b2 | ||
|
|
55d07b4560 | ||
|
|
289d2bf5ae | ||
|
|
6429e22d59 | ||
|
|
21ef850913 | ||
|
|
e4769220da | ||
|
|
378e02c0a4 | ||
|
|
51fa7df69c | ||
|
|
68806b9080 | ||
|
|
d80a538857 | ||
|
|
78669da01d |
@@ -7,4 +7,5 @@
|
||||
|
||||
server/public/*
|
||||
server/views/*
|
||||
server/logs
|
||||
client/build
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: meltyshev
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -44,6 +44,79 @@ docker-compose up -d
|
||||
|
||||
Demo user: demo@demo.demo demo
|
||||
|
||||
### Logging
|
||||
|
||||
Planka currently allows you to expose the applciation's logfile directory to the host machine via a shared volume. This feature is not enabled by default.
|
||||
|
||||
To expose the logfile director to the host machine, add the item `./logs/:/app/logs/` under `services.planka.volumes`.
|
||||
|
||||
Note that the directory to the left of the semicolon is regarding the host machine while the directory to the right of the semicolon is regarding the Docker container.
|
||||
|
||||
For example, in the above step, `./logs/:/app/logs/` will create the folder `logs` in the same directory where the `docker-compose.yml` file lives.
|
||||
|
||||
### Rotating Logs
|
||||
|
||||
Logrotate is designed to ease administration of systems that generate large numbers of log files. It allows automatic rotation, compression, removal, and mailing of log files. Each log file may be handled daily, weekly, monthly, or when it grows too large.
|
||||
|
||||
#### Setup logrotate for Planka logs
|
||||
|
||||
Create a file in `/etc/logrotate.d` named `planka` with the following contents:
|
||||
|
||||
```
|
||||
/path/to/planka/logs/planka.log {
|
||||
daily
|
||||
missingok
|
||||
rotate 14
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 640 root adm
|
||||
sharedscripts
|
||||
}
|
||||
```
|
||||
|
||||
Ensure to replace logfile directory with your installation’s `/logs/planka.log` location.
|
||||
|
||||
Restart the logrotate service.
|
||||
|
||||
### Fail2ban
|
||||
|
||||
Fail2ban is a service that uses iptables to automatically drop connections for a pre-defined amount of time from IPs that continuously failed to authenticate to the configured services.
|
||||
|
||||
#### Setup a filter and a jail for Planka
|
||||
|
||||
A filter defines regex rules to identify when users fail to authenticate on Planka's user interface.
|
||||
|
||||
Create a file in `/etc/fail2ban/filter.d` named `planka.conf` with the following contents:
|
||||
|
||||
```conf
|
||||
[Definition]
|
||||
failregex = ^(.*) Invalid (email or username:|password!) (\"(.*)\"!)? ?\(IP: <ADDR>\)$
|
||||
ignoreregex =
|
||||
```
|
||||
|
||||
The jail file defines how to handle the failed authentication attempts found by the Planka filter.
|
||||
|
||||
Create a file in `/etc/fail2ban/jail.d` named `planka.local` with the following contents:
|
||||
|
||||
```conf
|
||||
[planka]
|
||||
enabled = true
|
||||
port = http,https
|
||||
filter = planka
|
||||
logpath = /path/to/planka/logs/planka.log
|
||||
maxretry = 5
|
||||
bantime = 900
|
||||
```
|
||||
|
||||
Ensure to replace `logpath`'s value with your installation’s `/logs/planka.log` location. If you are using ports other than 80 and 443 for your Web server you should replace those too. The bantime and findtime are defined in seconds.
|
||||
|
||||
Restart the fail2ban service. You can check the status of your Planka jail by running:
|
||||
|
||||
```bash
|
||||
fail2ban-client status planka
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Clone the repository and install dependencies:
|
||||
|
||||
@@ -69,6 +69,36 @@ handleBoardMembershipCreate.fetchProject = (id, currentUserId, currentBoardId) =
|
||||
},
|
||||
});
|
||||
|
||||
const updateBoardMembership = (id, data) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
updateBoardMembership.success = (boardMembership) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE__SUCCESS,
|
||||
payload: {
|
||||
boardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
updateBoardMembership.failure = (id, error) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const handleBoardMembershipUpdate = (boardMembership) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_UPDATE_HANDLE,
|
||||
payload: {
|
||||
boardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteBoardMembership = (id) => ({
|
||||
type: ActionTypes.BOARD_MEMBERSHIP_DELETE,
|
||||
payload: {
|
||||
@@ -101,6 +131,8 @@ const handleBoardMembershipDelete = (boardMembership) => ({
|
||||
export default {
|
||||
createBoardMembership,
|
||||
handleBoardMembershipCreate,
|
||||
updateBoardMembership,
|
||||
handleBoardMembershipUpdate,
|
||||
deleteBoardMembership,
|
||||
handleBoardMembershipDelete,
|
||||
};
|
||||
|
||||
@@ -5,10 +5,14 @@ import socket from './socket';
|
||||
const createBoardMembership = (boardId, data, headers) =>
|
||||
socket.post(`/boards/${boardId}/memberships`, data, headers);
|
||||
|
||||
const updateBoardMembership = (id, data, headers) =>
|
||||
socket.patch(`/board-memberships/${id}`, data, headers);
|
||||
|
||||
const deleteBoardMembership = (id, headers) =>
|
||||
socket.delete(`/board-memberships/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createBoardMembership,
|
||||
updateBoardMembership,
|
||||
deleteBoardMembership,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import Filters from './Filters';
|
||||
import Memberships from '../Memberships';
|
||||
import BoardMembershipPermissionsSelectStep from '../BoardMembershipPermissionsSelectStep';
|
||||
|
||||
import styles from './BoardActions.module.scss';
|
||||
|
||||
@@ -15,6 +16,7 @@ const BoardActions = React.memo(
|
||||
allUsers,
|
||||
canEditMemberships,
|
||||
onMembershipCreate,
|
||||
onMembershipUpdate,
|
||||
onMembershipDelete,
|
||||
onUserToFilterAdd,
|
||||
onUserFromFilterRemove,
|
||||
@@ -30,8 +32,10 @@ const BoardActions = React.memo(
|
||||
<Memberships
|
||||
items={memberships}
|
||||
allUsers={allUsers}
|
||||
permissionsSelectStep={BoardMembershipPermissionsSelectStep}
|
||||
canEdit={canEditMemberships}
|
||||
onCreate={onMembershipCreate}
|
||||
onUpdate={onMembershipUpdate}
|
||||
onDelete={onMembershipDelete}
|
||||
/>
|
||||
</div>
|
||||
@@ -65,6 +69,7 @@ BoardActions.propTypes = {
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
canEditMemberships: PropTypes.bool.isRequired,
|
||||
onMembershipCreate: PropTypes.func.isRequired,
|
||||
onMembershipUpdate: PropTypes.func.isRequired,
|
||||
onMembershipDelete: PropTypes.func.isRequired,
|
||||
onUserToFilterAdd: PropTypes.func.isRequired,
|
||||
onUserFromFilterRemove: PropTypes.func.isRequired,
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import omit from 'lodash/omit';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Menu, Radio, Segment } from 'semantic-ui-react';
|
||||
import { Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { BoardMembershipRoles } from '../../constants/Enums';
|
||||
|
||||
import styles from './BoardMembershipPermissionsSelectStep.module.scss';
|
||||
|
||||
const BoardMembershipPermissionsSelectStep = React.memo(
|
||||
({ defaultData, title, buttonContent, onSelect, onBack }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [data, setData] = useState(() => ({
|
||||
role: BoardMembershipRoles.EDITOR,
|
||||
canComment: null,
|
||||
...defaultData,
|
||||
}));
|
||||
|
||||
const handleRoleSelectClick = useCallback((role) => {
|
||||
setData((prevData) => ({
|
||||
...prevData,
|
||||
role,
|
||||
canComment: role === BoardMembershipRoles.EDITOR ? null : !!prevData.canComment,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleSettingChange = useCallback((_, { name: fieldName, checked: value }) => {
|
||||
setData((prevData) => ({
|
||||
...prevData,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
onSelect(data.role === BoardMembershipRoles.EDITOR ? omit(data, 'canComment') : data);
|
||||
}, [onSelect, data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t(title, {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
<Menu.Item
|
||||
active={data.role === BoardMembershipRoles.EDITOR}
|
||||
onClick={() => handleRoleSelectClick(BoardMembershipRoles.EDITOR)}
|
||||
>
|
||||
<div className={styles.menuItemTitle}>{t('common.editor')}</div>
|
||||
<div className={styles.menuItemDescription}>
|
||||
{t('common.canEditContentOfBoard')}
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
active={data.role === BoardMembershipRoles.VIEWER}
|
||||
onClick={() => handleRoleSelectClick(BoardMembershipRoles.VIEWER)}
|
||||
>
|
||||
<div className={styles.menuItemTitle}>{t('common.viewer')}</div>
|
||||
<div className={styles.menuItemDescription}>{t('common.canOnlyViewBoard')}</div>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
{data.role !== BoardMembershipRoles.EDITOR && (
|
||||
<Segment basic className={styles.settings}>
|
||||
<Radio
|
||||
toggle
|
||||
name="canComment"
|
||||
checked={data.canComment}
|
||||
label={t('common.canComment')}
|
||||
onChange={handleSettingChange}
|
||||
/>
|
||||
</Segment>
|
||||
)}
|
||||
<Button positive content={t(buttonContent)} />
|
||||
</Form>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
BoardMembershipPermissionsSelectStep.propTypes = {
|
||||
defaultData: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
title: PropTypes.string,
|
||||
buttonContent: PropTypes.string,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
BoardMembershipPermissionsSelectStep.defaultProps = {
|
||||
defaultData: undefined,
|
||||
title: 'common.selectPermissions',
|
||||
buttonContent: 'action.selectPermissions',
|
||||
};
|
||||
|
||||
export default BoardMembershipPermissionsSelectStep;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
:global(#app) {
|
||||
.menu {
|
||||
margin: 0 auto 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menuItemDescription {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.menuItemTitle {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.settings {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import BoardMembershipPermissionsSelectStep from './BoardMembershipPermissionsSelectStep';
|
||||
|
||||
export default BoardMembershipPermissionsSelectStep;
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Menu } from 'semantic-ui-react';
|
||||
import { Popup } from '../../lib/custom-ui';
|
||||
import { Input, Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { useField } from '../../hooks';
|
||||
import Item from './Item';
|
||||
|
||||
import styles from './BoardMembershipsStep.module.scss';
|
||||
@@ -11,6 +12,21 @@ import styles from './BoardMembershipsStep.module.scss';
|
||||
const BoardMembershipsStep = React.memo(
|
||||
({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => {
|
||||
const [t] = useTranslation();
|
||||
const [search, handleSearchChange] = useField('');
|
||||
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||
|
||||
const filteredItems = useMemo(
|
||||
() =>
|
||||
items.filter(
|
||||
({ user }) =>
|
||||
user.email.includes(cleanSearch) ||
|
||||
user.name.toLowerCase().includes(cleanSearch) ||
|
||||
(user.username && user.username.includes(cleanSearch)),
|
||||
),
|
||||
[items, cleanSearch],
|
||||
);
|
||||
|
||||
const searchField = useRef(null);
|
||||
|
||||
const handleUserSelect = useCallback(
|
||||
(id) => {
|
||||
@@ -26,22 +42,40 @@ const BoardMembershipsStep = React.memo(
|
||||
[onUserDeselect],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
searchField.current.focus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t(title, {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
{items.map((item) => (
|
||||
<Item
|
||||
key={item.id}
|
||||
isPersisted={item.isPersisted}
|
||||
isActive={currentUserIds.includes(item.user.id)}
|
||||
user={item.user}
|
||||
onUserSelect={() => handleUserSelect(item.user.id)}
|
||||
onUserDeselect={() => handleUserDeselect(item.user.id)}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
<Input
|
||||
fluid
|
||||
ref={searchField}
|
||||
value={search}
|
||||
placeholder={t('common.searchMembers')}
|
||||
icon="search"
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
{filteredItems.length > 0 && (
|
||||
<Menu secondary vertical className={styles.menu}>
|
||||
{filteredItems.map((item) => (
|
||||
<Item
|
||||
key={item.id}
|
||||
isPersisted={item.isPersisted}
|
||||
isActive={currentUserIds.includes(item.user.id)}
|
||||
user={item.user}
|
||||
onUserSelect={() => handleUserSelect(item.user.id)}
|
||||
onUserDeselect={() => handleUserDeselect(item.user.id)}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
:global(#app) {
|
||||
.menu {
|
||||
margin: -7px auto -5px;
|
||||
margin: 8px auto 0;
|
||||
max-height: 320px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
width: 100%;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
.menuItem {
|
||||
display: block;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import styles from './Attachments.module.scss';
|
||||
const INITIALLY_VISIBLE = 4;
|
||||
|
||||
const Attachments = React.memo(
|
||||
({ items, onUpdate, onDelete, onCoverUpdate, onGalleryOpen, onGalleryClose }) => {
|
||||
({ items, canEdit, onUpdate, onDelete, onCoverUpdate, onGalleryOpen, onGalleryClose }) => {
|
||||
const [t] = useTranslation();
|
||||
const [isAllVisible, toggleAllVisible] = useToggle();
|
||||
|
||||
@@ -99,6 +99,7 @@ const Attachments = React.memo(
|
||||
createdAt={item.createdAt}
|
||||
isCover={item.isCover}
|
||||
isPersisted={item.isPersisted}
|
||||
canEdit={canEdit}
|
||||
onClick={item.image || isPdf ? open : undefined}
|
||||
onCoverSelect={() => handleCoverSelect(item.id)}
|
||||
onCoverDeselect={handleCoverDeselect}
|
||||
@@ -151,6 +152,7 @@ const Attachments = React.memo(
|
||||
|
||||
Attachments.propTypes = {
|
||||
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onCoverUpdate: PropTypes.func.isRequired,
|
||||
|
||||
@@ -17,6 +17,7 @@ const Item = React.forwardRef(
|
||||
createdAt,
|
||||
isCover,
|
||||
isPersisted,
|
||||
canEdit,
|
||||
onCoverSelect,
|
||||
onCoverDeselect,
|
||||
onClick,
|
||||
@@ -96,7 +97,7 @@ const Item = React.forwardRef(
|
||||
value: createdAt,
|
||||
})}
|
||||
</span>
|
||||
{coverUrl && (
|
||||
{coverUrl && canEdit && (
|
||||
<span className={styles.options}>
|
||||
<button type="button" className={styles.option} onClick={handleToggleCoverClick}>
|
||||
<Icon
|
||||
@@ -118,17 +119,19 @@ const Item = React.forwardRef(
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<EditPopup
|
||||
defaultData={{
|
||||
name,
|
||||
}}
|
||||
onUpdate={onUpdate}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<Button className={classNames(styles.button, styles.target)}>
|
||||
<Icon fitted name="pencil" size="small" />
|
||||
</Button>
|
||||
</EditPopup>
|
||||
{canEdit && (
|
||||
<EditPopup
|
||||
defaultData={{
|
||||
name,
|
||||
}}
|
||||
onUpdate={onUpdate}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<Button className={classNames(styles.button, styles.target)}>
|
||||
<Icon fitted name="pencil" size="small" />
|
||||
</Button>
|
||||
</EditPopup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -141,6 +144,7 @@ Item.propTypes = {
|
||||
createdAt: PropTypes.instanceOf(Date),
|
||||
isCover: PropTypes.bool.isRequired,
|
||||
isPersisted: PropTypes.bool.isRequired,
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
onCoverSelect: PropTypes.func.isRequired,
|
||||
onCoverDeselect: PropTypes.func.isRequired,
|
||||
|
||||
@@ -48,6 +48,7 @@ const CardModal = React.memo(
|
||||
allBoardMemberships,
|
||||
allLabels,
|
||||
canEdit,
|
||||
canEditCommentActivities,
|
||||
canEditAllCommentActivities,
|
||||
onUpdate,
|
||||
onMove,
|
||||
@@ -302,7 +303,10 @@ const CardModal = React.memo(
|
||||
{canEdit ? (
|
||||
<DescriptionEdit defaultValue={description} onUpdate={handleDescriptionUpdate}>
|
||||
{description ? (
|
||||
<button type="button" className={styles.descriptionText}>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(styles.descriptionText, styles.cursorPointer)}
|
||||
>
|
||||
<Markdown linkStopPropagation linkTarget="_blank">
|
||||
{description}
|
||||
</Markdown>
|
||||
@@ -348,6 +352,7 @@ const CardModal = React.memo(
|
||||
<div className={styles.moduleHeader}>{t('common.attachments')}</div>
|
||||
<Attachments
|
||||
items={attachments}
|
||||
canEdit={canEdit}
|
||||
onUpdate={onAttachmentUpdate}
|
||||
onDelete={onAttachmentDelete}
|
||||
onCoverUpdate={handleCoverUpdate}
|
||||
@@ -363,7 +368,7 @@ const CardModal = React.memo(
|
||||
isAllFetched={isAllActivitiesFetched}
|
||||
isDetailsVisible={isActivitiesDetailsVisible}
|
||||
isDetailsFetching={isActivitiesDetailsFetching}
|
||||
canEdit={canEdit}
|
||||
canEdit={canEditCommentActivities}
|
||||
canEditAllComments={canEditAllCommentActivities}
|
||||
onFetch={onActivitiesFetch}
|
||||
onDetailsToggle={onActivitiesDetailsToggle}
|
||||
@@ -508,6 +513,7 @@ CardModal.propTypes = {
|
||||
allLabels: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
canEditCommentActivities: PropTypes.bool.isRequired,
|
||||
canEditAllCommentActivities: PropTypes.bool.isRequired,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onMove: PropTypes.func.isRequired,
|
||||
|
||||
@@ -64,6 +64,10 @@
|
||||
padding: 8px 8px 0 16px;
|
||||
}
|
||||
|
||||
.cursorPointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dueDate {
|
||||
background: rgba(9, 30, 66, 0.04);
|
||||
border: none;
|
||||
@@ -114,7 +118,6 @@
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #17394d;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
margin-bottom: 8px;
|
||||
outline: none;
|
||||
|
||||
@@ -36,7 +36,7 @@ const CardMoveStep = React.memo(
|
||||
[selectedBoard, path.listId],
|
||||
);
|
||||
|
||||
const handleBoardIdFieldChange = useCallback(
|
||||
const handleBoardIdChange = useCallback(
|
||||
(event, data) => {
|
||||
if (selectedProject.boards.find((board) => board.id === data.value).isFetching === null) {
|
||||
onBoardFetch(data.value);
|
||||
@@ -102,7 +102,7 @@ const CardMoveStep = React.memo(
|
||||
}
|
||||
disabled={selectedProject.boards.length === 0}
|
||||
className={styles.field}
|
||||
onChange={handleBoardIdFieldChange}
|
||||
onChange={handleBoardIdChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -74,7 +74,11 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>{t('common.notifications')}</Popup.Header>
|
||||
<Popup.Header>
|
||||
{t('common.notifications', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
{items.length > 0
|
||||
? items.map((item) => (
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import pick from 'lodash/pick';
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
import { Popup } from '../../lib/custom-ui';
|
||||
import { Input, Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { useSteps } from '../../hooks';
|
||||
import { useField, useSteps } from '../../hooks';
|
||||
import AddStep from './AddStep';
|
||||
import EditStep from './EditStep';
|
||||
import Item from './Item';
|
||||
@@ -21,6 +21,20 @@ const LabelsStep = React.memo(
|
||||
({ items, currentIds, title, onSelect, onDeselect, onCreate, onUpdate, onDelete, onBack }) => {
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
const [search, handleSearchChange] = useField('');
|
||||
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||
|
||||
const filteredItems = useMemo(
|
||||
() =>
|
||||
items.filter(
|
||||
(label) =>
|
||||
(label.name && label.name.toLowerCase().includes(cleanSearch)) ||
|
||||
label.color.includes(cleanSearch),
|
||||
),
|
||||
[items, cleanSearch],
|
||||
);
|
||||
|
||||
const searchField = useRef(null);
|
||||
|
||||
const handleAddClick = useCallback(() => {
|
||||
openStep(StepTypes.ADD);
|
||||
@@ -63,6 +77,10 @@ const LabelsStep = React.memo(
|
||||
[onDelete],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
searchField.current.focus();
|
||||
}, []);
|
||||
|
||||
if (step) {
|
||||
switch (step.type) {
|
||||
case StepTypes.ADD:
|
||||
@@ -91,20 +109,36 @@ const LabelsStep = React.memo(
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t(title, {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
{items.map((item) => (
|
||||
<Item
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
color={item.color}
|
||||
isPersisted={item.isPersisted}
|
||||
isActive={currentIds.includes(item.id)}
|
||||
onSelect={() => handleSelect(item.id)}
|
||||
onDeselect={() => handleDeselect(item.id)}
|
||||
onEdit={() => handleEdit(item.id)}
|
||||
/>
|
||||
))}
|
||||
<Input
|
||||
fluid
|
||||
ref={searchField}
|
||||
value={search}
|
||||
placeholder={t('common.searchLabels')}
|
||||
icon="search"
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
{filteredItems.length > 0 && (
|
||||
<div className={styles.items}>
|
||||
{filteredItems.map((item) => (
|
||||
<Item
|
||||
key={item.id}
|
||||
name={item.name}
|
||||
color={item.color}
|
||||
isPersisted={item.isPersisted}
|
||||
isActive={currentIds.includes(item.id)}
|
||||
onSelect={() => handleSelect(item.id)}
|
||||
onDeselect={() => handleDeselect(item.id)}
|
||||
onEdit={() => handleEdit(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
fluid
|
||||
content={t('action.createNewLabel')}
|
||||
|
||||
@@ -14,4 +14,24 @@
|
||||
background: #e9e9e9;
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
margin-top: 8px;
|
||||
max-height: 320px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import pick from 'lodash/pick';
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -11,12 +12,14 @@ import DeleteStep from '../DeleteStep';
|
||||
import styles from './ActionsPopup.module.scss';
|
||||
|
||||
const StepTypes = {
|
||||
EDIT_PERMISSIONS: 'EDIT_PERMISSIONS',
|
||||
DELETE: 'DELETE',
|
||||
};
|
||||
|
||||
const ActionsStep = React.memo(
|
||||
({
|
||||
user,
|
||||
membership,
|
||||
permissionsSelectStep,
|
||||
leaveButtonContent,
|
||||
leaveConfirmationTitle,
|
||||
leaveConfirmationContent,
|
||||
@@ -26,63 +29,115 @@ const ActionsStep = React.memo(
|
||||
deleteConfirmationContent,
|
||||
deleteConfirmationButtonContent,
|
||||
canLeave,
|
||||
canDelete,
|
||||
canEdit,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
onClose,
|
||||
}) => {
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
|
||||
const handleEditPermissionsClick = useCallback(() => {
|
||||
openStep(StepTypes.EDIT_PERMISSIONS);
|
||||
}, [openStep]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
openStep(StepTypes.DELETE);
|
||||
}, [openStep]);
|
||||
|
||||
if (step && step.type === StepTypes.DELETE) {
|
||||
return (
|
||||
<DeleteStep
|
||||
title={t(user.isCurrent ? leaveConfirmationTitle : deleteConfirmationTitle, {
|
||||
context: 'title',
|
||||
})}
|
||||
content={t(user.isCurrent ? leaveConfirmationContent : deleteConfirmationContent)}
|
||||
buttonContent={t(
|
||||
user.isCurrent ? leaveConfirmationButtonContent : deleteConfirmationButtonContent,
|
||||
)}
|
||||
onConfirm={onDelete}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
const handleRoleSelect = useCallback(
|
||||
(data) => {
|
||||
if (onUpdate) {
|
||||
onUpdate(data);
|
||||
}
|
||||
|
||||
onClose();
|
||||
},
|
||||
[onUpdate, onClose],
|
||||
);
|
||||
|
||||
if (step) {
|
||||
switch (step.type) {
|
||||
case StepTypes.EDIT_PERMISSIONS: {
|
||||
const PermissionsSelectStep = permissionsSelectStep;
|
||||
|
||||
return (
|
||||
<PermissionsSelectStep
|
||||
defaultData={pick(membership, ['role', 'canComment'])}
|
||||
title="common.editPermissions"
|
||||
buttonContent="action.save"
|
||||
onSelect={handleRoleSelect}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case StepTypes.DELETE:
|
||||
return (
|
||||
<DeleteStep
|
||||
title={t(
|
||||
membership.user.isCurrent ? leaveConfirmationTitle : deleteConfirmationTitle,
|
||||
{
|
||||
context: 'title',
|
||||
},
|
||||
)}
|
||||
content={t(
|
||||
membership.user.isCurrent ? leaveConfirmationContent : deleteConfirmationContent,
|
||||
)}
|
||||
buttonContent={t(
|
||||
membership.user.isCurrent
|
||||
? leaveConfirmationButtonContent
|
||||
: deleteConfirmationButtonContent,
|
||||
)}
|
||||
onConfirm={onDelete}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={styles.user}>
|
||||
<User name={user.name} avatarUrl={user.avatarUrl} size="large" />
|
||||
<User name={membership.user.name} avatarUrl={membership.user.avatarUrl} size="large" />
|
||||
</span>
|
||||
<span className={styles.content}>
|
||||
<div className={styles.name}>{user.name}</div>
|
||||
<div className={styles.email}>{user.email}</div>
|
||||
{user.isCurrent
|
||||
? canLeave && (
|
||||
<Button
|
||||
content={t(leaveButtonContent)}
|
||||
className={styles.deleteButton}
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
)
|
||||
: canDelete && (
|
||||
<Button
|
||||
content={t(deleteButtonContent)}
|
||||
className={styles.deleteButton}
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.name}>{membership.user.name}</div>
|
||||
<div className={styles.email}>{membership.user.email}</div>
|
||||
</span>
|
||||
{permissionsSelectStep && canEdit && (
|
||||
<Button
|
||||
fluid
|
||||
content={t('action.editPermissions')}
|
||||
className={styles.button}
|
||||
onClick={handleEditPermissionsClick}
|
||||
/>
|
||||
)}
|
||||
{membership.user.isCurrent
|
||||
? canLeave && (
|
||||
<Button
|
||||
fluid
|
||||
content={t(leaveButtonContent)}
|
||||
className={styles.button}
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
)
|
||||
: canEdit && (
|
||||
<Button
|
||||
fluid
|
||||
content={t(deleteButtonContent)}
|
||||
className={styles.button}
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ActionsStep.propTypes = {
|
||||
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
membership: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
permissionsSelectStep: PropTypes.elementType,
|
||||
leaveButtonContent: PropTypes.string,
|
||||
leaveConfirmationTitle: PropTypes.string,
|
||||
leaveConfirmationContent: PropTypes.string,
|
||||
@@ -92,11 +147,14 @@ ActionsStep.propTypes = {
|
||||
deleteConfirmationContent: PropTypes.string,
|
||||
deleteConfirmationButtonContent: PropTypes.string,
|
||||
canLeave: PropTypes.bool.isRequired,
|
||||
canDelete: PropTypes.bool.isRequired,
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
onUpdate: PropTypes.func,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ActionsStep.defaultProps = {
|
||||
permissionsSelectStep: undefined,
|
||||
leaveButtonContent: 'action.leaveBoard',
|
||||
leaveConfirmationTitle: 'common.leaveBoard',
|
||||
leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard',
|
||||
@@ -105,6 +163,7 @@ ActionsStep.defaultProps = {
|
||||
deleteConfirmationTitle: 'common.removeMember',
|
||||
deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard',
|
||||
deleteConfirmationButtonContent: 'action.removeMember',
|
||||
onUpdate: undefined,
|
||||
};
|
||||
|
||||
export default withPopup(ActionsStep);
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
:global(#app) {
|
||||
.button {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: #6b808c;
|
||||
font-weight: normal;
|
||||
margin-top: 8px;
|
||||
padding: 6px 11px;
|
||||
text-align: left;
|
||||
text-decoration: underline;
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
background: rgba(9, 30, 66, 0.08);
|
||||
color: #092d42;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: inline-block;
|
||||
width: calc(100% - 44px);
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
flex: 0 0 auto;
|
||||
font-weight: normal;
|
||||
margin: 0 0 0 -10px;
|
||||
padding: 11px 10px;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
}
|
||||
|
||||
.email {
|
||||
color: #888888;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
padding: 4px 0 4px 2px;
|
||||
padding: 2px 0 2px 2px;
|
||||
}
|
||||
|
||||
.name {
|
||||
@@ -36,7 +39,7 @@
|
||||
.user {
|
||||
display: inline-block;
|
||||
padding-right: 8px;
|
||||
padding-top: 8px;
|
||||
padding-top: 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,144 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { withPopup } from '../../../lib/popup';
|
||||
import { Popup } from '../../../lib/custom-ui';
|
||||
import { Input, Popup } from '../../../lib/custom-ui';
|
||||
|
||||
import { useField, useSteps } from '../../../hooks';
|
||||
import UserItem from './UserItem';
|
||||
|
||||
import styles from './AddPopup.module.scss';
|
||||
|
||||
const AddStep = React.memo(({ users, currentUserIds, title, onCreate, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
const StepTypes = {
|
||||
SELECT_PERMISSIONS: 'SELECT_PERMISSIONS',
|
||||
};
|
||||
|
||||
const handleUserSelect = useCallback(
|
||||
(id) => {
|
||||
onCreate({
|
||||
userId: id,
|
||||
});
|
||||
const AddStep = React.memo(
|
||||
({ users, currentUserIds, permissionsSelectStep, title, onCreate, onClose }) => {
|
||||
const [t] = useTranslation();
|
||||
const [step, openStep, handleBack] = useSteps();
|
||||
const [search, handleSearchChange] = useField('');
|
||||
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||
|
||||
onClose();
|
||||
},
|
||||
[onCreate, onClose],
|
||||
);
|
||||
const filteredUsers = useMemo(
|
||||
() =>
|
||||
users.filter(
|
||||
(user) =>
|
||||
user.email.includes(cleanSearch) ||
|
||||
user.name.toLowerCase().includes(cleanSearch) ||
|
||||
(user.username && user.username.includes(cleanSearch)),
|
||||
),
|
||||
[users, cleanSearch],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t(title, {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<div className={styles.menu}>
|
||||
{users.map((user) => (
|
||||
<UserItem
|
||||
key={user.id}
|
||||
name={user.name}
|
||||
avatarUrl={user.avatarUrl}
|
||||
isActive={currentUserIds.includes(user.id)}
|
||||
onSelect={() => handleUserSelect(user.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
const searchField = useRef(null);
|
||||
|
||||
const handleUserSelect = useCallback(
|
||||
(id) => {
|
||||
if (permissionsSelectStep) {
|
||||
openStep(StepTypes.SELECT_PERMISSIONS, {
|
||||
userId: id,
|
||||
});
|
||||
} else {
|
||||
onCreate({
|
||||
userId: id,
|
||||
});
|
||||
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[permissionsSelectStep, onCreate, onClose, openStep],
|
||||
);
|
||||
|
||||
const handleRoleSelect = useCallback(
|
||||
(data) => {
|
||||
onCreate({
|
||||
userId: step.params.userId,
|
||||
...data,
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
[onCreate, onClose, step],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
searchField.current.focus();
|
||||
}, []);
|
||||
|
||||
if (step) {
|
||||
switch (step.type) {
|
||||
case StepTypes.SELECT_PERMISSIONS: {
|
||||
const currentUser = users.find((user) => user.id === step.params.userId);
|
||||
|
||||
if (currentUser) {
|
||||
const PermissionsSelectStep = permissionsSelectStep;
|
||||
|
||||
return (
|
||||
<PermissionsSelectStep
|
||||
buttonContent="action.addMember"
|
||||
onSelect={handleRoleSelect}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
openStep(null);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t(title, {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Input
|
||||
fluid
|
||||
ref={searchField}
|
||||
value={search}
|
||||
placeholder={t('common.searchUsers')}
|
||||
icon="search"
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
{filteredUsers.length > 0 && (
|
||||
<div className={styles.users}>
|
||||
{filteredUsers.map((user) => (
|
||||
<UserItem
|
||||
key={user.id}
|
||||
name={user.name}
|
||||
avatarUrl={user.avatarUrl}
|
||||
isActive={currentUserIds.includes(user.id)}
|
||||
onSelect={() => handleUserSelect(user.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
AddStep.propTypes = {
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
users: PropTypes.array.isRequired,
|
||||
currentUserIds: PropTypes.array.isRequired,
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
permissionsSelectStep: PropTypes.elementType,
|
||||
title: PropTypes.string,
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
AddStep.defaultProps = {
|
||||
permissionsSelectStep: undefined,
|
||||
title: 'common.addMember',
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
:global(#app) {
|
||||
.menu {
|
||||
border: none;
|
||||
margin: -7px auto -5px;
|
||||
width: 100%;
|
||||
.users {
|
||||
margin-top: 8px;
|
||||
max-height: 320px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
.menuItem {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0.28571429rem;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
overflow: hidden;
|
||||
padding: 4px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
@@ -12,6 +12,7 @@ const Memberships = React.memo(
|
||||
({
|
||||
items,
|
||||
allUsers,
|
||||
permissionsSelectStep,
|
||||
addTitle,
|
||||
leaveButtonContent,
|
||||
leaveConfirmationTitle,
|
||||
@@ -24,6 +25,7 @@ const Memberships = React.memo(
|
||||
canEdit,
|
||||
canLeaveIfLast,
|
||||
onCreate,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}) => {
|
||||
return (
|
||||
@@ -32,7 +34,8 @@ const Memberships = React.memo(
|
||||
{items.map((item) => (
|
||||
<span key={item.id} className={styles.user}>
|
||||
<ActionsPopup
|
||||
user={item.user}
|
||||
membership={item}
|
||||
permissionsSelectStep={permissionsSelectStep}
|
||||
leaveButtonContent={leaveButtonContent}
|
||||
leaveConfirmationTitle={leaveConfirmationTitle}
|
||||
leaveConfirmationContent={leaveConfirmationContent}
|
||||
@@ -42,7 +45,8 @@ const Memberships = React.memo(
|
||||
deleteConfirmationContent={deleteConfirmationContent}
|
||||
deleteConfirmationButtonContent={deleteConfirmationButtonContent}
|
||||
canLeave={items.length > 1 || canLeaveIfLast}
|
||||
canDelete={canEdit}
|
||||
canEdit={canEdit}
|
||||
onUpdate={(data) => onUpdate(item.id, data)}
|
||||
onDelete={() => onDelete(item.id)}
|
||||
>
|
||||
<User
|
||||
@@ -59,6 +63,7 @@ const Memberships = React.memo(
|
||||
<AddPopup
|
||||
users={allUsers}
|
||||
currentUserIds={items.map((item) => item.user.id)}
|
||||
permissionsSelectStep={permissionsSelectStep}
|
||||
title={addTitle}
|
||||
onCreate={onCreate}
|
||||
>
|
||||
@@ -75,6 +80,7 @@ Memberships.propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
allUsers: PropTypes.array.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
permissionsSelectStep: PropTypes.elementType,
|
||||
addTitle: PropTypes.string,
|
||||
leaveButtonContent: PropTypes.string,
|
||||
leaveConfirmationTitle: PropTypes.string,
|
||||
@@ -87,10 +93,12 @@ Memberships.propTypes = {
|
||||
canEdit: PropTypes.bool,
|
||||
canLeaveIfLast: PropTypes.bool,
|
||||
onCreate: PropTypes.func.isRequired,
|
||||
onUpdate: PropTypes.func,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Memberships.defaultProps = {
|
||||
permissionsSelectStep: undefined,
|
||||
addTitle: undefined,
|
||||
leaveButtonContent: undefined,
|
||||
leaveConfirmationTitle: undefined,
|
||||
@@ -102,6 +110,7 @@ Memberships.defaultProps = {
|
||||
deleteConfirmationButtonContent: undefined,
|
||||
canEdit: true,
|
||||
canLeaveIfLast: true,
|
||||
onUpdate: undefined,
|
||||
};
|
||||
|
||||
export default Memberships;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
:global(#app) {
|
||||
.action {
|
||||
border: none;
|
||||
border-radius: 0.28571429rem;
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
:global(#app) {
|
||||
.action {
|
||||
border: none;
|
||||
border-radius: 0.28571429rem;
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
|
||||
import { Input, Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { useForm } from '../../hooks';
|
||||
import { isPassword } from '../../utils/validator';
|
||||
|
||||
import styles from './UserPasswordEditStep.module.scss';
|
||||
|
||||
@@ -56,7 +57,7 @@ const UserPasswordEditStep = React.memo(
|
||||
const currentPasswordField = useRef(null);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!data.password) {
|
||||
if (!data.password || !isPassword(data.password)) {
|
||||
passwordField.current.select();
|
||||
return;
|
||||
}
|
||||
@@ -112,14 +113,18 @@ const UserPasswordEditStep = React.memo(
|
||||
)}
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div className={styles.text}>{t('common.newPassword')}</div>
|
||||
<Input.Password
|
||||
fluid
|
||||
ref={passwordField}
|
||||
name="password"
|
||||
value={data.password}
|
||||
className={styles.field}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
<div className={styles.field}>
|
||||
<Input.Password
|
||||
fluid
|
||||
ref={passwordField}
|
||||
name="password"
|
||||
value={data.password}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
<div className={styles.note}>
|
||||
{t('common.mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber')}
|
||||
</div>
|
||||
</div>
|
||||
{usePasswordConfirmation && (
|
||||
<>
|
||||
<div className={styles.text}>{t('common.currentPassword')}</div>
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.note {
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #444444;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
:global(#app) {
|
||||
.action {
|
||||
border: none;
|
||||
border-radius: 0.28571429rem;
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
:global(#app) {
|
||||
.action {
|
||||
border: none;
|
||||
border-radius: 0.28571429rem;
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Button, Icon, Radio, Table } from 'semantic-ui-react';
|
||||
|
||||
import ActionsPopup from './ActionsPopup';
|
||||
import User from '../../User';
|
||||
|
||||
import styles from './Item.module.scss';
|
||||
|
||||
@@ -11,6 +12,7 @@ const Item = React.memo(
|
||||
email,
|
||||
username,
|
||||
name,
|
||||
avatarUrl,
|
||||
organization,
|
||||
phone,
|
||||
isAdmin,
|
||||
@@ -34,13 +36,16 @@ const Item = React.memo(
|
||||
|
||||
return (
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
<User name={name} avatarUrl={avatarUrl} />
|
||||
</Table.Cell>
|
||||
<Table.Cell>{name}</Table.Cell>
|
||||
<Table.Cell>{username || '-'}</Table.Cell>
|
||||
<Table.Cell>{email}</Table.Cell>
|
||||
<Table.Cell collapsing>
|
||||
<Table.Cell>
|
||||
<Radio toggle checked={isAdmin} onChange={handleIsAdminChange} />
|
||||
</Table.Cell>
|
||||
<Table.Cell collapsing>
|
||||
<Table.Cell textAlign="right">
|
||||
<ActionsPopup
|
||||
user={{
|
||||
email,
|
||||
@@ -76,6 +81,7 @@ Item.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
username: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
avatarUrl: PropTypes.string,
|
||||
organization: PropTypes.string,
|
||||
phone: PropTypes.string,
|
||||
isAdmin: PropTypes.bool.isRequired,
|
||||
@@ -96,6 +102,7 @@ Item.propTypes = {
|
||||
|
||||
Item.defaultProps = {
|
||||
username: undefined,
|
||||
avatarUrl: undefined,
|
||||
organization: undefined,
|
||||
phone: undefined,
|
||||
};
|
||||
|
||||
@@ -84,10 +84,11 @@ const UsersModal = React.memo(
|
||||
context: 'title',
|
||||
})}
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Table basic="very">
|
||||
<Modal.Content scrolling>
|
||||
<Table unstackable basic="very">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell />
|
||||
<Table.HeaderCell width={4}>{t('common.name')}</Table.HeaderCell>
|
||||
<Table.HeaderCell width={4}>{t('common.username')}</Table.HeaderCell>
|
||||
<Table.HeaderCell width={4}>{t('common.email')}</Table.HeaderCell>
|
||||
@@ -102,6 +103,7 @@ const UsersModal = React.memo(
|
||||
email={item.email}
|
||||
username={item.username}
|
||||
name={item.name}
|
||||
avatarUrl={item.avatarUrl}
|
||||
organization={item.organization}
|
||||
phone={item.phone}
|
||||
isAdmin={item.isAdmin}
|
||||
|
||||
@@ -123,6 +123,10 @@ export default {
|
||||
BOARD_MEMBERSHIP_CREATE__FAILURE: 'BOARD_MEMBERSHIP_CREATE__FAILURE',
|
||||
BOARD_MEMBERSHIP_CREATE_HANDLE: 'BOARD_MEMBERSHIP_CREATE_HANDLE',
|
||||
BOARD_MEMBERSHIP_CREATE_HANDLE__PROJECT_FETCH: 'BOARD_MEMBERSHIP_CREATE_HANDLE__PROJECT_FETCH',
|
||||
BOARD_MEMBERSHIP_UPDATE: 'BOARD_MEMBERSHIP_UPDATE',
|
||||
BOARD_MEMBERSHIP_UPDATE__SUCCESS: 'BOARD_MEMBERSHIP_UPDATE__SUCCESS',
|
||||
BOARD_MEMBERSHIP_UPDATE__FAILURE: 'BOARD_MEMBERSHIP_UPDATE__FAILURE',
|
||||
BOARD_MEMBERSHIP_UPDATE_HANDLE: 'BOARD_MEMBERSHIP_UPDATE_HANDLE',
|
||||
BOARD_MEMBERSHIP_DELETE: 'BOARD_MEMBERSHIP_DELETE',
|
||||
BOARD_MEMBERSHIP_DELETE__SUCCESS: 'BOARD_MEMBERSHIP_DELETE__SUCCESS',
|
||||
BOARD_MEMBERSHIP_DELETE__FAILURE: 'BOARD_MEMBERSHIP_DELETE__FAILURE',
|
||||
|
||||
@@ -88,6 +88,8 @@ export default {
|
||||
|
||||
MEMBERSHIP_IN_CURRENT_BOARD_CREATE: `${PREFIX}/MEMBERSHIP_IN_CURRENT_BOARD_CREATE`,
|
||||
BOARD_MEMBERSHIP_CREATE_HANDLE: `${PREFIX}/BOARD_MEMBERSHIP_CREATE_HANDLE`,
|
||||
BOARD_MEMBERSHIP_UPDATE: `${PREFIX}/BOARD_MEMBERSHIP_UPDATE`,
|
||||
BOARD_MEMBERSHIP_UPDATE_HANDLE: `${PREFIX}/BOARD_MEMBERSHIP_UPDATE_HANDLE`,
|
||||
BOARD_MEMBERSHIP_DELETE: `${PREFIX}/BOARD_MEMBERSHIP_DELETE`,
|
||||
BOARD_MEMBERSHIP_DELETE_HANDLE: `${PREFIX}/BOARD_MEMBERSHIP_DELETE_HANDLE`,
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@ export const BoardTypes = {
|
||||
KANBAN: 'kanban',
|
||||
};
|
||||
|
||||
export const BoardMembershipRoles = {
|
||||
EDITOR: 'editor',
|
||||
VIEWER: 'viewer',
|
||||
};
|
||||
|
||||
export const ActivityTypes = {
|
||||
CREATE_CARD: 'createCard',
|
||||
MOVE_CARD: 'moveCard',
|
||||
|
||||
@@ -27,6 +27,7 @@ const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
onMembershipCreate: entryActions.createMembershipInCurrentBoard,
|
||||
onMembershipUpdate: entryActions.updateBoardMembership,
|
||||
onMembershipDelete: entryActions.deleteBoardMembership,
|
||||
onUserToFilterAdd: entryActions.addUserToFilterInCurrentBoard,
|
||||
onUserFromFilterRemove: entryActions.removeUserFromFilterInCurrentBoard,
|
||||
|
||||
@@ -3,17 +3,18 @@ import { connect } from 'react-redux';
|
||||
|
||||
import selectors from '../selectors';
|
||||
import entryActions from '../entry-actions';
|
||||
import { BoardMembershipRoles } from '../constants/Enums';
|
||||
import BoardKanban from '../components/BoardKanban';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { cardId } = selectors.selectPath(state);
|
||||
const isCurrentUserMember = selectors.selectIsCurrentUserMemberForCurrentBoard(state);
|
||||
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
const listIds = selectors.selectListIdsForCurrentBoard(state);
|
||||
|
||||
return {
|
||||
listIds,
|
||||
isCardModalOpened: !!cardId,
|
||||
canEdit: isCurrentUserMember,
|
||||
canEdit: !!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
|
||||
|
||||
import selectors from '../selectors';
|
||||
import entryActions from '../entry-actions';
|
||||
import { BoardMembershipRoles } from '../constants/Enums';
|
||||
import Card from '../components/Card';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
@@ -17,7 +18,7 @@ const makeMapStateToProps = () => {
|
||||
const allProjectsToLists = selectors.selectProjectsToListsForCurrentUser(state);
|
||||
const allBoardMemberships = selectors.selectMembershipsForCurrentBoard(state);
|
||||
const allLabels = selectors.selectLabelsForCurrentBoard(state);
|
||||
const isCurrentUserMember = selectors.selectIsCurrentUserMemberForCurrentBoard(state);
|
||||
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
|
||||
const { name, dueDate, timer, coverUrl, boardId, listId, isPersisted } = selectCardById(
|
||||
state,
|
||||
@@ -47,7 +48,8 @@ const makeMapStateToProps = () => {
|
||||
allProjectsToLists,
|
||||
allBoardMemberships,
|
||||
allLabels,
|
||||
canEdit: isCurrentUserMember,
|
||||
canEdit:
|
||||
!!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import omit from 'lodash/omit';
|
||||
import selectors from '../selectors';
|
||||
import entryActions from '../entry-actions';
|
||||
import Paths from '../constants/Paths';
|
||||
import { BoardMembershipRoles } from '../constants/Enums';
|
||||
import CardModal from '../components/CardModal';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
@@ -14,7 +15,7 @@ const mapStateToProps = (state) => {
|
||||
const isCurrentUserManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
|
||||
const allBoardMemberships = selectors.selectMembershipsForCurrentBoard(state);
|
||||
const allLabels = selectors.selectLabelsForCurrentBoard(state);
|
||||
const isCurrentUserMember = selectors.selectIsCurrentUserMemberForCurrentBoard(state);
|
||||
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
|
||||
const {
|
||||
name,
|
||||
@@ -57,7 +58,11 @@ const mapStateToProps = (state) => {
|
||||
allProjectsToLists,
|
||||
allBoardMemberships,
|
||||
allLabels,
|
||||
canEdit: isCurrentUserMember,
|
||||
canEdit: !!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
|
||||
canEditCommentActivities:
|
||||
!!currentUserMembership &&
|
||||
(currentUserMembership.role === BoardMembershipRoles.EDITOR ||
|
||||
currentUserMembership.canComment),
|
||||
canEditAllCommentActivities: isCurrentUserManager,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
|
||||
|
||||
import selectors from '../selectors';
|
||||
import entryActions from '../entry-actions';
|
||||
import { BoardMembershipRoles } from '../constants/Enums';
|
||||
import List from '../components/List';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
@@ -12,7 +13,7 @@ const makeMapStateToProps = () => {
|
||||
return (state, { id, index }) => {
|
||||
const { name, isPersisted } = selectListById(state, id);
|
||||
const cardIds = selectCardIdsByListId(state, id);
|
||||
const isCurrentUserMember = selectors.selectIsCurrentUserMemberForCurrentBoard(state);
|
||||
const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -20,7 +21,8 @@ const makeMapStateToProps = () => {
|
||||
name,
|
||||
isPersisted,
|
||||
cardIds,
|
||||
canEdit: isCurrentUserMember,
|
||||
canEdit:
|
||||
!!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,6 +14,21 @@ const handleBoardMembershipCreate = (boardMembership) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const updateBoardMembership = (id, data) => ({
|
||||
type: EntryActionTypes.BOARD_MEMBERSHIP_UPDATE,
|
||||
payload: {
|
||||
id,
|
||||
data,
|
||||
},
|
||||
});
|
||||
|
||||
const handleBoardMembershipUpdate = (boardMembership) => ({
|
||||
type: EntryActionTypes.BOARD_MEMBERSHIP_UPDATE_HANDLE,
|
||||
payload: {
|
||||
boardMembership,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteBoardMembership = (id) => ({
|
||||
type: EntryActionTypes.BOARD_MEMBERSHIP_DELETE,
|
||||
payload: {
|
||||
@@ -31,6 +46,8 @@ const handleBoardMembershipDelete = (boardMembership) => ({
|
||||
export default {
|
||||
createMembershipInCurrentBoard,
|
||||
handleBoardMembershipCreate,
|
||||
updateBoardMembership,
|
||||
handleBoardMembershipUpdate,
|
||||
deleteBoardMembership,
|
||||
handleBoardMembershipDelete,
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
phone: 'Telefon',
|
||||
preferences: 'Volby',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: dejte Ctrl-V (Cmd-V na Mac) pro vložení přílohy ze schránky.',
|
||||
'Tip: dejte Ctrl-V (Cmd-V na Mac) pro vložení přílohy ze schránky',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt nenalezen',
|
||||
removeManager_title: 'Odstranit vedoucího',
|
||||
|
||||
@@ -23,7 +23,7 @@ export default {
|
||||
administrator: 'Administrator',
|
||||
all: 'Alle',
|
||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||
'Alle Änderungen werden automatisch gespeichert,<br />sobald die Verbindung wiederhergestellt wurde.',
|
||||
'Alle Änderungen werden automatisch gespeichert,<br />sobald die Verbindung wiederhergestellt wurde',
|
||||
areYouSureYouWantToDeleteThisAttachment:
|
||||
'Sind Sie sicher, dass Sie diesen Anhang löschen möchten?',
|
||||
areYouSureYouWantToDeleteThisBoard: 'Sind Sie sicher, dass Sie dieses Board löschen möchten?',
|
||||
@@ -123,7 +123,7 @@ export default {
|
||||
phone: 'Telefon',
|
||||
preferences: 'Voreinstellungen',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen.',
|
||||
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt nicht gefunden',
|
||||
removeManager_title: 'Manager entfernen',
|
||||
|
||||
@@ -41,6 +41,9 @@ export default {
|
||||
background: 'Background',
|
||||
board: 'Board',
|
||||
boardNotFound_title: 'Board Not Found',
|
||||
canComment: 'Can comment',
|
||||
canEditContentOfBoard: 'Can edit the content of the board',
|
||||
canOnlyViewBoard: 'Can only view the board',
|
||||
cardActions_title: 'Card Actions',
|
||||
cardNotFound_title: 'Card Not Found',
|
||||
cardOrActionAreDeleted: 'Card or action are deleted',
|
||||
@@ -66,6 +69,7 @@ export default {
|
||||
description: 'Description',
|
||||
detectAutomatically: 'Detect automatically',
|
||||
dropFileToUpload: 'Drop file to upload',
|
||||
editor: 'Editor',
|
||||
editAttachment_title: 'Edit Attachment',
|
||||
editAvatar_title: 'Edit Avatar',
|
||||
editBoard_title: 'Edit Board',
|
||||
@@ -74,6 +78,7 @@ export default {
|
||||
editInformation_title: 'Edit Information',
|
||||
editLabel_title: 'Edit Label',
|
||||
editPassword_title: 'Edit Password',
|
||||
editPermissions_title: 'Edit Permissions',
|
||||
editTimer_title: 'Edit Timer',
|
||||
editUsername_title: 'Edit Username',
|
||||
email: 'E-mail',
|
||||
@@ -100,6 +105,8 @@ export default {
|
||||
members: 'Members',
|
||||
minutes: 'Minutes',
|
||||
moveCard_title: 'Move Card',
|
||||
mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber:
|
||||
'Must be at least 6 characters long and contain at least one letter and number',
|
||||
name: 'Name',
|
||||
newEmail: 'New e-mail',
|
||||
newPassword: 'New password',
|
||||
@@ -116,14 +123,18 @@ export default {
|
||||
phone: 'Phone',
|
||||
preferences: 'Preferences',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard.',
|
||||
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard',
|
||||
project: 'Project',
|
||||
projectNotFound_title: 'Project Not Found',
|
||||
removeManager_title: 'Remove Manager',
|
||||
removeMember_title: 'Remove Member',
|
||||
searchLabels: 'Search labels...',
|
||||
searchMembers: 'Search members...',
|
||||
searchUsers: 'Search users...',
|
||||
seconds: 'Seconds',
|
||||
selectBoard: 'Select board',
|
||||
selectList: 'Select list',
|
||||
selectPermissions_title: 'Select Permissions',
|
||||
selectProject: 'Select project',
|
||||
settings: 'Settings',
|
||||
subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default',
|
||||
@@ -143,6 +154,7 @@ export default {
|
||||
username: 'Username',
|
||||
usernameAlreadyInUse: 'Username already in use',
|
||||
users: 'Users',
|
||||
viewer: 'Viewer',
|
||||
writeComment: 'Write a comment...',
|
||||
},
|
||||
|
||||
@@ -154,6 +166,7 @@ export default {
|
||||
addCard_title: 'Add Card',
|
||||
addComment: 'Add comment',
|
||||
addList: 'Add list',
|
||||
addMember: 'Add member',
|
||||
addMoreDetailedDescription: 'Add more detailed description',
|
||||
addTask: 'Add task',
|
||||
addToCard: 'Add to card',
|
||||
@@ -185,6 +198,7 @@ export default {
|
||||
editEmail_title: 'Edit E-mail',
|
||||
editInformation_title: 'Edit Information',
|
||||
editPassword_title: 'Edit Password',
|
||||
editPermissions: 'Edit permissions',
|
||||
editTimer_title: 'Edit Timer',
|
||||
editTitle_title: 'Edit Title',
|
||||
editUsername_title: 'Edit Username',
|
||||
|
||||
@@ -106,7 +106,7 @@ export default {
|
||||
phone: 'Teléfono',
|
||||
preferences: 'Preferencias',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: presiona Ctrl-V (Cmd-V en Mac) para añadir adjuntos desde el portapapeles.',
|
||||
'Tip: presiona Ctrl-V (Cmd-V en Mac) para añadir adjuntos desde el portapapeles',
|
||||
project: 'Proyecto',
|
||||
projectNotFound_title: 'Proyecto no encontrado',
|
||||
removeMember_title: 'Remover Miembro',
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
selectList: 'Seleccionar Lista',
|
||||
selectProject: 'Seleccionar Proyecto',
|
||||
settings: 'Configuraciones',
|
||||
subscribeToMyOwnCardsByDefault: 'Suscribirme a mis propias tarjetas por defecto.',
|
||||
subscribeToMyOwnCardsByDefault: 'Suscribirme a mis propias tarjetas por defecto',
|
||||
taskActions_title: 'Acciones de Tareas',
|
||||
tasks: 'Tareas',
|
||||
time: 'Tiempo',
|
||||
|
||||
@@ -4,6 +4,7 @@ import de from './de';
|
||||
import en from './en';
|
||||
import es from './es';
|
||||
import fr from './fr';
|
||||
import it from './it';
|
||||
import ja from './ja';
|
||||
import ko from './ko';
|
||||
import pl from './pl';
|
||||
@@ -13,7 +14,7 @@ import sv from './sv';
|
||||
import uz from './uz';
|
||||
import zh from './zh';
|
||||
|
||||
const locales = [cs, da, de, en, es, fr, ja, ko, pl, ru, sk, sv, uz, zh];
|
||||
const locales = [cs, da, de, en, es, fr, it, ja, ko, pl, ru, sk, sv, uz, zh];
|
||||
|
||||
export default locales;
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
export default {
|
||||
format: {
|
||||
date: 'd/M/yyyy',
|
||||
time: 'p',
|
||||
dateTime: '$t(format:date) $t(format:time)',
|
||||
longDate: 'MMM d',
|
||||
longDateTime: "MMMM d 'at' p",
|
||||
},
|
||||
|
||||
translation: {
|
||||
common: {
|
||||
account: 'Account',
|
||||
actions: 'Azioni',
|
||||
addAttachment_title: 'Aggiungi Allegato',
|
||||
addComment: 'Aggiungi commento',
|
||||
addManager_title: 'Aggiungi Manager',
|
||||
addMember_title: 'Aggiungi Membro',
|
||||
addUser_title: 'Aggiungi Utente',
|
||||
administrator: 'Amministratore',
|
||||
all: 'tutto',
|
||||
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
|
||||
'All changes will be automatically saved<br />after connection restored',
|
||||
areYouSureYouWantToDeleteThisAttachment: 'Sei sicuro di voler eliminare questo allegato?',
|
||||
areYouSureYouWantToDeleteThisBoard: 'Sei sicuro di voler eliminare questa bacheca?',
|
||||
areYouSureYouWantToDeleteThisCard: 'Sei sicuro di voler eliminare questa card?',
|
||||
areYouSureYouWantToDeleteThisComment: 'Sei sicuro di voler eliminare questo commento?',
|
||||
areYouSureYouWantToDeleteThisLabel: 'Sei sicuro di voler eliminare questa etichetta?',
|
||||
areYouSureYouWantToDeleteThisList: 'Sei sicuro di voler eliminare questo elenco?',
|
||||
areYouSureYouWantToDeleteThisProject: 'Sei sicuro di voler eliminare questo progetto?',
|
||||
areYouSureYouWantToDeleteThisTask: 'Sei sicuro di voler eliminare questo task?',
|
||||
areYouSureYouWantToDeleteThisUser: 'Sei sicuro di voler eliminare questo utente?',
|
||||
areYouSureYouWantToLeaveBoard: 'Sei sicuro di voler abbandonare la bacheca?',
|
||||
areYouSureYouWantToLeaveProject: 'Sei sicuro di voler abbandonare il progetto?',
|
||||
areYouSureYouWantToRemoveThisManagerFromProject:
|
||||
'Sei sicuro di voler rimuovere questo amministratore dal progetto?',
|
||||
areYouSureYouWantToRemoveThisMemberFromBoard:
|
||||
'Sei sicuro di voler rimuovere questo membro dalla bacheca?',
|
||||
attachment: 'Allegato',
|
||||
attachments: 'Allegati',
|
||||
authentication: 'Autenticazione',
|
||||
background: 'Background',
|
||||
board: 'Bacheca',
|
||||
boardNotFound_title: 'Bacheca non trovata',
|
||||
canComment: 'Può commentare',
|
||||
canEditContentOfBoard: 'Può modificare il contenuto della bacheca',
|
||||
canOnlyViewBoard: 'Può solo vedere la bacheca',
|
||||
cardActions_title: 'Card Azioni',
|
||||
cardNotFound_title: 'Card non trovata',
|
||||
cardOrActionAreDeleted: "La card o l'azione vengono eliminate",
|
||||
color: 'Colore',
|
||||
createBoard_title: 'Crea Bacheca',
|
||||
createLabel_title: 'Crea Etichetta',
|
||||
createNewOneOrSelectExistingOne: 'Crea nuovo o seleziona <br /> esistente',
|
||||
createProject_title: 'Crea Progetto',
|
||||
createTextFile_title: 'Create File di Testo',
|
||||
currentPassword: 'Password corrente',
|
||||
dangerZone_title: 'Danger Zone',
|
||||
date: 'Data',
|
||||
dueDate_title: 'Data di scadenza',
|
||||
deleteAttachment_title: 'Elimina Allegato',
|
||||
deleteBoard_title: 'Elimina Bacheca',
|
||||
deleteCard_title: 'Elimina card',
|
||||
deleteComment_title: 'Elimina Commento',
|
||||
deleteLabel_title: 'Elimina Etichetta',
|
||||
deleteList_title: 'Elimina Elenco',
|
||||
deleteProject_title: 'Elimina Progetto',
|
||||
deleteTask_title: 'Elimina Task',
|
||||
deleteUser_title: 'Elimina Utente',
|
||||
description: 'Descrizione',
|
||||
detectAutomatically: 'Rileva automaticamente',
|
||||
dropFileToUpload: 'Trascina il file da caricare',
|
||||
editor: 'Editor',
|
||||
editAttachment_title: 'Modifica Allegato',
|
||||
editAvatar_title: 'Modifica Avatar',
|
||||
editBoard_title: 'Modifica Board',
|
||||
editDueDate_title: 'Modifica Data di Scadenza',
|
||||
editEmail_title: 'Modifica E-mail',
|
||||
editInformation_title: 'Modifica Informazioni',
|
||||
editLabel_title: 'Modifica Etichetta',
|
||||
editPassword_title: 'Modifica Password',
|
||||
editPermissions_title: 'Modifica Permessi',
|
||||
editTimer_title: 'Modifica Timer',
|
||||
editUsername_title: 'Modifica Username',
|
||||
email: 'E-mail',
|
||||
emailAlreadyInUse: 'E-mail già in uso',
|
||||
enterCardTitle: 'Inserire titolo della card...',
|
||||
enterDescription: 'Inserire descrizione...',
|
||||
enterFilename: 'Inserire nome del file',
|
||||
enterListTitle: 'Inserire titolo della lista...',
|
||||
enterProjectTitle: 'Inserire titolo del progetto',
|
||||
enterTaskDescription: 'Inserire descrizione del task...',
|
||||
filterByLabels_title: 'Filtra per Etichetta',
|
||||
filterByMembers_title: 'Filtra per Membro',
|
||||
fromComputer_title: 'Dal Computer',
|
||||
general: 'Generale',
|
||||
hours: 'Ore',
|
||||
invalidCurrentPassword: 'Password corrente non valida',
|
||||
labels: 'Etichette',
|
||||
language: 'Lingua',
|
||||
leaveBoard_title: 'Lascia Bacheca',
|
||||
leaveProject_title: 'Lascia Progetto',
|
||||
list: 'Lista',
|
||||
listActions_title: 'Lista delle Azioni',
|
||||
managers: 'Managers',
|
||||
members: 'Membri',
|
||||
minutes: 'Minuti',
|
||||
moveCard_title: 'Sposta card',
|
||||
name: 'Nome',
|
||||
newEmail: 'Nuova e-mail',
|
||||
newPassword: 'Nuova password',
|
||||
newUsername: 'Nuovo username',
|
||||
noConnectionToServer: 'Nessuna connessione al server',
|
||||
noBoards: 'No bacheche',
|
||||
noLists: 'No liste',
|
||||
noProjects: 'No progetti',
|
||||
notifications: 'Notifiche',
|
||||
noUnreadNotifications: 'Nessuna notifica non letta',
|
||||
openBoard_title: 'Bacheca aperta',
|
||||
optional_inline: 'opzionale',
|
||||
organization: 'Organizazzione',
|
||||
phone: 'Telefono',
|
||||
preferences: 'Preferenze',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: prmi Ctrl-V (Cmd-V on Mac) per aggiungere un allegato dalla clipboard',
|
||||
project: 'Progetto',
|
||||
projectNotFound_title: 'Progetto non trovato',
|
||||
removeManager_title: 'Rimuovi Manager',
|
||||
removeMember_title: 'Rimuovi Membro',
|
||||
searchLabels: 'Cerca etichette...',
|
||||
searchMembers: 'Cerca membri...',
|
||||
searchUsers: 'Cerca utenti...',
|
||||
seconds: 'Secondi',
|
||||
selectBoard: 'Seleziona bacheca',
|
||||
selectList: 'Seleziona lista',
|
||||
selectPermissions_title: 'Seleziona Permessi',
|
||||
selectProject: 'Seleziona progetto',
|
||||
settings: 'Impostazioni',
|
||||
subscribeToMyOwnCardsByDefault: 'Abbonati alle mie card per impostazione predefinita',
|
||||
taskActions_title: 'Task Azioni',
|
||||
tasks: 'Tasks',
|
||||
thereIsNoPreviewAvailableForThisAttachment:
|
||||
'Non è disponibile alcuna anteprima per questo allegato',
|
||||
time: 'Tempo',
|
||||
timer: 'Timer',
|
||||
title: 'Titolo',
|
||||
userActions_title: 'Azioni Utente',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> ha aggiunto questa card a {{list}}</1>',
|
||||
userLeftNewCommentToCard:
|
||||
'{{user}} ha lasciato un nuovo commento «{{comment}}» a <2>{{card}}</2>',
|
||||
userMovedCardFromListToList: '{{user}} ha mosso <2>{{card}}</2> da {{fromList}} a {{toList}}',
|
||||
userMovedThisCardFromListToList:
|
||||
'<0>{{user}}</0><1> ha mosso questa card da {{fromList}} a {{toList}}</1>',
|
||||
username: 'Username',
|
||||
usernameAlreadyInUse: 'Username già in uso',
|
||||
users: 'Utenti',
|
||||
viewer: 'Viewer',
|
||||
writeComment: 'Scrivi un commento...',
|
||||
},
|
||||
|
||||
action: {
|
||||
addAnotherCard: "Aggiungi un'altra card",
|
||||
addAnotherList: "Aggiungi un'altra lista",
|
||||
addAnotherTask: 'Aggiungi un altro task',
|
||||
addCard: 'Aggiungi card',
|
||||
addCard_title: 'Aggiungi Card',
|
||||
addComment: 'Aggiungi commento',
|
||||
addList: 'Aggiungi lista',
|
||||
addMember: 'Add membro',
|
||||
addMoreDetailedDescription: 'Aggiungi una descrizione più dettagliata',
|
||||
addTask: 'Aggiungi task',
|
||||
addToCard: 'Aggiungi alla card',
|
||||
addUser: 'Aggiungi utente',
|
||||
createBoard: 'Crea bacheca',
|
||||
createFile: 'Crea file',
|
||||
createLabel: 'Crea etichetta',
|
||||
createNewLabel: 'Crea nuova etichetta',
|
||||
createProject: 'Crea progetto',
|
||||
delete: 'Elimina',
|
||||
deleteAttachment: 'Elimina allegato',
|
||||
deleteAvatar: 'Elimina avatar',
|
||||
deleteBoard: 'Elimina bacheca',
|
||||
deleteCard: 'Elimina card',
|
||||
deleteCard_title: 'Elimina Card',
|
||||
deleteComment: 'Elimina commento',
|
||||
deleteImage: 'Elimina immagine',
|
||||
deleteLabel: 'Elimina etichetta',
|
||||
deleteList: 'Elimina lista',
|
||||
deleteList_title: 'Elimina Lista',
|
||||
deleteProject: 'Elimina progetto',
|
||||
deleteProject_title: 'Elimina Progetto',
|
||||
deleteTask: 'Elimina task',
|
||||
deleteTask_title: 'Elimina Task',
|
||||
deleteUser: 'Elimina utente',
|
||||
edit: 'Modifica',
|
||||
editDueDate_title: 'Modifica data di scadenza',
|
||||
editDescription_title: 'Modifica Descrizione',
|
||||
editEmail_title: 'Modifica E-mail',
|
||||
editInformation_title: 'Modifca Informazioni',
|
||||
editPassword_title: 'Modifica Password',
|
||||
editPermissions: 'Modifica permessi',
|
||||
editTimer_title: 'Modifica Timer',
|
||||
editTitle_title: 'Modifica Titolo',
|
||||
editUsername_title: 'Modifica Username',
|
||||
hideDetails: 'Nascondi dettagli',
|
||||
leaveBoard: 'Lascia bacheca',
|
||||
leaveProject: 'Lascia progetto',
|
||||
logOut_title: 'Log Out',
|
||||
makeCover_title: 'Crea Cover',
|
||||
move: 'Muovi',
|
||||
moveCard_title: 'Muovi Card',
|
||||
remove: 'Rimuovi',
|
||||
removeBackground: 'Rimuovi background',
|
||||
removeCover_title: 'Rimuovi Cover',
|
||||
removeFromBoard: 'Rimuovi dalla bacheca',
|
||||
removeFromProject: 'Rimuovi dal Progetto',
|
||||
removeManager: 'Rimuovi manager',
|
||||
removeMember: 'Rimuovi membro',
|
||||
save: 'Salva',
|
||||
showAllAttachments: 'Mostra tutti gli allegati ({{hidden}} hidden)',
|
||||
showDetails: 'Mostra dettagli',
|
||||
showFewerAttachments: 'Mostra meno allegati',
|
||||
start: 'Start',
|
||||
stop: 'Stop',
|
||||
subscribe: 'Subscribe',
|
||||
unsubscribe: 'Unsubscribe',
|
||||
uploadNewAvatar: 'Carica nuovo avatar',
|
||||
uploadNewImage: 'Carica nuova immagine',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import login from './login';
|
||||
|
||||
export default {
|
||||
language: 'it',
|
||||
country: 'it',
|
||||
name: 'Italiano',
|
||||
embeddedLocale: login,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
translation: {
|
||||
common: {
|
||||
emailOrUsername: 'E-mail o username',
|
||||
invalidEmailOrUsername: 'E-mail or username invalido',
|
||||
invalidPassword: 'Password non valida',
|
||||
logInToPlanka: 'Log in Planka',
|
||||
noInternetConnection: 'Nessuna connessione internet',
|
||||
pageNotFound_title: 'Pagina non trovata',
|
||||
password: 'Password',
|
||||
projectManagement: 'Project management',
|
||||
serverConnectionFailed: 'Connesione al server fallita',
|
||||
unknownError: 'Errore sconosciuto, prova ancora',
|
||||
},
|
||||
|
||||
action: {
|
||||
logIn: 'Log in',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
phone: '電話番号',
|
||||
preferences: '環境設定',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'ヒント: Ctrl-V(MacではCmd-V)を押して、クリップボードから添付ファイルを追加します。',
|
||||
'ヒント: Ctrl-V(MacではCmd-V)を押して、クリップボードから添付ファイルを追加します',
|
||||
project: 'プロジェクト',
|
||||
projectNotFound_title: 'プロジェクトがありません',
|
||||
removeManager_title: 'マネージャーを削除',
|
||||
|
||||
@@ -106,7 +106,7 @@ export default {
|
||||
phone: 'Telefon',
|
||||
preferences: 'Preferencje',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Podpowiedź: naciśnij Ctrl-V (Cmd-V na Macu) aby dodać załącznik ze schowka.',
|
||||
'Podpowiedź: naciśnij Ctrl-V (Cmd-V na Macu) aby dodać załącznik ze schowka',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt Nie Znaleziony',
|
||||
removeMember_title: 'Usuń Członka',
|
||||
|
||||
@@ -39,6 +39,9 @@ export default {
|
||||
authentication: 'Аутентификация',
|
||||
board: 'Доска',
|
||||
boardNotFound: 'Доска не найдена',
|
||||
canComment: 'Может комментировать',
|
||||
canEditContentOfBoard: 'Может редактировать содержимое доски',
|
||||
canOnlyViewBoard: 'Может только просматривать доску',
|
||||
cardActions: 'Действия с карточкой',
|
||||
cardNotFound: 'Карточка не найдена',
|
||||
cardOrActionAreDeleted: 'Карточка или действие удалены',
|
||||
@@ -64,6 +67,7 @@ export default {
|
||||
description: 'Описание',
|
||||
detectAutomatically: 'Определить автоматически',
|
||||
dropFileToUpload: 'Перетяните файл, чтобы загрузить',
|
||||
editor: 'Редактор',
|
||||
editAttachment: 'Изменение вложения',
|
||||
editAvatar: 'Изменение аватара',
|
||||
editBoard: 'Изменение доски',
|
||||
@@ -72,6 +76,7 @@ export default {
|
||||
editEmail: 'Изменение e-mail',
|
||||
editLabel: 'Изменения метки',
|
||||
editPassword: 'Изменение пароля',
|
||||
editPermissions: 'Редактирование разрешений',
|
||||
editTimer: 'Изменение таймера',
|
||||
editTitle: 'Изменение названия',
|
||||
editUsername: 'Изменение имени пользователя',
|
||||
@@ -95,6 +100,8 @@ export default {
|
||||
members: 'Участники',
|
||||
minutes: 'Минуты',
|
||||
moveCard: 'Перемещение карточки',
|
||||
mustBeAtLeast6CharactersLongAndContainAtLeastOneLetterAndNumber:
|
||||
'Должен быть не менее 6 символов и содержать хотя бы одну букву и цифру',
|
||||
name: 'Имя',
|
||||
newEmail: 'Новый e-mail',
|
||||
newPassword: 'Новый пароль',
|
||||
@@ -111,14 +118,18 @@ export default {
|
||||
phone: 'Телефон',
|
||||
preferences: 'Предпочтения',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Совет: нажмите Ctrl-V (Cmd-V на Mac), чтобы добавить вложение из буфера обмена.',
|
||||
'Совет: нажмите Ctrl-V (Cmd-V на Mac), чтобы добавить вложение из буфера обмена',
|
||||
project: 'Проект',
|
||||
projectActions: 'Действия с проектом',
|
||||
projectNotFound: 'Проект не найден',
|
||||
removeMember: 'Удаление участника',
|
||||
searchLabels: 'Поиск меток...',
|
||||
searchMembers: 'Поиск участников...',
|
||||
searchUsers: 'Поиск пользователей...',
|
||||
seconds: 'Секунды',
|
||||
selectBoard: 'Выберите доску',
|
||||
selectList: 'Выберите список',
|
||||
selectPermissions: 'Выбор разрешений',
|
||||
selectProject: 'Выберите проект',
|
||||
settings: 'Настройки',
|
||||
subscribeToMyOwnCardsByDefault: 'По умолчанию подписаться на мои собственные карточки',
|
||||
@@ -128,7 +139,7 @@ export default {
|
||||
time: 'Время',
|
||||
timer: 'Таймер',
|
||||
title: 'Название',
|
||||
userActions_title: 'Действия с пользователем',
|
||||
userActions: 'Действия с пользователем',
|
||||
userAddedThisCardToList: '<0>{{user}}</0><1> добавил(а) эту карточку в {{list}}</1>',
|
||||
userLeftNewCommentToCard: '{{user}} оставил(а) комментарий «{{comment}}» к <2>{{card}}</2>',
|
||||
userMovedCardFromListToList:
|
||||
@@ -138,6 +149,7 @@ export default {
|
||||
username: 'Имя пользователя',
|
||||
usernameAlreadyInUse: 'Имя пользователя уже занято',
|
||||
users: 'Пользователи',
|
||||
viewer: 'Читатель',
|
||||
writeComment: 'Напишите комментарий...',
|
||||
},
|
||||
|
||||
@@ -148,6 +160,7 @@ export default {
|
||||
addCard: 'Добавить карточку',
|
||||
addComment: 'Добавить комментарий',
|
||||
addList: 'Добавить список',
|
||||
addMember: 'Добавить участника',
|
||||
addMoreDetailedDescription: 'Добавить более подробное описание',
|
||||
addTask: 'Добавить задачу',
|
||||
addToCard: 'Добавить на карточку',
|
||||
@@ -175,6 +188,7 @@ export default {
|
||||
editDescription: 'Изменить описание',
|
||||
editEmail: 'Изменить e-mail',
|
||||
editPassword: 'Изменить пароль',
|
||||
editPermissions: 'Изменить разрешения',
|
||||
editTask: 'Изменить задачу',
|
||||
editTimer: 'Изменить таймер',
|
||||
editTitle: 'Изменить название',
|
||||
|
||||
@@ -118,7 +118,7 @@ export default {
|
||||
phone: 'Telefón',
|
||||
preferences: 'Voľby',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tip: stlačte Ctrl-V (Cmd-V na Mac) pre vloženie prílohy zo schránky.',
|
||||
'Tip: stlačte Ctrl-V (Cmd-V na Mac) pre vloženie prílohy zo schránky',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt neexistuje',
|
||||
removeManager_title: 'Odstrániť správcu',
|
||||
|
||||
@@ -119,7 +119,7 @@ export default {
|
||||
phone: 'Telefon',
|
||||
preferences: 'Preferenser',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'Tips: tryck på Ctrl-V (Cmd-V på Mac) för att lägga till en bilaga från urklipp.',
|
||||
'Tips: tryck på Ctrl-V (Cmd-V på Mac) för att lägga till en bilaga från urklipp',
|
||||
project: 'Projekt',
|
||||
projectNotFound_title: 'Projekt Ej Funnet',
|
||||
removeManager_title: 'Ta Bort Projektledare',
|
||||
|
||||
@@ -112,7 +112,7 @@ export default {
|
||||
phone: 'Telefon',
|
||||
preferences: 'Afzalliklar',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
"Tip: Buferdan ilova qo'shish uchun Ctrl-V (Mac da Cmd-V) ni bosing.",
|
||||
"Tip: Buferdan ilova qo'shish uchun Ctrl-V (Mac da Cmd-V) ni bosing",
|
||||
project: 'Loyiha',
|
||||
projectNotFound_title: 'Loyiha Topilmadi',
|
||||
removeManager_title: "Boshqaruvchini O'chirish",
|
||||
|
||||
@@ -110,7 +110,7 @@ export default {
|
||||
phone: '电话',
|
||||
preferences: '偏好',
|
||||
pressPasteShortcutToAddAttachmentFromClipboard:
|
||||
'提示: 点击 Ctrl-V (Mac: Cmd-V) 从剪切板添加附件.',
|
||||
'提示: 点击 Ctrl-V (Mac: Cmd-V) 从剪切板添加附件',
|
||||
project: '项目',
|
||||
projectNotFound_title: '项目未找到',
|
||||
removeManager_title: '删除管理员',
|
||||
|
||||
@@ -175,6 +175,14 @@ export default class extends Model {
|
||||
return this.lists.orderBy('position');
|
||||
}
|
||||
|
||||
getMembershipModel(userId) {
|
||||
return this.memberships
|
||||
.filter({
|
||||
userId,
|
||||
})
|
||||
.first();
|
||||
}
|
||||
|
||||
hasMemberUser(userId) {
|
||||
return this.memberships
|
||||
.filter({
|
||||
|
||||
@@ -7,6 +7,8 @@ export default class extends Model {
|
||||
|
||||
static fields = {
|
||||
id: attr(),
|
||||
role: attr(),
|
||||
canComment: attr(),
|
||||
boardId: fk({
|
||||
to: 'Board',
|
||||
as: 'board',
|
||||
@@ -65,6 +67,15 @@ export default class extends Model {
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
case ActionTypes.BOARD_MEMBERSHIP_UPDATE:
|
||||
BoardMembership.withId(payload.id).update(payload.data);
|
||||
|
||||
break;
|
||||
case ActionTypes.BOARD_MEMBERSHIP_UPDATE__SUCCESS:
|
||||
case ActionTypes.BOARD_MEMBERSHIP_UPDATE_HANDLE:
|
||||
BoardMembership.upsert(payload.boardMembership);
|
||||
|
||||
break;
|
||||
case ActionTypes.BOARD_MEMBERSHIP_DELETE:
|
||||
BoardMembership.withId(payload.id).deleteWithRelated();
|
||||
|
||||
@@ -84,7 +84,7 @@ export function* handleBoardMembershipCreate(boardMembership) {
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = yield call(requests.fetchBoardByCurrentPathRequest);
|
||||
body = yield call(requests.fetchBoardByCurrentPath);
|
||||
} catch {} // eslint-disable-line no-empty
|
||||
|
||||
if (body && body.project && body.project.id === board2.projectId) {
|
||||
@@ -135,6 +135,24 @@ export function* handleBoardMembershipCreate(boardMembership) {
|
||||
);
|
||||
}
|
||||
|
||||
export function* updateBoardMembership(id, data) {
|
||||
yield put(actions.updateBoardMembership(id, data));
|
||||
|
||||
let boardMembership;
|
||||
try {
|
||||
({ item: boardMembership } = yield call(request, api.updateBoardMembership, id, data));
|
||||
} catch (error) {
|
||||
yield put(actions.updateBoardMembership.failure(id, error));
|
||||
return;
|
||||
}
|
||||
|
||||
yield put(actions.updateBoardMembership.success(boardMembership));
|
||||
}
|
||||
|
||||
export function* handleBoardMembershipUpdate(boardMembership) {
|
||||
yield put(actions.handleBoardMembershipUpdate(boardMembership));
|
||||
}
|
||||
|
||||
export function* deleteBoardMembership(id) {
|
||||
let boardMembership = yield select(selectors.selectBoardMembershipById, id);
|
||||
|
||||
@@ -184,6 +202,8 @@ export default {
|
||||
createBoardMembership,
|
||||
createMembershipInCurrentBoard,
|
||||
handleBoardMembershipCreate,
|
||||
updateBoardMembership,
|
||||
handleBoardMembershipUpdate,
|
||||
deleteBoardMembership,
|
||||
handleBoardMembershipDelete,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,12 @@ export default function* boardMembershipsWatchers() {
|
||||
takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE, ({ payload: { boardMembership } }) =>
|
||||
services.handleBoardMembershipCreate(boardMembership),
|
||||
),
|
||||
takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_UPDATE, ({ payload: { id, data } }) =>
|
||||
services.updateBoardMembership(id, data),
|
||||
),
|
||||
takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_UPDATE_HANDLE, ({ payload: { boardMembership } }) =>
|
||||
services.handleBoardMembershipUpdate(boardMembership),
|
||||
),
|
||||
takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_DELETE, ({ payload: { id } }) =>
|
||||
services.deleteBoardMembership(id),
|
||||
),
|
||||
|
||||
@@ -64,6 +64,10 @@ const createSocketEventsChannel = () =>
|
||||
emit(entryActions.handleBoardMembershipCreate(item));
|
||||
};
|
||||
|
||||
const handleBoardMembershipUpdate = ({ item }) => {
|
||||
emit(entryActions.handleBoardMembershipUpdate(item));
|
||||
};
|
||||
|
||||
const handleBoardMembershipDelete = ({ item }) => {
|
||||
emit(entryActions.handleBoardMembershipDelete(item));
|
||||
};
|
||||
@@ -183,6 +187,7 @@ const createSocketEventsChannel = () =>
|
||||
socket.on('boardDelete', handleBoardDelete);
|
||||
|
||||
socket.on('boardMembershipCreate', handleBoardMembershipCreate);
|
||||
socket.on('boardMembershipUpdate', handleBoardMembershipUpdate);
|
||||
socket.on('boardMembershipDelete', handleBoardMembershipDelete);
|
||||
|
||||
socket.on('listCreate', handleListCreate);
|
||||
@@ -238,6 +243,7 @@ const createSocketEventsChannel = () =>
|
||||
socket.off('boardDelete', handleBoardDelete);
|
||||
|
||||
socket.off('boardMembershipCreate', handleBoardMembershipCreate);
|
||||
socket.off('boardMembershipUpdate', handleBoardMembershipUpdate);
|
||||
socket.off('boardMembershipDelete', handleBoardMembershipDelete);
|
||||
|
||||
socket.off('listCreate', handleListCreate);
|
||||
|
||||
@@ -147,22 +147,28 @@ export const selectFilterLabelsForCurrentBoard = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
export const selectIsCurrentUserMemberForCurrentBoard = createSelector(
|
||||
export const selectCurrentUserMembershipForCurrentBoard = createSelector(
|
||||
orm,
|
||||
(state) => selectPath(state).boardId,
|
||||
(state) => selectCurrentUserId(state),
|
||||
({ Board }, id, currentUserId) => {
|
||||
if (!id) {
|
||||
return false;
|
||||
return id;
|
||||
}
|
||||
|
||||
const boardModel = Board.withId(id);
|
||||
|
||||
if (!boardModel) {
|
||||
return false;
|
||||
return boardModel;
|
||||
}
|
||||
|
||||
return boardModel.hasMemberUser(currentUserId);
|
||||
const boardMembershipModel = boardModel.getMembershipModel(currentUserId);
|
||||
|
||||
if (!boardMembershipModel) {
|
||||
return boardMembershipModel;
|
||||
}
|
||||
|
||||
return boardMembershipModel.ref;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -175,5 +181,5 @@ export default {
|
||||
selectListIdsForCurrentBoard,
|
||||
selectFilterUsersForCurrentBoard,
|
||||
selectFilterLabelsForCurrentBoard,
|
||||
selectIsCurrentUserMemberForCurrentBoard,
|
||||
selectCurrentUserMembershipForCurrentBoard,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
const PASSWORD_REGEX = /^(?=.*[A-Za-z])(?=.*\d).+$/;
|
||||
const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/;
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const isPassword = (string) => {
|
||||
return string.length >= 3 && PASSWORD_REGEX.test(string);
|
||||
};
|
||||
|
||||
export const isUsername = (string) => {
|
||||
return string.length >= 3 && string.length <= 16 && USERNAME_REGEX.test(string);
|
||||
};
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "planka",
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "planka",
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.3",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planka",
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.3",
|
||||
"private": true,
|
||||
"homepage": "https://plankanban.github.io/planka",
|
||||
"repository": {
|
||||
|
||||
@@ -143,3 +143,5 @@ private/attachments/*
|
||||
|
||||
views/*
|
||||
!views/.gitkeep
|
||||
|
||||
logs
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const validator = require('validator');
|
||||
|
||||
const { getRemoteAddress } = require('../../../utils/remoteAddress');
|
||||
|
||||
const Errors = {
|
||||
INVALID_EMAIL_OR_USERNAME: {
|
||||
invalidEmailOrUsername: 'Invalid email or username',
|
||||
@@ -41,10 +43,16 @@ module.exports = {
|
||||
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
|
||||
|
||||
if (!user) {
|
||||
sails.log.warn(
|
||||
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${getRemoteAddress(
|
||||
this.req,
|
||||
)})`,
|
||||
);
|
||||
throw Errors.INVALID_EMAIL_OR_USERNAME;
|
||||
}
|
||||
|
||||
if (!bcrypt.compareSync(inputs.password, user.password)) {
|
||||
sails.log.warn(`Invalid password! (IP: ${getRemoteAddress(this.req)})`);
|
||||
throw Errors.INVALID_PASSWORD;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
const util = require('util');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
CARD_NOT_FOUND: {
|
||||
cardNotFound: 'Card not found',
|
||||
},
|
||||
NO_FILE_WAS_UPLOADED: {
|
||||
noFileWasUploaded: 'No file was uploaded',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
@@ -18,9 +27,15 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
cardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
noFileWasUploaded: {
|
||||
responseType: 'unprocessableEntity',
|
||||
},
|
||||
uploadError: {
|
||||
responseType: 'unprocessableEntity',
|
||||
},
|
||||
@@ -33,39 +48,50 @@ module.exports = {
|
||||
.getProjectPath(inputs.cardId)
|
||||
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: card.boardId,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.CARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
this.req
|
||||
.file('file')
|
||||
.upload(sails.helpers.utils.createAttachmentReceiver(), async (error, files) => {
|
||||
if (error) {
|
||||
return exits.uploadError(error.message);
|
||||
}
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
return exits.uploadError('No file was uploaded');
|
||||
}
|
||||
const upload = util.promisify((options, callback) =>
|
||||
this.req.file('file').upload(options, (error, files) => callback(error, files)),
|
||||
);
|
||||
|
||||
const file = files[0];
|
||||
|
||||
const attachment = await sails.helpers.attachments.createOne(
|
||||
{
|
||||
...file.extra,
|
||||
filename: file.filename,
|
||||
},
|
||||
currentUser,
|
||||
card,
|
||||
inputs.requestId,
|
||||
this.req,
|
||||
);
|
||||
|
||||
return exits.success({
|
||||
item: attachment.toJSON(),
|
||||
});
|
||||
let files;
|
||||
try {
|
||||
files = await upload({
|
||||
saveAs: uuid(),
|
||||
maxBytes: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return exits.uploadError(error.message); // TODO: add error
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
throw Errors.NO_FILE_WAS_UPLOADED;
|
||||
}
|
||||
|
||||
const file = _.last(files);
|
||||
const fileData = await sails.helpers.attachments.processUploadedFile(file);
|
||||
|
||||
const attachment = await sails.helpers.attachments.createOne(
|
||||
fileData,
|
||||
currentUser,
|
||||
card,
|
||||
inputs.requestId,
|
||||
this.req,
|
||||
);
|
||||
|
||||
return exits.success({
|
||||
item: attachment,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
ATTACHMENT_NOT_FOUND: {
|
||||
attachmentNotFound: 'Attachment not found',
|
||||
},
|
||||
@@ -14,6 +17,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
attachmentNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -29,12 +35,19 @@ module.exports = {
|
||||
let { attachment } = path;
|
||||
const { card, board } = path;
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: board.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
attachment = await sails.helpers.attachments.deleteOne(attachment, board, card, this.req);
|
||||
|
||||
if (!attachment) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
ATTACHMENT_NOT_FOUND: {
|
||||
attachmentNotFound: 'Attachment not found',
|
||||
},
|
||||
@@ -18,6 +21,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
attachmentNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -33,12 +39,19 @@ module.exports = {
|
||||
let { attachment } = path;
|
||||
const { board } = path;
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: board.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const values = _.pick(inputs, ['name']);
|
||||
attachment = await sails.helpers.attachments.updateOne(attachment, values, board, this.req);
|
||||
|
||||
|
||||
@@ -22,6 +22,14 @@ module.exports = {
|
||||
regex: /^[0-9]+$/,
|
||||
required: true,
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
isIn: Object.values(BoardMembership.Roles),
|
||||
required: true,
|
||||
},
|
||||
canComment: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
|
||||
exits: {
|
||||
@@ -58,8 +66,10 @@ module.exports = {
|
||||
throw Error.USER_NOT_FOUND;
|
||||
}
|
||||
|
||||
const values = _.pick(inputs, ['role', 'canComment']);
|
||||
|
||||
const boardMembership = await sails.helpers.boardMemberships
|
||||
.createOne(user, board, this.req)
|
||||
.createOne(values, user, board, this.req)
|
||||
.intercept('userAlreadyBoardMember', () => Errors.USER_ALREADY_BOARD_MEMBER);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
const Errors = {
|
||||
BOARD_MEMBERSHIP_NOT_FOUND: {
|
||||
boardMembershipNotFound: 'Board membership not found',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
regex: /^[0-9]+$/,
|
||||
required: true,
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
isIn: Object.values(BoardMembership.Roles),
|
||||
},
|
||||
canComment: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
|
||||
exits: {
|
||||
boardMembershipNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const { currentUser } = this.req;
|
||||
|
||||
const path = await sails.helpers.boardMemberships
|
||||
.getProjectPath(inputs.id)
|
||||
.intercept('pathNotFound', () => Errors.BOARD_MEMBERSHIP_NOT_FOUND);
|
||||
|
||||
let { boardMembership } = path;
|
||||
const { project } = path;
|
||||
|
||||
const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id);
|
||||
|
||||
if (!isProjectManager) {
|
||||
throw Errors.BOARD_MEMBERSHIP_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
const values = _.pick(inputs, ['role', 'canComment']);
|
||||
|
||||
boardMembership = await sails.helpers.boardMemberships.updateOne(
|
||||
boardMembership,
|
||||
values,
|
||||
this.req,
|
||||
);
|
||||
|
||||
return {
|
||||
item: boardMembership,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
CARD_NOT_FOUND: {
|
||||
cardNotFound: 'Card not found',
|
||||
},
|
||||
@@ -25,6 +28,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
cardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -43,12 +49,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.cardId)
|
||||
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: card.boardId,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.CARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const label = await Label.findOne({
|
||||
id: inputs.labelId,
|
||||
boardId: card.boardId,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
CARD_NOT_FOUND: {
|
||||
cardNotFound: 'Card not found',
|
||||
},
|
||||
@@ -22,6 +25,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
cardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -37,12 +43,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.cardId)
|
||||
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: board.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.CARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
let cardLabel = await CardLabel.findOne({
|
||||
cardId: inputs.cardId,
|
||||
labelId: inputs.labelId,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
CARD_NOT_FOUND: {
|
||||
cardNotFound: 'Card not found',
|
||||
},
|
||||
@@ -25,6 +28,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
cardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -43,13 +49,20 @@ module.exports = {
|
||||
.getProjectPath(inputs.cardId)
|
||||
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
|
||||
|
||||
let isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: card.boardId,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.CARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
isBoardMember = await sails.helpers.users.isBoardMember(inputs.userId, card.boardId);
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(inputs.userId, card.boardId);
|
||||
|
||||
if (!isBoardMember) {
|
||||
throw Errors.USER_NOT_FOUND;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
CARD_NOT_FOUND: {
|
||||
cardNotFound: 'Card not found',
|
||||
},
|
||||
@@ -22,6 +25,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
cardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -37,12 +43,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.cardId)
|
||||
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: board.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.CARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
let cardMembership = await CardMembership.findOne({
|
||||
cardId: inputs.cardId,
|
||||
userId: inputs.userId,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const moment = require('moment');
|
||||
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
BOARD_NOT_FOUND: {
|
||||
boardNotFound: 'Board not found',
|
||||
},
|
||||
@@ -67,6 +70,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
boardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -88,12 +94,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.boardId)
|
||||
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: board.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.BOARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
let list;
|
||||
if (!_.isUndefined(inputs.listId)) {
|
||||
list = await List.findOne({
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
CARD_NOT_FOUND: {
|
||||
cardNotFound: 'Card not found',
|
||||
},
|
||||
@@ -14,6 +17,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
cardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -26,12 +32,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.id)
|
||||
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: card.boardId,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.CARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
card = await sails.helpers.cards.deleteOne(card, this.req);
|
||||
|
||||
if (!card) {
|
||||
|
||||
@@ -23,7 +23,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs, exits) {
|
||||
async fn(inputs) {
|
||||
const { currentUser } = this.req;
|
||||
|
||||
const { board } = await sails.helpers.boards
|
||||
@@ -61,7 +61,7 @@ module.exports = {
|
||||
const tasks = await sails.helpers.cards.getTasks(cardIds);
|
||||
const attachments = await sails.helpers.cards.getAttachments(cardIds);
|
||||
|
||||
return exits.success({
|
||||
return {
|
||||
items: cards,
|
||||
included: {
|
||||
cardMemberships,
|
||||
@@ -69,6 +69,6 @@ module.exports = {
|
||||
tasks,
|
||||
attachments,
|
||||
},
|
||||
});
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const moment = require('moment');
|
||||
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
CARD_NOT_FOUND: {
|
||||
cardNotFound: 'Card not found',
|
||||
},
|
||||
@@ -83,6 +86,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
cardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -110,23 +116,37 @@ module.exports = {
|
||||
let { card } = path;
|
||||
const { list, board } = path;
|
||||
|
||||
let isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id);
|
||||
let boardMembership = await BoardMembership.findOne({
|
||||
boardId: board.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.CARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
let nextBoard;
|
||||
if (!_.isUndefined(inputs.boardId)) {
|
||||
({ board: nextBoard } = await sails.helpers.boards
|
||||
.getProjectPath(inputs.boardId)
|
||||
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND));
|
||||
|
||||
isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, nextBoard.id);
|
||||
boardMembership = await BoardMembership.findOne({
|
||||
boardId: nextBoard.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.BOARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
}
|
||||
|
||||
let nextList;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
CARD_NOT_FOUND: {
|
||||
cardNotFound: 'Card not found',
|
||||
},
|
||||
@@ -18,6 +21,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
cardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -30,12 +36,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.cardId)
|
||||
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: card.boardId,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.CARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR && !boardMembership.canComment) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const values = {
|
||||
type: Action.Types.COMMENT_CARD,
|
||||
data: _.pick(inputs, ['text']),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
COMMENT_ACTION_NOT_FOUND: {
|
||||
commentActionNotFound: 'Comment action not found',
|
||||
},
|
||||
@@ -14,6 +17,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
commentActionNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -39,11 +45,18 @@ module.exports = {
|
||||
throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: board.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR && !boardMembership.canComment) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
}
|
||||
|
||||
action = await sails.helpers.actions.deleteOne(action, board, this.req);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
COMMENT_ACTION_NOT_FOUND: {
|
||||
commentActionNotFound: 'Comment action not found',
|
||||
},
|
||||
@@ -18,6 +21,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
commentActionNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -43,11 +49,18 @@ module.exports = {
|
||||
throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: board.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR && !boardMembership.canComment) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
}
|
||||
|
||||
const values = {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
BOARD_NOT_FOUND: {
|
||||
boardNotFound: 'Board not found',
|
||||
},
|
||||
@@ -24,6 +27,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
boardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -36,12 +42,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.boardId)
|
||||
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: board.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.BOARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const values = _.pick(inputs, ['name', 'color']);
|
||||
const label = await sails.helpers.labels.createOne(values, board, this.req);
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
LABEL_NOT_FOUND: {
|
||||
labelNotFound: 'Label not found',
|
||||
},
|
||||
@@ -14,6 +17,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
labelNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -26,12 +32,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.id)
|
||||
.intercept('pathNotFound', () => Errors.LABEL_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, label.boardId);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: label.boardId,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.LABEL_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
label = await sails.helpers.labels.deleteOne(label, this.req);
|
||||
|
||||
if (!label) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
LABEL_NOT_FOUND: {
|
||||
labelNotFound: 'Label not found',
|
||||
},
|
||||
@@ -24,6 +27,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
labelNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -36,12 +42,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.id)
|
||||
.intercept('pathNotFound', () => Errors.LABEL_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, label.boardId);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: label.boardId,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.LABEL_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const values = _.pick(inputs, ['name', 'color']);
|
||||
label = await sails.helpers.labels.updateOne(label, values, this.req);
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
BOARD_NOT_FOUND: {
|
||||
boardNotFound: 'Board not found',
|
||||
},
|
||||
@@ -22,6 +25,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
boardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -34,12 +40,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.boardId)
|
||||
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: board.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.BOARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const values = _.pick(inputs, ['position', 'name']);
|
||||
const list = await sails.helpers.lists.createOne(values, board, this.req);
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
LIST_NOT_FOUND: {
|
||||
listNotFound: 'List not found',
|
||||
},
|
||||
@@ -14,6 +17,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
listNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -26,12 +32,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.id)
|
||||
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, list.boardId);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: list.boardId,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.LIST_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
list = await sails.helpers.lists.deleteOne(list, this.req);
|
||||
|
||||
if (!list) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
LIST_NOT_FOUND: {
|
||||
listNotFound: 'List not found',
|
||||
},
|
||||
@@ -21,6 +24,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
listNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -33,12 +39,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.id)
|
||||
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, list.boardId);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: list.boardId,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.LIST_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const values = _.pick(inputs, ['position', 'name']);
|
||||
list = await sails.helpers.lists.updateOne(list, values, this.req);
|
||||
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
const util = require('util');
|
||||
const rimraf = require('rimraf');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
const Errors = {
|
||||
PROJECT_NOT_FOUND: {
|
||||
projectNotFound: 'Project not found',
|
||||
},
|
||||
NO_FILE_WAS_UPLOADED: {
|
||||
noFileWasUploaded: 'No file was uploaded',
|
||||
},
|
||||
FILE_IS_NOT_IMAGE: {
|
||||
fileIsNotImage: 'File is not image',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
@@ -17,6 +27,12 @@ module.exports = {
|
||||
projectNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
noFileWasUploaded: {
|
||||
responseType: 'unprocessableEntity',
|
||||
},
|
||||
fileIsNotImage: {
|
||||
responseType: 'unprocessableEntity',
|
||||
},
|
||||
uploadError: {
|
||||
responseType: 'unprocessableEntity',
|
||||
},
|
||||
@@ -37,32 +53,52 @@ module.exports = {
|
||||
throw Errors.PROJECT_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
this.req
|
||||
.file('file')
|
||||
.upload(sails.helpers.utils.createProjectBackgroundImageReceiver(), async (error, files) => {
|
||||
if (error) {
|
||||
return exits.uploadError(error.message);
|
||||
}
|
||||
const upload = util.promisify((options, callback) =>
|
||||
this.req.file('file').upload(options, (error, files) => callback(error, files)),
|
||||
);
|
||||
|
||||
if (files.length === 0) {
|
||||
return exits.uploadError('No file was uploaded');
|
||||
}
|
||||
|
||||
project = await sails.helpers.projects.updateOne(
|
||||
project,
|
||||
{
|
||||
backgroundImageDirname: files[0].extra.dirname,
|
||||
},
|
||||
this.req,
|
||||
);
|
||||
|
||||
if (!project) {
|
||||
throw Errors.PROJECT_NOT_FOUND;
|
||||
}
|
||||
|
||||
return exits.success({
|
||||
item: project.toJSON(),
|
||||
});
|
||||
let files;
|
||||
try {
|
||||
files = await upload({
|
||||
saveAs: uuid(),
|
||||
maxBytes: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return exits.uploadError(error.message); // TODO: add error
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
throw Errors.NO_FILE_WAS_UPLOADED;
|
||||
}
|
||||
|
||||
const file = _.last(files);
|
||||
|
||||
const fileData = await sails.helpers.projects
|
||||
.processUploadedBackgroundImageFile(file)
|
||||
.intercept('fileIsNotImage', () => {
|
||||
try {
|
||||
rimraf.sync(file.fd);
|
||||
} catch (error) {
|
||||
console.warn(error.stack); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
return Errors.FILE_IS_NOT_IMAGE;
|
||||
});
|
||||
|
||||
project = await sails.helpers.projects.updateOne(
|
||||
project,
|
||||
{
|
||||
backgroundImageDirname: fileData.dirname,
|
||||
},
|
||||
this.req,
|
||||
);
|
||||
|
||||
if (!project) {
|
||||
throw Errors.PROJECT_NOT_FOUND;
|
||||
}
|
||||
|
||||
return exits.success({
|
||||
item: project,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
CARD_NOT_FOUND: {
|
||||
cardNotFound: 'Card not found',
|
||||
},
|
||||
@@ -25,6 +28,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
cardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -37,12 +43,19 @@ module.exports = {
|
||||
.getProjectPath(inputs.cardId)
|
||||
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, card.boardId);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: card.boardId,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.CARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const values = _.pick(inputs, ['position', 'name', 'isCompleted']);
|
||||
const task = await sails.helpers.tasks.createOne(values, card, this.req);
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
TASK_NOT_FOUND: {
|
||||
taskNotFound: 'Task not found',
|
||||
},
|
||||
@@ -14,6 +17,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
taskNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -29,12 +35,19 @@ module.exports = {
|
||||
let { task } = path;
|
||||
const { board } = path;
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: board.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.TASK_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
task = await sails.helpers.tasks.deleteOne(task, board, this.req);
|
||||
|
||||
if (!task) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
TASK_NOT_FOUND: {
|
||||
taskNotFound: 'Task not found',
|
||||
},
|
||||
@@ -24,6 +27,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
taskNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
@@ -39,12 +45,19 @@ module.exports = {
|
||||
let { task } = path;
|
||||
const { board } = path;
|
||||
|
||||
const isBoardMember = await sails.helpers.users.isBoardMember(currentUser.id, board.id);
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: board.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!isBoardMember) {
|
||||
if (!boardMembership) {
|
||||
throw Errors.TASK_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const values = _.pick(inputs, ['position', 'name', 'isCompleted']);
|
||||
task = await sails.helpers.tasks.updateOne(task, values, board, this.req);
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ module.exports = {
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
minLength: 6,
|
||||
regex: /^(?=.*[A-Za-z])(?=.*\d).+$/,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
const util = require('util');
|
||||
const rimraf = require('rimraf');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
const Errors = {
|
||||
USER_NOT_FOUND: {
|
||||
userNotFound: 'User not found',
|
||||
},
|
||||
NO_FILE_WAS_UPLOADED: {
|
||||
noFileWasUploaded: 'No file was uploaded',
|
||||
},
|
||||
FILE_IS_NOT_IMAGE: {
|
||||
fileIsNotImage: 'File is not image',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
@@ -17,6 +27,12 @@ module.exports = {
|
||||
userNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
noFileWasUploaded: {
|
||||
responseType: 'unprocessableEntity',
|
||||
},
|
||||
fileIsNotImage: {
|
||||
responseType: 'unprocessableEntity',
|
||||
},
|
||||
uploadError: {
|
||||
responseType: 'unprocessableEntity',
|
||||
},
|
||||
@@ -38,33 +54,53 @@ module.exports = {
|
||||
user = currentUser;
|
||||
}
|
||||
|
||||
this.req
|
||||
.file('file')
|
||||
.upload(sails.helpers.utils.createUserAvatarReceiver(), async (error, files) => {
|
||||
if (error) {
|
||||
return exits.uploadError(error.message);
|
||||
}
|
||||
const upload = util.promisify((options, callback) =>
|
||||
this.req.file('file').upload(options, (error, files) => callback(error, files)),
|
||||
);
|
||||
|
||||
if (files.length === 0) {
|
||||
return exits.uploadError('No file was uploaded');
|
||||
}
|
||||
|
||||
user = await sails.helpers.users.updateOne(
|
||||
user,
|
||||
{
|
||||
avatarDirname: files[0].extra.dirname,
|
||||
},
|
||||
currentUser,
|
||||
this.req,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
throw Errors.USER_NOT_FOUND;
|
||||
}
|
||||
|
||||
return exits.success({
|
||||
item: user.toJSON(),
|
||||
});
|
||||
let files;
|
||||
try {
|
||||
files = await upload({
|
||||
saveAs: uuid(),
|
||||
maxBytes: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return exits.uploadError(error.message); // TODO: add error
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
throw Errors.NO_FILE_WAS_UPLOADED;
|
||||
}
|
||||
|
||||
const file = _.last(files);
|
||||
|
||||
const fileData = await sails.helpers.users
|
||||
.processUploadedAvatarFile(file)
|
||||
.intercept('fileIsNotImage', () => {
|
||||
try {
|
||||
rimraf.sync(file.fd);
|
||||
} catch (error) {
|
||||
console.warn(error.stack); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
return Errors.FILE_IS_NOT_IMAGE;
|
||||
});
|
||||
|
||||
user = await sails.helpers.users.updateOne(
|
||||
user,
|
||||
{
|
||||
avatarDirname: fileData.dirname,
|
||||
},
|
||||
currentUser,
|
||||
this.req,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
throw Errors.USER_NOT_FOUND;
|
||||
}
|
||||
|
||||
return exits.success({
|
||||
item: user,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@ module.exports = {
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
minLength: 6,
|
||||
regex: /^(?=.*[A-Za-z])(?=.*\d).+$/,
|
||||
required: true,
|
||||
},
|
||||
currentPassword: {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const rimraf = require('rimraf');
|
||||
const moveFile = require('move-file');
|
||||
const filenamify = require('filenamify');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const sharp = require('sharp');
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
file: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const dirname = uuid();
|
||||
|
||||
// FIXME: https://github.com/sindresorhus/filenamify/issues/13
|
||||
const filename = filenamify(inputs.file.filename);
|
||||
|
||||
const rootPath = path.join(sails.config.custom.attachmentsPath, dirname);
|
||||
const filePath = path.join(rootPath, filename);
|
||||
|
||||
fs.mkdirSync(rootPath);
|
||||
await moveFile(inputs.file.fd, filePath);
|
||||
|
||||
const image = sharp(filePath);
|
||||
let metadata;
|
||||
|
||||
try {
|
||||
metadata = await image.metadata();
|
||||
} catch (error) {} // eslint-disable-line no-empty
|
||||
|
||||
const fileData = {
|
||||
dirname,
|
||||
filename,
|
||||
image: null,
|
||||
name: inputs.file.filename,
|
||||
};
|
||||
|
||||
if (metadata && !['svg', 'pdf'].includes(metadata.format)) {
|
||||
const thumbnailsPath = path.join(rootPath, 'thumbnails');
|
||||
fs.mkdirSync(thumbnailsPath);
|
||||
|
||||
try {
|
||||
await image
|
||||
.resize(
|
||||
metadata.height > metadata.width
|
||||
? {
|
||||
width: 256,
|
||||
height: 320,
|
||||
}
|
||||
: {
|
||||
width: 256,
|
||||
},
|
||||
)
|
||||
.jpeg({
|
||||
quality: 100,
|
||||
chromaSubsampling: '4:4:4',
|
||||
})
|
||||
.toFile(path.join(thumbnailsPath, 'cover-256.jpg'));
|
||||
|
||||
fileData.image = _.pick(metadata, ['width', 'height']);
|
||||
} catch (error1) {
|
||||
try {
|
||||
rimraf.sync(thumbnailsPath);
|
||||
} catch (error2) {
|
||||
console.warn(error2.stack); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fileData;
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,9 @@
|
||||
module.exports = {
|
||||
inputs: {
|
||||
values: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
},
|
||||
user: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
@@ -18,7 +22,16 @@ module.exports = {
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
if (inputs.values.role === BoardMembership.Roles.EDITOR) {
|
||||
delete inputs.values.canComment; // eslint-disable-line no-param-reassign
|
||||
} else if (inputs.values.role === BoardMembership.Roles.VIEWER) {
|
||||
if (_.isNil(inputs.values.canComment)) {
|
||||
inputs.values.canComment = false; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
}
|
||||
|
||||
const boardMembership = await BoardMembership.create({
|
||||
...inputs.values,
|
||||
boardId: inputs.board.id,
|
||||
userId: inputs.user.id,
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user