Compare commits

...
20 Commits
Author SHA1 Message Date
Maksim Eltyshev fd8f40f1b0 chore: Update version 2022-08-26 16:05:47 +02:00
Maksim Eltyshev 5b64d465e2 fix: Use move-file instead of rename 2022-08-26 16:02:47 +02:00
Maksim Eltyshev 212fb8ac8c fix: Add missing border radius 2022-08-26 02:47:43 +02:00
Maksim Eltyshev cece2254d7 fix: Change mechanics of file uploading 2022-08-26 02:45:27 +02:00
Maksim Eltyshev c940805f14 chore: Fix version 2022-08-24 15:44:01 +02:00
Maksim Eltyshev 052ab7f653 chore: Update version 2022-08-24 15:37:27 +02:00
Maksim Eltyshev 5a38805588 feat: Add Italian translation. Thanks to @belf88 2022-08-24 15:27:12 +02:00
Maksim Eltyshev 2dbbead2e9 fix: Add missing border radiuses 2022-08-23 21:19:43 +02:00
Maksim Eltyshev 039eee0aac ref: Little refactoring 2022-08-23 21:06:50 +02:00
Maksim Eltyshev 07867fc0b2 fix: Use custom logger only for production 2022-08-23 20:45:42 +02:00
Maksim Eltyshev 55d07b4560 meta: Fixes in readme 2022-08-23 01:14:11 +02:00
Maksim Eltyshev 289d2bf5ae meta: Fix ignore files for logs 2022-08-23 00:51:33 +02:00
Steven Correia 6429e22d59 feat: Modify logger to log to file that supports fail2ban (#284) 2022-08-23 03:42:56 +05:00
Maksim Eltyshev 21ef850913 fix: Fix missed function name 2022-08-22 16:27:14 +02:00
Maksim Eltyshev e4769220da chore: Update version 2022-08-19 14:04:26 +02:00
Maksim Eltyshev 378e02c0a4 Merge branch 'master' of https://github.com/plankanban/planka 2022-08-19 14:01:24 +02:00
Maksim Eltyshev 51fa7df69c feat: Permissions for board members
Closes #262
2022-08-19 14:00:40 +02:00
Maksim Eltyshev 68806b9080 meta: Add sponsor button 2022-08-18 16:51:59 +05:00
Maksim Eltyshev d80a538857 fix: Little improvements in users modal 2022-08-17 02:00:24 +02:00
Maksim Eltyshev 78669da01d feat: Add search by users, members and labels 2022-08-17 00:53:08 +02:00
104 changed files with 2531 additions and 570 deletions
+1
View File
@@ -7,4 +7,5 @@
server/public/* server/public/*
server/views/* server/views/*
server/logs
client/build client/build
+13
View File
@@ -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']
+73
View File
@@ -44,6 +44,79 @@ docker-compose up -d
Demo user: demo@demo.demo demo 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 ## Development
Clone the repository and install dependencies: Clone the repository and install dependencies:
+32
View File
@@ -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) => ({ const deleteBoardMembership = (id) => ({
type: ActionTypes.BOARD_MEMBERSHIP_DELETE, type: ActionTypes.BOARD_MEMBERSHIP_DELETE,
payload: { payload: {
@@ -101,6 +131,8 @@ const handleBoardMembershipDelete = (boardMembership) => ({
export default { export default {
createBoardMembership, createBoardMembership,
handleBoardMembershipCreate, handleBoardMembershipCreate,
updateBoardMembership,
handleBoardMembershipUpdate,
deleteBoardMembership, deleteBoardMembership,
handleBoardMembershipDelete, handleBoardMembershipDelete,
}; };
+4
View File
@@ -5,10 +5,14 @@ import socket from './socket';
const createBoardMembership = (boardId, data, headers) => const createBoardMembership = (boardId, data, headers) =>
socket.post(`/boards/${boardId}/memberships`, 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) => const deleteBoardMembership = (id, headers) =>
socket.delete(`/board-memberships/${id}`, undefined, headers); socket.delete(`/board-memberships/${id}`, undefined, headers);
export default { export default {
createBoardMembership, createBoardMembership,
updateBoardMembership,
deleteBoardMembership, deleteBoardMembership,
}; };
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import Filters from './Filters'; import Filters from './Filters';
import Memberships from '../Memberships'; import Memberships from '../Memberships';
import BoardMembershipPermissionsSelectStep from '../BoardMembershipPermissionsSelectStep';
import styles from './BoardActions.module.scss'; import styles from './BoardActions.module.scss';
@@ -15,6 +16,7 @@ const BoardActions = React.memo(
allUsers, allUsers,
canEditMemberships, canEditMemberships,
onMembershipCreate, onMembershipCreate,
onMembershipUpdate,
onMembershipDelete, onMembershipDelete,
onUserToFilterAdd, onUserToFilterAdd,
onUserFromFilterRemove, onUserFromFilterRemove,
@@ -30,8 +32,10 @@ const BoardActions = React.memo(
<Memberships <Memberships
items={memberships} items={memberships}
allUsers={allUsers} allUsers={allUsers}
permissionsSelectStep={BoardMembershipPermissionsSelectStep}
canEdit={canEditMemberships} canEdit={canEditMemberships}
onCreate={onMembershipCreate} onCreate={onMembershipCreate}
onUpdate={onMembershipUpdate}
onDelete={onMembershipDelete} onDelete={onMembershipDelete}
/> />
</div> </div>
@@ -65,6 +69,7 @@ BoardActions.propTypes = {
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
canEditMemberships: PropTypes.bool.isRequired, canEditMemberships: PropTypes.bool.isRequired,
onMembershipCreate: PropTypes.func.isRequired, onMembershipCreate: PropTypes.func.isRequired,
onMembershipUpdate: PropTypes.func.isRequired,
onMembershipDelete: PropTypes.func.isRequired, onMembershipDelete: PropTypes.func.isRequired,
onUserToFilterAdd: PropTypes.func.isRequired, onUserToFilterAdd: PropTypes.func.isRequired,
onUserFromFilterRemove: PropTypes.func.isRequired, onUserFromFilterRemove: PropTypes.func.isRequired,
@@ -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;
@@ -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 PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react'; 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 Item from './Item';
import styles from './BoardMembershipsStep.module.scss'; import styles from './BoardMembershipsStep.module.scss';
@@ -11,6 +12,21 @@ import styles from './BoardMembershipsStep.module.scss';
const BoardMembershipsStep = React.memo( const BoardMembershipsStep = React.memo(
({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => { ({ items, currentUserIds, title, onUserSelect, onUserDeselect, onBack }) => {
const [t] = useTranslation(); 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( const handleUserSelect = useCallback(
(id) => { (id) => {
@@ -26,22 +42,40 @@ const BoardMembershipsStep = React.memo(
[onUserDeselect], [onUserDeselect],
); );
useEffect(() => {
searchField.current.focus();
}, []);
return ( return (
<> <>
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header> <Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content> <Popup.Content>
<Menu secondary vertical className={styles.menu}> <Input
{items.map((item) => ( fluid
<Item ref={searchField}
key={item.id} value={search}
isPersisted={item.isPersisted} placeholder={t('common.searchMembers')}
isActive={currentUserIds.includes(item.user.id)} icon="search"
user={item.user} onChange={handleSearchChange}
onUserSelect={() => handleUserSelect(item.user.id)} />
onUserDeselect={() => handleUserDeselect(item.user.id)} {filteredItems.length > 0 && (
/> <Menu secondary vertical className={styles.menu}>
))} {filteredItems.map((item) => (
</Menu> <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> </Popup.Content>
</> </>
); );
@@ -1,6 +1,22 @@
:global(#app) { :global(#app) {
.menu { .menu {
margin: -7px auto -5px; margin: 8px auto 0;
max-height: 320px;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
width: 100%; width: 100%;
&::-webkit-scrollbar {
width: 5px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
}
} }
} }
@@ -2,6 +2,7 @@
.menuItem { .menuItem {
display: block; display: block;
margin: 0; margin: 0;
overflow: hidden;
padding: 4px; padding: 4px;
} }
@@ -13,7 +13,7 @@ import styles from './Attachments.module.scss';
const INITIALLY_VISIBLE = 4; const INITIALLY_VISIBLE = 4;
const Attachments = React.memo( const Attachments = React.memo(
({ items, onUpdate, onDelete, onCoverUpdate, onGalleryOpen, onGalleryClose }) => { ({ items, canEdit, onUpdate, onDelete, onCoverUpdate, onGalleryOpen, onGalleryClose }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [isAllVisible, toggleAllVisible] = useToggle(); const [isAllVisible, toggleAllVisible] = useToggle();
@@ -99,6 +99,7 @@ const Attachments = React.memo(
createdAt={item.createdAt} createdAt={item.createdAt}
isCover={item.isCover} isCover={item.isCover}
isPersisted={item.isPersisted} isPersisted={item.isPersisted}
canEdit={canEdit}
onClick={item.image || isPdf ? open : undefined} onClick={item.image || isPdf ? open : undefined}
onCoverSelect={() => handleCoverSelect(item.id)} onCoverSelect={() => handleCoverSelect(item.id)}
onCoverDeselect={handleCoverDeselect} onCoverDeselect={handleCoverDeselect}
@@ -151,6 +152,7 @@ const Attachments = React.memo(
Attachments.propTypes = { Attachments.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onCoverUpdate: PropTypes.func.isRequired, onCoverUpdate: PropTypes.func.isRequired,
@@ -17,6 +17,7 @@ const Item = React.forwardRef(
createdAt, createdAt,
isCover, isCover,
isPersisted, isPersisted,
canEdit,
onCoverSelect, onCoverSelect,
onCoverDeselect, onCoverDeselect,
onClick, onClick,
@@ -96,7 +97,7 @@ const Item = React.forwardRef(
value: createdAt, value: createdAt,
})} })}
</span> </span>
{coverUrl && ( {coverUrl && canEdit && (
<span className={styles.options}> <span className={styles.options}>
<button type="button" className={styles.option} onClick={handleToggleCoverClick}> <button type="button" className={styles.option} onClick={handleToggleCoverClick}>
<Icon <Icon
@@ -118,17 +119,19 @@ const Item = React.forwardRef(
</span> </span>
)} )}
</div> </div>
<EditPopup {canEdit && (
defaultData={{ <EditPopup
name, defaultData={{
}} name,
onUpdate={onUpdate} }}
onDelete={onDelete} onUpdate={onUpdate}
> onDelete={onDelete}
<Button className={classNames(styles.button, styles.target)}> >
<Icon fitted name="pencil" size="small" /> <Button className={classNames(styles.button, styles.target)}>
</Button> <Icon fitted name="pencil" size="small" />
</EditPopup> </Button>
</EditPopup>
)}
</div> </div>
); );
}, },
@@ -141,6 +144,7 @@ Item.propTypes = {
createdAt: PropTypes.instanceOf(Date), createdAt: PropTypes.instanceOf(Date),
isCover: PropTypes.bool.isRequired, isCover: PropTypes.bool.isRequired,
isPersisted: PropTypes.bool.isRequired, isPersisted: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
onClick: PropTypes.func, onClick: PropTypes.func,
onCoverSelect: PropTypes.func.isRequired, onCoverSelect: PropTypes.func.isRequired,
onCoverDeselect: PropTypes.func.isRequired, onCoverDeselect: PropTypes.func.isRequired,
@@ -48,6 +48,7 @@ const CardModal = React.memo(
allBoardMemberships, allBoardMemberships,
allLabels, allLabels,
canEdit, canEdit,
canEditCommentActivities,
canEditAllCommentActivities, canEditAllCommentActivities,
onUpdate, onUpdate,
onMove, onMove,
@@ -302,7 +303,10 @@ const CardModal = React.memo(
{canEdit ? ( {canEdit ? (
<DescriptionEdit defaultValue={description} onUpdate={handleDescriptionUpdate}> <DescriptionEdit defaultValue={description} onUpdate={handleDescriptionUpdate}>
{description ? ( {description ? (
<button type="button" className={styles.descriptionText}> <button
type="button"
className={classNames(styles.descriptionText, styles.cursorPointer)}
>
<Markdown linkStopPropagation linkTarget="_blank"> <Markdown linkStopPropagation linkTarget="_blank">
{description} {description}
</Markdown> </Markdown>
@@ -348,6 +352,7 @@ const CardModal = React.memo(
<div className={styles.moduleHeader}>{t('common.attachments')}</div> <div className={styles.moduleHeader}>{t('common.attachments')}</div>
<Attachments <Attachments
items={attachments} items={attachments}
canEdit={canEdit}
onUpdate={onAttachmentUpdate} onUpdate={onAttachmentUpdate}
onDelete={onAttachmentDelete} onDelete={onAttachmentDelete}
onCoverUpdate={handleCoverUpdate} onCoverUpdate={handleCoverUpdate}
@@ -363,7 +368,7 @@ const CardModal = React.memo(
isAllFetched={isAllActivitiesFetched} isAllFetched={isAllActivitiesFetched}
isDetailsVisible={isActivitiesDetailsVisible} isDetailsVisible={isActivitiesDetailsVisible}
isDetailsFetching={isActivitiesDetailsFetching} isDetailsFetching={isActivitiesDetailsFetching}
canEdit={canEdit} canEdit={canEditCommentActivities}
canEditAllComments={canEditAllCommentActivities} canEditAllComments={canEditAllCommentActivities}
onFetch={onActivitiesFetch} onFetch={onActivitiesFetch}
onDetailsToggle={onActivitiesDetailsToggle} onDetailsToggle={onActivitiesDetailsToggle}
@@ -508,6 +513,7 @@ CardModal.propTypes = {
allLabels: PropTypes.array.isRequired, allLabels: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
canEdit: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
canEditCommentActivities: PropTypes.bool.isRequired,
canEditAllCommentActivities: PropTypes.bool.isRequired, canEditAllCommentActivities: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired, onMove: PropTypes.func.isRequired,
@@ -64,6 +64,10 @@
padding: 8px 8px 0 16px; padding: 8px 8px 0 16px;
} }
.cursorPointer {
cursor: pointer;
}
.dueDate { .dueDate {
background: rgba(9, 30, 66, 0.04); background: rgba(9, 30, 66, 0.04);
border: none; border: none;
@@ -114,7 +118,6 @@
background: transparent; background: transparent;
border: none; border: none;
color: #17394d; color: #17394d;
cursor: pointer;
font-size: 15px; font-size: 15px;
margin-bottom: 8px; margin-bottom: 8px;
outline: none; outline: none;
@@ -36,7 +36,7 @@ const CardMoveStep = React.memo(
[selectedBoard, path.listId], [selectedBoard, path.listId],
); );
const handleBoardIdFieldChange = useCallback( const handleBoardIdChange = useCallback(
(event, data) => { (event, data) => {
if (selectedProject.boards.find((board) => board.id === data.value).isFetching === null) { if (selectedProject.boards.find((board) => board.id === data.value).isFetching === null) {
onBoardFetch(data.value); onBoardFetch(data.value);
@@ -102,7 +102,7 @@ const CardMoveStep = React.memo(
} }
disabled={selectedProject.boards.length === 0} disabled={selectedProject.boards.length === 0}
className={styles.field} className={styles.field}
onChange={handleBoardIdFieldChange} onChange={handleBoardIdChange}
/> />
</> </>
)} )}
@@ -74,7 +74,11 @@ const NotificationsStep = React.memo(({ items, onDelete, onClose }) => {
return ( return (
<> <>
<Popup.Header>{t('common.notifications')}</Popup.Header> <Popup.Header>
{t('common.notifications', {
context: 'title',
})}
</Popup.Header>
<Popup.Content> <Popup.Content>
{items.length > 0 {items.length > 0
? items.map((item) => ( ? items.map((item) => (
+50 -16
View File
@@ -1,11 +1,11 @@
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import React, { useCallback } from 'react'; import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from 'semantic-ui-react'; 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 AddStep from './AddStep';
import EditStep from './EditStep'; import EditStep from './EditStep';
import Item from './Item'; import Item from './Item';
@@ -21,6 +21,20 @@ const LabelsStep = React.memo(
({ items, currentIds, title, onSelect, onDeselect, onCreate, onUpdate, onDelete, onBack }) => { ({ items, currentIds, title, onSelect, onDeselect, onCreate, onUpdate, onDelete, onBack }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps(); 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(() => { const handleAddClick = useCallback(() => {
openStep(StepTypes.ADD); openStep(StepTypes.ADD);
@@ -63,6 +77,10 @@ const LabelsStep = React.memo(
[onDelete], [onDelete],
); );
useEffect(() => {
searchField.current.focus();
}, []);
if (step) { if (step) {
switch (step.type) { switch (step.type) {
case StepTypes.ADD: case StepTypes.ADD:
@@ -91,20 +109,36 @@ const LabelsStep = React.memo(
return ( return (
<> <>
<Popup.Header onBack={onBack}>{t(title)}</Popup.Header> <Popup.Header onBack={onBack}>
{t(title, {
context: 'title',
})}
</Popup.Header>
<Popup.Content> <Popup.Content>
{items.map((item) => ( <Input
<Item fluid
key={item.id} ref={searchField}
name={item.name} value={search}
color={item.color} placeholder={t('common.searchLabels')}
isPersisted={item.isPersisted} icon="search"
isActive={currentIds.includes(item.id)} onChange={handleSearchChange}
onSelect={() => handleSelect(item.id)} />
onDeselect={() => handleDeselect(item.id)} {filteredItems.length > 0 && (
onEdit={() => handleEdit(item.id)} <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 <Button
fluid fluid
content={t('action.createNewLabel')} content={t('action.createNewLabel')}
@@ -14,4 +14,24 @@
background: #e9e9e9; 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 React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -11,12 +12,14 @@ import DeleteStep from '../DeleteStep';
import styles from './ActionsPopup.module.scss'; import styles from './ActionsPopup.module.scss';
const StepTypes = { const StepTypes = {
EDIT_PERMISSIONS: 'EDIT_PERMISSIONS',
DELETE: 'DELETE', DELETE: 'DELETE',
}; };
const ActionsStep = React.memo( const ActionsStep = React.memo(
({ ({
user, membership,
permissionsSelectStep,
leaveButtonContent, leaveButtonContent,
leaveConfirmationTitle, leaveConfirmationTitle,
leaveConfirmationContent, leaveConfirmationContent,
@@ -26,63 +29,115 @@ const ActionsStep = React.memo(
deleteConfirmationContent, deleteConfirmationContent,
deleteConfirmationButtonContent, deleteConfirmationButtonContent,
canLeave, canLeave,
canDelete, canEdit,
onUpdate,
onDelete, onDelete,
onClose,
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps(); const [step, openStep, handleBack] = useSteps();
const handleEditPermissionsClick = useCallback(() => {
openStep(StepTypes.EDIT_PERMISSIONS);
}, [openStep]);
const handleDeleteClick = useCallback(() => { const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE); openStep(StepTypes.DELETE);
}, [openStep]); }, [openStep]);
if (step && step.type === StepTypes.DELETE) { const handleRoleSelect = useCallback(
return ( (data) => {
<DeleteStep if (onUpdate) {
title={t(user.isCurrent ? leaveConfirmationTitle : deleteConfirmationTitle, { onUpdate(data);
context: 'title', }
})}
content={t(user.isCurrent ? leaveConfirmationContent : deleteConfirmationContent)} onClose();
buttonContent={t( },
user.isCurrent ? leaveConfirmationButtonContent : deleteConfirmationButtonContent, [onUpdate, onClose],
)} );
onConfirm={onDelete}
onBack={handleBack} 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 ( return (
<> <>
<span className={styles.user}> <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>
<span className={styles.content}> <span className={styles.content}>
<div className={styles.name}>{user.name}</div> <div className={styles.name}>{membership.user.name}</div>
<div className={styles.email}>{user.email}</div> <div className={styles.email}>{membership.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}
/>
)}
</span> </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 = { 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, leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string, leaveConfirmationTitle: PropTypes.string,
leaveConfirmationContent: PropTypes.string, leaveConfirmationContent: PropTypes.string,
@@ -92,11 +147,14 @@ ActionsStep.propTypes = {
deleteConfirmationContent: PropTypes.string, deleteConfirmationContent: PropTypes.string,
deleteConfirmationButtonContent: PropTypes.string, deleteConfirmationButtonContent: PropTypes.string,
canLeave: PropTypes.bool.isRequired, canLeave: PropTypes.bool.isRequired,
canDelete: PropTypes.bool.isRequired, canEdit: PropTypes.bool.isRequired,
onUpdate: PropTypes.func,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
}; };
ActionsStep.defaultProps = { ActionsStep.defaultProps = {
permissionsSelectStep: undefined,
leaveButtonContent: 'action.leaveBoard', leaveButtonContent: 'action.leaveBoard',
leaveConfirmationTitle: 'common.leaveBoard', leaveConfirmationTitle: 'common.leaveBoard',
leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard', leaveConfirmationContent: 'common.areYouSureYouWantToLeaveBoard',
@@ -105,6 +163,7 @@ ActionsStep.defaultProps = {
deleteConfirmationTitle: 'common.removeMember', deleteConfirmationTitle: 'common.removeMember',
deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard', deleteConfirmationContent: 'common.areYouSureYouWantToRemoveThisMemberFromBoard',
deleteConfirmationButtonContent: 'action.removeMember', deleteConfirmationButtonContent: 'action.removeMember',
onUpdate: undefined,
}; };
export default withPopup(ActionsStep); export default withPopup(ActionsStep);
@@ -1,28 +1,31 @@
:global(#app) { :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 { .content {
display: inline-block; display: inline-block;
width: calc(100% - 44px); 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 { .email {
color: #888888; color: #888888;
font-size: 14px; font-size: 14px;
line-height: 1.2; line-height: 1.2;
padding: 4px 0 4px 2px; padding: 2px 0 2px 2px;
} }
.name { .name {
@@ -36,7 +39,7 @@
.user { .user {
display: inline-block; display: inline-block;
padding-right: 8px; padding-right: 8px;
padding-top: 8px; padding-top: 10px;
vertical-align: top; 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 PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { withPopup } from '../../../lib/popup'; 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 UserItem from './UserItem';
import styles from './AddPopup.module.scss'; import styles from './AddPopup.module.scss';
const AddStep = React.memo(({ users, currentUserIds, title, onCreate, onClose }) => { const StepTypes = {
const [t] = useTranslation(); SELECT_PERMISSIONS: 'SELECT_PERMISSIONS',
};
const handleUserSelect = useCallback( const AddStep = React.memo(
(id) => { ({ users, currentUserIds, permissionsSelectStep, title, onCreate, onClose }) => {
onCreate({ const [t] = useTranslation();
userId: id, const [step, openStep, handleBack] = useSteps();
}); const [search, handleSearchChange] = useField('');
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
onClose(); const filteredUsers = useMemo(
}, () =>
[onCreate, onClose], users.filter(
); (user) =>
user.email.includes(cleanSearch) ||
user.name.toLowerCase().includes(cleanSearch) ||
(user.username && user.username.includes(cleanSearch)),
),
[users, cleanSearch],
);
return ( const searchField = useRef(null);
<>
<Popup.Header> const handleUserSelect = useCallback(
{t(title, { (id) => {
context: 'title', if (permissionsSelectStep) {
})} openStep(StepTypes.SELECT_PERMISSIONS, {
</Popup.Header> userId: id,
<Popup.Content> });
<div className={styles.menu}> } else {
{users.map((user) => ( onCreate({
<UserItem userId: id,
key={user.id} });
name={user.name}
avatarUrl={user.avatarUrl} onClose();
isActive={currentUserIds.includes(user.id)} }
onSelect={() => handleUserSelect(user.id)} },
/> [permissionsSelectStep, onCreate, onClose, openStep],
))} );
</div>
</Popup.Content> 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 = { AddStep.propTypes = {
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
users: PropTypes.array.isRequired, users: PropTypes.array.isRequired,
currentUserIds: PropTypes.array.isRequired, currentUserIds: PropTypes.array.isRequired,
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
permissionsSelectStep: PropTypes.elementType,
title: PropTypes.string, title: PropTypes.string,
onCreate: PropTypes.func.isRequired, onCreate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
AddStep.defaultProps = { AddStep.defaultProps = {
permissionsSelectStep: undefined,
title: 'common.addMember', title: 'common.addMember',
}; };
@@ -1,7 +1,21 @@
:global(#app) { :global(#app) {
.menu { .users {
border: none; margin-top: 8px;
margin: -7px auto -5px; max-height: 320px;
width: 100%; 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 { .menuItem {
background: transparent; background: transparent;
border: none; border: none;
border-radius: 0.28571429rem;
cursor: pointer; cursor: pointer;
display: block; display: block;
margin: 0; margin: 0;
outline: 0; outline: 0;
overflow: hidden;
padding: 4px; padding: 4px;
text-align: left; text-align: left;
width: 100%; width: 100%;
@@ -12,6 +12,7 @@ const Memberships = React.memo(
({ ({
items, items,
allUsers, allUsers,
permissionsSelectStep,
addTitle, addTitle,
leaveButtonContent, leaveButtonContent,
leaveConfirmationTitle, leaveConfirmationTitle,
@@ -24,6 +25,7 @@ const Memberships = React.memo(
canEdit, canEdit,
canLeaveIfLast, canLeaveIfLast,
onCreate, onCreate,
onUpdate,
onDelete, onDelete,
}) => { }) => {
return ( return (
@@ -32,7 +34,8 @@ const Memberships = React.memo(
{items.map((item) => ( {items.map((item) => (
<span key={item.id} className={styles.user}> <span key={item.id} className={styles.user}>
<ActionsPopup <ActionsPopup
user={item.user} membership={item}
permissionsSelectStep={permissionsSelectStep}
leaveButtonContent={leaveButtonContent} leaveButtonContent={leaveButtonContent}
leaveConfirmationTitle={leaveConfirmationTitle} leaveConfirmationTitle={leaveConfirmationTitle}
leaveConfirmationContent={leaveConfirmationContent} leaveConfirmationContent={leaveConfirmationContent}
@@ -42,7 +45,8 @@ const Memberships = React.memo(
deleteConfirmationContent={deleteConfirmationContent} deleteConfirmationContent={deleteConfirmationContent}
deleteConfirmationButtonContent={deleteConfirmationButtonContent} deleteConfirmationButtonContent={deleteConfirmationButtonContent}
canLeave={items.length > 1 || canLeaveIfLast} canLeave={items.length > 1 || canLeaveIfLast}
canDelete={canEdit} canEdit={canEdit}
onUpdate={(data) => onUpdate(item.id, data)}
onDelete={() => onDelete(item.id)} onDelete={() => onDelete(item.id)}
> >
<User <User
@@ -59,6 +63,7 @@ const Memberships = React.memo(
<AddPopup <AddPopup
users={allUsers} users={allUsers}
currentUserIds={items.map((item) => item.user.id)} currentUserIds={items.map((item) => item.user.id)}
permissionsSelectStep={permissionsSelectStep}
title={addTitle} title={addTitle}
onCreate={onCreate} onCreate={onCreate}
> >
@@ -75,6 +80,7 @@ Memberships.propTypes = {
items: PropTypes.array.isRequired, items: PropTypes.array.isRequired,
allUsers: PropTypes.array.isRequired, allUsers: PropTypes.array.isRequired,
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
permissionsSelectStep: PropTypes.elementType,
addTitle: PropTypes.string, addTitle: PropTypes.string,
leaveButtonContent: PropTypes.string, leaveButtonContent: PropTypes.string,
leaveConfirmationTitle: PropTypes.string, leaveConfirmationTitle: PropTypes.string,
@@ -87,10 +93,12 @@ Memberships.propTypes = {
canEdit: PropTypes.bool, canEdit: PropTypes.bool,
canLeaveIfLast: PropTypes.bool, canLeaveIfLast: PropTypes.bool,
onCreate: PropTypes.func.isRequired, onCreate: PropTypes.func.isRequired,
onUpdate: PropTypes.func,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
}; };
Memberships.defaultProps = { Memberships.defaultProps = {
permissionsSelectStep: undefined,
addTitle: undefined, addTitle: undefined,
leaveButtonContent: undefined, leaveButtonContent: undefined,
leaveConfirmationTitle: undefined, leaveConfirmationTitle: undefined,
@@ -102,6 +110,7 @@ Memberships.defaultProps = {
deleteConfirmationButtonContent: undefined, deleteConfirmationButtonContent: undefined,
canEdit: true, canEdit: true,
canLeaveIfLast: true, canLeaveIfLast: true,
onUpdate: undefined,
}; };
export default Memberships; export default Memberships;
@@ -1,6 +1,7 @@
:global(#app) { :global(#app) {
.action { .action {
border: none; border: none;
border-radius: 0.28571429rem;
display: inline-block; display: inline-block;
height: 36px; height: 36px;
overflow: hidden; overflow: hidden;
@@ -1,6 +1,7 @@
:global(#app) { :global(#app) {
.action { .action {
border: none; border: none;
border-radius: 0.28571429rem;
display: inline-block; display: inline-block;
height: 36px; height: 36px;
overflow: hidden; overflow: hidden;
@@ -1,6 +1,7 @@
:global(#app) { :global(#app) {
.action { .action {
border: none; border: none;
border-radius: 0.28571429rem;
display: inline-block; display: inline-block;
height: 36px; height: 36px;
overflow: hidden; overflow: hidden;
@@ -1,6 +1,7 @@
:global(#app) { :global(#app) {
.action { .action {
border: none; border: none;
border-radius: 0.28571429rem;
display: inline-block; display: inline-block;
height: 36px; height: 36px;
overflow: hidden; overflow: hidden;
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Button, Icon, Radio, Table } from 'semantic-ui-react'; import { Button, Icon, Radio, Table } from 'semantic-ui-react';
import ActionsPopup from './ActionsPopup'; import ActionsPopup from './ActionsPopup';
import User from '../../User';
import styles from './Item.module.scss'; import styles from './Item.module.scss';
@@ -11,6 +12,7 @@ const Item = React.memo(
email, email,
username, username,
name, name,
avatarUrl,
organization, organization,
phone, phone,
isAdmin, isAdmin,
@@ -34,13 +36,16 @@ const Item = React.memo(
return ( return (
<Table.Row> <Table.Row>
<Table.Cell>
<User name={name} avatarUrl={avatarUrl} />
</Table.Cell>
<Table.Cell>{name}</Table.Cell> <Table.Cell>{name}</Table.Cell>
<Table.Cell>{username || '-'}</Table.Cell> <Table.Cell>{username || '-'}</Table.Cell>
<Table.Cell>{email}</Table.Cell> <Table.Cell>{email}</Table.Cell>
<Table.Cell collapsing> <Table.Cell>
<Radio toggle checked={isAdmin} onChange={handleIsAdminChange} /> <Radio toggle checked={isAdmin} onChange={handleIsAdminChange} />
</Table.Cell> </Table.Cell>
<Table.Cell collapsing> <Table.Cell textAlign="right">
<ActionsPopup <ActionsPopup
user={{ user={{
email, email,
@@ -76,6 +81,7 @@ Item.propTypes = {
email: PropTypes.string.isRequired, email: PropTypes.string.isRequired,
username: PropTypes.string, username: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
avatarUrl: PropTypes.string,
organization: PropTypes.string, organization: PropTypes.string,
phone: PropTypes.string, phone: PropTypes.string,
isAdmin: PropTypes.bool.isRequired, isAdmin: PropTypes.bool.isRequired,
@@ -96,6 +102,7 @@ Item.propTypes = {
Item.defaultProps = { Item.defaultProps = {
username: undefined, username: undefined,
avatarUrl: undefined,
organization: undefined, organization: undefined,
phone: undefined, phone: undefined,
}; };
@@ -84,10 +84,11 @@ const UsersModal = React.memo(
context: 'title', context: 'title',
})} })}
</Modal.Header> </Modal.Header>
<Modal.Content> <Modal.Content scrolling>
<Table basic="very"> <Table unstackable basic="very">
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
<Table.HeaderCell />
<Table.HeaderCell width={4}>{t('common.name')}</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.username')}</Table.HeaderCell>
<Table.HeaderCell width={4}>{t('common.email')}</Table.HeaderCell> <Table.HeaderCell width={4}>{t('common.email')}</Table.HeaderCell>
@@ -102,6 +103,7 @@ const UsersModal = React.memo(
email={item.email} email={item.email}
username={item.username} username={item.username}
name={item.name} name={item.name}
avatarUrl={item.avatarUrl}
organization={item.organization} organization={item.organization}
phone={item.phone} phone={item.phone}
isAdmin={item.isAdmin} isAdmin={item.isAdmin}
+4
View File
@@ -123,6 +123,10 @@ export default {
BOARD_MEMBERSHIP_CREATE__FAILURE: 'BOARD_MEMBERSHIP_CREATE__FAILURE', BOARD_MEMBERSHIP_CREATE__FAILURE: 'BOARD_MEMBERSHIP_CREATE__FAILURE',
BOARD_MEMBERSHIP_CREATE_HANDLE: 'BOARD_MEMBERSHIP_CREATE_HANDLE', BOARD_MEMBERSHIP_CREATE_HANDLE: 'BOARD_MEMBERSHIP_CREATE_HANDLE',
BOARD_MEMBERSHIP_CREATE_HANDLE__PROJECT_FETCH: 'BOARD_MEMBERSHIP_CREATE_HANDLE__PROJECT_FETCH', 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: 'BOARD_MEMBERSHIP_DELETE',
BOARD_MEMBERSHIP_DELETE__SUCCESS: 'BOARD_MEMBERSHIP_DELETE__SUCCESS', BOARD_MEMBERSHIP_DELETE__SUCCESS: 'BOARD_MEMBERSHIP_DELETE__SUCCESS',
BOARD_MEMBERSHIP_DELETE__FAILURE: 'BOARD_MEMBERSHIP_DELETE__FAILURE', BOARD_MEMBERSHIP_DELETE__FAILURE: 'BOARD_MEMBERSHIP_DELETE__FAILURE',
+2
View File
@@ -88,6 +88,8 @@ export default {
MEMBERSHIP_IN_CURRENT_BOARD_CREATE: `${PREFIX}/MEMBERSHIP_IN_CURRENT_BOARD_CREATE`, MEMBERSHIP_IN_CURRENT_BOARD_CREATE: `${PREFIX}/MEMBERSHIP_IN_CURRENT_BOARD_CREATE`,
BOARD_MEMBERSHIP_CREATE_HANDLE: `${PREFIX}/BOARD_MEMBERSHIP_CREATE_HANDLE`, 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: `${PREFIX}/BOARD_MEMBERSHIP_DELETE`,
BOARD_MEMBERSHIP_DELETE_HANDLE: `${PREFIX}/BOARD_MEMBERSHIP_DELETE_HANDLE`, BOARD_MEMBERSHIP_DELETE_HANDLE: `${PREFIX}/BOARD_MEMBERSHIP_DELETE_HANDLE`,
+5
View File
@@ -7,6 +7,11 @@ export const BoardTypes = {
KANBAN: 'kanban', KANBAN: 'kanban',
}; };
export const BoardMembershipRoles = {
EDITOR: 'editor',
VIEWER: 'viewer',
};
export const ActivityTypes = { export const ActivityTypes = {
CREATE_CARD: 'createCard', CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard', MOVE_CARD: 'moveCard',
@@ -27,6 +27,7 @@ const mapDispatchToProps = (dispatch) =>
bindActionCreators( bindActionCreators(
{ {
onMembershipCreate: entryActions.createMembershipInCurrentBoard, onMembershipCreate: entryActions.createMembershipInCurrentBoard,
onMembershipUpdate: entryActions.updateBoardMembership,
onMembershipDelete: entryActions.deleteBoardMembership, onMembershipDelete: entryActions.deleteBoardMembership,
onUserToFilterAdd: entryActions.addUserToFilterInCurrentBoard, onUserToFilterAdd: entryActions.addUserToFilterInCurrentBoard,
onUserFromFilterRemove: entryActions.removeUserFromFilterInCurrentBoard, onUserFromFilterRemove: entryActions.removeUserFromFilterInCurrentBoard,
@@ -3,17 +3,18 @@ import { connect } from 'react-redux';
import selectors from '../selectors'; import selectors from '../selectors';
import entryActions from '../entry-actions'; import entryActions from '../entry-actions';
import { BoardMembershipRoles } from '../constants/Enums';
import BoardKanban from '../components/BoardKanban'; import BoardKanban from '../components/BoardKanban';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const { cardId } = selectors.selectPath(state); const { cardId } = selectors.selectPath(state);
const isCurrentUserMember = selectors.selectIsCurrentUserMemberForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const listIds = selectors.selectListIdsForCurrentBoard(state); const listIds = selectors.selectListIdsForCurrentBoard(state);
return { return {
listIds, listIds,
isCardModalOpened: !!cardId, isCardModalOpened: !!cardId,
canEdit: isCurrentUserMember, canEdit: !!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
}; };
}; };
+4 -2
View File
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import selectors from '../selectors'; import selectors from '../selectors';
import entryActions from '../entry-actions'; import entryActions from '../entry-actions';
import { BoardMembershipRoles } from '../constants/Enums';
import Card from '../components/Card'; import Card from '../components/Card';
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@@ -17,7 +18,7 @@ const makeMapStateToProps = () => {
const allProjectsToLists = selectors.selectProjectsToListsForCurrentUser(state); const allProjectsToLists = selectors.selectProjectsToListsForCurrentUser(state);
const allBoardMemberships = selectors.selectMembershipsForCurrentBoard(state); const allBoardMemberships = selectors.selectMembershipsForCurrentBoard(state);
const allLabels = selectors.selectLabelsForCurrentBoard(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( const { name, dueDate, timer, coverUrl, boardId, listId, isPersisted } = selectCardById(
state, state,
@@ -47,7 +48,8 @@ const makeMapStateToProps = () => {
allProjectsToLists, allProjectsToLists,
allBoardMemberships, allBoardMemberships,
allLabels, allLabels,
canEdit: isCurrentUserMember, canEdit:
!!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
}; };
}; };
}; };
+7 -2
View File
@@ -6,6 +6,7 @@ import omit from 'lodash/omit';
import selectors from '../selectors'; import selectors from '../selectors';
import entryActions from '../entry-actions'; import entryActions from '../entry-actions';
import Paths from '../constants/Paths'; import Paths from '../constants/Paths';
import { BoardMembershipRoles } from '../constants/Enums';
import CardModal from '../components/CardModal'; import CardModal from '../components/CardModal';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
@@ -14,7 +15,7 @@ const mapStateToProps = (state) => {
const isCurrentUserManager = selectors.selectIsCurrentUserManagerForCurrentProject(state); const isCurrentUserManager = selectors.selectIsCurrentUserManagerForCurrentProject(state);
const allBoardMemberships = selectors.selectMembershipsForCurrentBoard(state); const allBoardMemberships = selectors.selectMembershipsForCurrentBoard(state);
const allLabels = selectors.selectLabelsForCurrentBoard(state); const allLabels = selectors.selectLabelsForCurrentBoard(state);
const isCurrentUserMember = selectors.selectIsCurrentUserMemberForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
const { const {
name, name,
@@ -57,7 +58,11 @@ const mapStateToProps = (state) => {
allProjectsToLists, allProjectsToLists,
allBoardMemberships, allBoardMemberships,
allLabels, allLabels,
canEdit: isCurrentUserMember, canEdit: !!currentUserMembership && currentUserMembership.role === BoardMembershipRoles.EDITOR,
canEditCommentActivities:
!!currentUserMembership &&
(currentUserMembership.role === BoardMembershipRoles.EDITOR ||
currentUserMembership.canComment),
canEditAllCommentActivities: isCurrentUserManager, canEditAllCommentActivities: isCurrentUserManager,
}; };
}; };
+4 -2
View File
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import selectors from '../selectors'; import selectors from '../selectors';
import entryActions from '../entry-actions'; import entryActions from '../entry-actions';
import { BoardMembershipRoles } from '../constants/Enums';
import List from '../components/List'; import List from '../components/List';
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@@ -12,7 +13,7 @@ const makeMapStateToProps = () => {
return (state, { id, index }) => { return (state, { id, index }) => {
const { name, isPersisted } = selectListById(state, id); const { name, isPersisted } = selectListById(state, id);
const cardIds = selectCardIdsByListId(state, id); const cardIds = selectCardIdsByListId(state, id);
const isCurrentUserMember = selectors.selectIsCurrentUserMemberForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state);
return { return {
id, id,
@@ -20,7 +21,8 @@ const makeMapStateToProps = () => {
name, name,
isPersisted, isPersisted,
cardIds, 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) => ({ const deleteBoardMembership = (id) => ({
type: EntryActionTypes.BOARD_MEMBERSHIP_DELETE, type: EntryActionTypes.BOARD_MEMBERSHIP_DELETE,
payload: { payload: {
@@ -31,6 +46,8 @@ const handleBoardMembershipDelete = (boardMembership) => ({
export default { export default {
createMembershipInCurrentBoard, createMembershipInCurrentBoard,
handleBoardMembershipCreate, handleBoardMembershipCreate,
updateBoardMembership,
handleBoardMembershipUpdate,
deleteBoardMembership, deleteBoardMembership,
handleBoardMembershipDelete, handleBoardMembershipDelete,
}; };
+12
View File
@@ -41,6 +41,9 @@ export default {
background: 'Background', background: 'Background',
board: 'Board', board: 'Board',
boardNotFound_title: 'Board Not Found', 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', cardActions_title: 'Card Actions',
cardNotFound_title: 'Card Not Found', cardNotFound_title: 'Card Not Found',
cardOrActionAreDeleted: 'Card or action are deleted', cardOrActionAreDeleted: 'Card or action are deleted',
@@ -66,6 +69,7 @@ export default {
description: 'Description', description: 'Description',
detectAutomatically: 'Detect automatically', detectAutomatically: 'Detect automatically',
dropFileToUpload: 'Drop file to upload', dropFileToUpload: 'Drop file to upload',
editor: 'Editor',
editAttachment_title: 'Edit Attachment', editAttachment_title: 'Edit Attachment',
editAvatar_title: 'Edit Avatar', editAvatar_title: 'Edit Avatar',
editBoard_title: 'Edit Board', editBoard_title: 'Edit Board',
@@ -74,6 +78,7 @@ export default {
editInformation_title: 'Edit Information', editInformation_title: 'Edit Information',
editLabel_title: 'Edit Label', editLabel_title: 'Edit Label',
editPassword_title: 'Edit Password', editPassword_title: 'Edit Password',
editPermissions_title: 'Edit Permissions',
editTimer_title: 'Edit Timer', editTimer_title: 'Edit Timer',
editUsername_title: 'Edit Username', editUsername_title: 'Edit Username',
email: 'E-mail', email: 'E-mail',
@@ -121,9 +126,13 @@ export default {
projectNotFound_title: 'Project Not Found', projectNotFound_title: 'Project Not Found',
removeManager_title: 'Remove Manager', removeManager_title: 'Remove Manager',
removeMember_title: 'Remove Member', removeMember_title: 'Remove Member',
searchLabels: 'Search labels...',
searchMembers: 'Search members...',
searchUsers: 'Search users...',
seconds: 'Seconds', seconds: 'Seconds',
selectBoard: 'Select board', selectBoard: 'Select board',
selectList: 'Select list', selectList: 'Select list',
selectPermissions_title: 'Select Permissions',
selectProject: 'Select project', selectProject: 'Select project',
settings: 'Settings', settings: 'Settings',
subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default', subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default',
@@ -143,6 +152,7 @@ export default {
username: 'Username', username: 'Username',
usernameAlreadyInUse: 'Username already in use', usernameAlreadyInUse: 'Username already in use',
users: 'Users', users: 'Users',
viewer: 'Viewer',
writeComment: 'Write a comment...', writeComment: 'Write a comment...',
}, },
@@ -154,6 +164,7 @@ export default {
addCard_title: 'Add Card', addCard_title: 'Add Card',
addComment: 'Add comment', addComment: 'Add comment',
addList: 'Add list', addList: 'Add list',
addMember: 'Add member',
addMoreDetailedDescription: 'Add more detailed description', addMoreDetailedDescription: 'Add more detailed description',
addTask: 'Add task', addTask: 'Add task',
addToCard: 'Add to card', addToCard: 'Add to card',
@@ -185,6 +196,7 @@ export default {
editEmail_title: 'Edit E-mail', editEmail_title: 'Edit E-mail',
editInformation_title: 'Edit Information', editInformation_title: 'Edit Information',
editPassword_title: 'Edit Password', editPassword_title: 'Edit Password',
editPermissions: 'Edit permissions',
editTimer_title: 'Edit Timer', editTimer_title: 'Edit Timer',
editTitle_title: 'Edit Title', editTitle_title: 'Edit Title',
editUsername_title: 'Edit Username', editUsername_title: 'Edit Username',
+2 -1
View File
@@ -4,6 +4,7 @@ import de from './de';
import en from './en'; import en from './en';
import es from './es'; import es from './es';
import fr from './fr'; import fr from './fr';
import it from './it';
import ja from './ja'; import ja from './ja';
import ko from './ko'; import ko from './ko';
import pl from './pl'; import pl from './pl';
@@ -13,7 +14,7 @@ import sv from './sv';
import uz from './uz'; import uz from './uz';
import zh from './zh'; 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; export default locales;
+230
View File
@@ -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',
},
},
};
+8
View File
@@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'it',
country: 'it',
name: 'Italiano',
embeddedLocale: login,
};
+20
View File
@@ -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',
},
},
};
+13 -1
View File
@@ -39,6 +39,9 @@ export default {
authentication: 'Аутентификация', authentication: 'Аутентификация',
board: 'Доска', board: 'Доска',
boardNotFound: 'Доска не найдена', boardNotFound: 'Доска не найдена',
canComment: 'Может комментировать',
canEditContentOfBoard: 'Может редактировать содержимое доски',
canOnlyViewBoard: 'Может только просматривать доску',
cardActions: 'Действия с карточкой', cardActions: 'Действия с карточкой',
cardNotFound: 'Карточка не найдена', cardNotFound: 'Карточка не найдена',
cardOrActionAreDeleted: 'Карточка или действие удалены', cardOrActionAreDeleted: 'Карточка или действие удалены',
@@ -64,6 +67,7 @@ export default {
description: 'Описание', description: 'Описание',
detectAutomatically: 'Определить автоматически', detectAutomatically: 'Определить автоматически',
dropFileToUpload: 'Перетяните файл, чтобы загрузить', dropFileToUpload: 'Перетяните файл, чтобы загрузить',
editor: 'Редактор',
editAttachment: 'Изменение вложения', editAttachment: 'Изменение вложения',
editAvatar: 'Изменение аватара', editAvatar: 'Изменение аватара',
editBoard: 'Изменение доски', editBoard: 'Изменение доски',
@@ -72,6 +76,7 @@ export default {
editEmail: 'Изменение e-mail', editEmail: 'Изменение e-mail',
editLabel: 'Изменения метки', editLabel: 'Изменения метки',
editPassword: 'Изменение пароля', editPassword: 'Изменение пароля',
editPermissions: 'Редактирование разрешений',
editTimer: 'Изменение таймера', editTimer: 'Изменение таймера',
editTitle: 'Изменение названия', editTitle: 'Изменение названия',
editUsername: 'Изменение имени пользователя', editUsername: 'Изменение имени пользователя',
@@ -116,9 +121,13 @@ export default {
projectActions: 'Действия с проектом', projectActions: 'Действия с проектом',
projectNotFound: 'Проект не найден', projectNotFound: 'Проект не найден',
removeMember: 'Удаление участника', removeMember: 'Удаление участника',
searchLabels: 'Поиск меток...',
searchMembers: 'Поиск участников...',
searchUsers: 'Поиск пользователей...',
seconds: 'Секунды', seconds: 'Секунды',
selectBoard: 'Выберите доску', selectBoard: 'Выберите доску',
selectList: 'Выберите список', selectList: 'Выберите список',
selectPermissions: 'Выбор разрешений',
selectProject: 'Выберите проект', selectProject: 'Выберите проект',
settings: 'Настройки', settings: 'Настройки',
subscribeToMyOwnCardsByDefault: 'По умолчанию подписаться на мои собственные карточки', subscribeToMyOwnCardsByDefault: 'По умолчанию подписаться на мои собственные карточки',
@@ -128,7 +137,7 @@ export default {
time: 'Время', time: 'Время',
timer: 'Таймер', timer: 'Таймер',
title: 'Название', title: 'Название',
userActions_title: 'Действия с пользователем', userActions: 'Действия с пользователем',
userAddedThisCardToList: '<0>{{user}}</0><1> добавил(а) эту карточку в {{list}}</1>', userAddedThisCardToList: '<0>{{user}}</0><1> добавил(а) эту карточку в {{list}}</1>',
userLeftNewCommentToCard: '{{user}} оставил(а) комментарий «{{comment}}» к <2>{{card}}</2>', userLeftNewCommentToCard: '{{user}} оставил(а) комментарий «{{comment}}» к <2>{{card}}</2>',
userMovedCardFromListToList: userMovedCardFromListToList:
@@ -138,6 +147,7 @@ export default {
username: 'Имя пользователя', username: 'Имя пользователя',
usernameAlreadyInUse: 'Имя пользователя уже занято', usernameAlreadyInUse: 'Имя пользователя уже занято',
users: 'Пользователи', users: 'Пользователи',
viewer: 'Читатель',
writeComment: 'Напишите комментарий...', writeComment: 'Напишите комментарий...',
}, },
@@ -148,6 +158,7 @@ export default {
addCard: 'Добавить карточку', addCard: 'Добавить карточку',
addComment: 'Добавить комментарий', addComment: 'Добавить комментарий',
addList: 'Добавить список', addList: 'Добавить список',
addMember: 'Добавить участника',
addMoreDetailedDescription: 'Добавить более подробное описание', addMoreDetailedDescription: 'Добавить более подробное описание',
addTask: 'Добавить задачу', addTask: 'Добавить задачу',
addToCard: 'Добавить на карточку', addToCard: 'Добавить на карточку',
@@ -175,6 +186,7 @@ export default {
editDescription: 'Изменить описание', editDescription: 'Изменить описание',
editEmail: 'Изменить e-mail', editEmail: 'Изменить e-mail',
editPassword: 'Изменить пароль', editPassword: 'Изменить пароль',
editPermissions: 'Изменить разрешения',
editTask: 'Изменить задачу', editTask: 'Изменить задачу',
editTimer: 'Изменить таймер', editTimer: 'Изменить таймер',
editTitle: 'Изменить название', editTitle: 'Изменить название',
+8
View File
@@ -175,6 +175,14 @@ export default class extends Model {
return this.lists.orderBy('position'); return this.lists.orderBy('position');
} }
getMembershipModel(userId) {
return this.memberships
.filter({
userId,
})
.first();
}
hasMemberUser(userId) { hasMemberUser(userId) {
return this.memberships return this.memberships
.filter({ .filter({
+11
View File
@@ -7,6 +7,8 @@ export default class extends Model {
static fields = { static fields = {
id: attr(), id: attr(),
role: attr(),
canComment: attr(),
boardId: fk({ boardId: fk({
to: 'Board', to: 'Board',
as: '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; break;
case ActionTypes.BOARD_MEMBERSHIP_DELETE: case ActionTypes.BOARD_MEMBERSHIP_DELETE:
BoardMembership.withId(payload.id).deleteWithRelated(); BoardMembership.withId(payload.id).deleteWithRelated();
@@ -84,7 +84,7 @@ export function* handleBoardMembershipCreate(boardMembership) {
let body; let body;
try { try {
body = yield call(requests.fetchBoardByCurrentPathRequest); body = yield call(requests.fetchBoardByCurrentPath);
} catch {} // eslint-disable-line no-empty } catch {} // eslint-disable-line no-empty
if (body && body.project && body.project.id === board2.projectId) { 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) { export function* deleteBoardMembership(id) {
let boardMembership = yield select(selectors.selectBoardMembershipById, id); let boardMembership = yield select(selectors.selectBoardMembershipById, id);
@@ -184,6 +202,8 @@ export default {
createBoardMembership, createBoardMembership,
createMembershipInCurrentBoard, createMembershipInCurrentBoard,
handleBoardMembershipCreate, handleBoardMembershipCreate,
updateBoardMembership,
handleBoardMembershipUpdate,
deleteBoardMembership, deleteBoardMembership,
handleBoardMembershipDelete, handleBoardMembershipDelete,
}; };
@@ -11,6 +11,12 @@ export default function* boardMembershipsWatchers() {
takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE, ({ payload: { boardMembership } }) => takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_CREATE_HANDLE, ({ payload: { boardMembership } }) =>
services.handleBoardMembershipCreate(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 } }) => takeEvery(EntryActionTypes.BOARD_MEMBERSHIP_DELETE, ({ payload: { id } }) =>
services.deleteBoardMembership(id), services.deleteBoardMembership(id),
), ),
+6
View File
@@ -64,6 +64,10 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleBoardMembershipCreate(item)); emit(entryActions.handleBoardMembershipCreate(item));
}; };
const handleBoardMembershipUpdate = ({ item }) => {
emit(entryActions.handleBoardMembershipUpdate(item));
};
const handleBoardMembershipDelete = ({ item }) => { const handleBoardMembershipDelete = ({ item }) => {
emit(entryActions.handleBoardMembershipDelete(item)); emit(entryActions.handleBoardMembershipDelete(item));
}; };
@@ -183,6 +187,7 @@ const createSocketEventsChannel = () =>
socket.on('boardDelete', handleBoardDelete); socket.on('boardDelete', handleBoardDelete);
socket.on('boardMembershipCreate', handleBoardMembershipCreate); socket.on('boardMembershipCreate', handleBoardMembershipCreate);
socket.on('boardMembershipUpdate', handleBoardMembershipUpdate);
socket.on('boardMembershipDelete', handleBoardMembershipDelete); socket.on('boardMembershipDelete', handleBoardMembershipDelete);
socket.on('listCreate', handleListCreate); socket.on('listCreate', handleListCreate);
@@ -238,6 +243,7 @@ const createSocketEventsChannel = () =>
socket.off('boardDelete', handleBoardDelete); socket.off('boardDelete', handleBoardDelete);
socket.off('boardMembershipCreate', handleBoardMembershipCreate); socket.off('boardMembershipCreate', handleBoardMembershipCreate);
socket.off('boardMembershipUpdate', handleBoardMembershipUpdate);
socket.off('boardMembershipDelete', handleBoardMembershipDelete); socket.off('boardMembershipDelete', handleBoardMembershipDelete);
socket.off('listCreate', handleListCreate); socket.off('listCreate', handleListCreate);
+11 -5
View File
@@ -147,22 +147,28 @@ export const selectFilterLabelsForCurrentBoard = createSelector(
}, },
); );
export const selectIsCurrentUserMemberForCurrentBoard = createSelector( export const selectCurrentUserMembershipForCurrentBoard = createSelector(
orm, orm,
(state) => selectPath(state).boardId, (state) => selectPath(state).boardId,
(state) => selectCurrentUserId(state), (state) => selectCurrentUserId(state),
({ Board }, id, currentUserId) => { ({ Board }, id, currentUserId) => {
if (!id) { if (!id) {
return false; return id;
} }
const boardModel = Board.withId(id); const boardModel = Board.withId(id);
if (!boardModel) { 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, selectListIdsForCurrentBoard,
selectFilterUsersForCurrentBoard, selectFilterUsersForCurrentBoard,
selectFilterLabelsForCurrentBoard, selectFilterLabelsForCurrentBoard,
selectIsCurrentUserMemberForCurrentBoard, selectCurrentUserMembershipForCurrentBoard,
}; };
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "planka", "name": "planka",
"version": "1.6.0", "version": "1.7.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "planka", "name": "planka",
"version": "1.6.0", "version": "1.7.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "planka", "name": "planka",
"version": "1.6.0", "version": "1.7.2",
"private": true, "private": true,
"homepage": "https://plankanban.github.io/planka", "homepage": "https://plankanban.github.io/planka",
"repository": { "repository": {
+2
View File
@@ -143,3 +143,5 @@ private/attachments/*
views/* views/*
!views/.gitkeep !views/.gitkeep
logs
@@ -1,6 +1,8 @@
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const validator = require('validator'); const validator = require('validator');
const { getRemoteAddress } = require('../../../utils/remoteAddress');
const Errors = { const Errors = {
INVALID_EMAIL_OR_USERNAME: { INVALID_EMAIL_OR_USERNAME: {
invalidEmailOrUsername: 'Invalid email or username', invalidEmailOrUsername: 'Invalid email or username',
@@ -41,10 +43,16 @@ module.exports = {
const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername); const user = await sails.helpers.users.getOneByEmailOrUsername(inputs.emailOrUsername);
if (!user) { if (!user) {
sails.log.warn(
`Invalid email or username: "${inputs.emailOrUsername}"! (IP: ${getRemoteAddress(
this.req,
)})`,
);
throw Errors.INVALID_EMAIL_OR_USERNAME; throw Errors.INVALID_EMAIL_OR_USERNAME;
} }
if (!bcrypt.compareSync(inputs.password, user.password)) { if (!bcrypt.compareSync(inputs.password, user.password)) {
sails.log.warn(`Invalid password! (IP: ${getRemoteAddress(this.req)})`);
throw Errors.INVALID_PASSWORD; throw Errors.INVALID_PASSWORD;
} }
+53 -27
View File
@@ -1,7 +1,16 @@
const util = require('util');
const { v4: uuid } = require('uuid');
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
NO_FILE_WAS_UPLOADED: {
noFileWasUploaded: 'No file was uploaded',
},
}; };
module.exports = { module.exports = {
@@ -18,9 +27,15 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
noFileWasUploaded: {
responseType: 'unprocessableEntity',
},
uploadError: { uploadError: {
responseType: 'unprocessableEntity', responseType: 'unprocessableEntity',
}, },
@@ -33,39 +48,50 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .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 throw Errors.CARD_NOT_FOUND; // Forbidden
} }
this.req if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
.file('file') throw Errors.NOT_ENOUGH_RIGHTS;
.upload(sails.helpers.utils.createAttachmentReceiver(), async (error, files) => { }
if (error) {
return exits.uploadError(error.message);
}
if (files.length === 0) { const upload = util.promisify((options, callback) =>
return exits.uploadError('No file was uploaded'); this.req.file('file').upload(options, (error, files) => callback(error, files)),
} );
const file = files[0]; let files;
try {
const attachment = await sails.helpers.attachments.createOne( files = await upload({
{ saveAs: uuid(),
...file.extra, maxBytes: null,
filename: file.filename,
},
currentUser,
card,
inputs.requestId,
this.req,
);
return exits.success({
item: attachment.toJSON(),
});
}); });
} 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,
});
}, },
}; };
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
ATTACHMENT_NOT_FOUND: { ATTACHMENT_NOT_FOUND: {
attachmentNotFound: 'Attachment not found', attachmentNotFound: 'Attachment not found',
}, },
@@ -14,6 +17,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
attachmentNotFound: { attachmentNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -29,12 +35,19 @@ module.exports = {
let { attachment } = path; let { attachment } = path;
const { card, board } = 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 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); attachment = await sails.helpers.attachments.deleteOne(attachment, board, card, this.req);
if (!attachment) { if (!attachment) {
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
ATTACHMENT_NOT_FOUND: { ATTACHMENT_NOT_FOUND: {
attachmentNotFound: 'Attachment not found', attachmentNotFound: 'Attachment not found',
}, },
@@ -18,6 +21,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
attachmentNotFound: { attachmentNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -33,12 +39,19 @@ module.exports = {
let { attachment } = path; let { attachment } = path;
const { board } = 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 throw Errors.ATTACHMENT_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['name']); const values = _.pick(inputs, ['name']);
attachment = await sails.helpers.attachments.updateOne(attachment, values, board, this.req); attachment = await sails.helpers.attachments.updateOne(attachment, values, board, this.req);
@@ -22,6 +22,14 @@ module.exports = {
regex: /^[0-9]+$/, regex: /^[0-9]+$/,
required: true, required: true,
}, },
role: {
type: 'string',
isIn: Object.values(BoardMembership.Roles),
required: true,
},
canComment: {
type: 'boolean',
},
}, },
exits: { exits: {
@@ -58,8 +66,10 @@ module.exports = {
throw Error.USER_NOT_FOUND; throw Error.USER_NOT_FOUND;
} }
const values = _.pick(inputs, ['role', 'canComment']);
const boardMembership = await sails.helpers.boardMemberships const boardMembership = await sails.helpers.boardMemberships
.createOne(user, board, this.req) .createOne(values, user, board, this.req)
.intercept('userAlreadyBoardMember', () => Errors.USER_ALREADY_BOARD_MEMBER); .intercept('userAlreadyBoardMember', () => Errors.USER_ALREADY_BOARD_MEMBER);
return { 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,
};
},
};
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@@ -25,6 +28,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -43,12 +49,19 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .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 throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const label = await Label.findOne({ const label = await Label.findOne({
id: inputs.labelId, id: inputs.labelId,
boardId: card.boardId, boardId: card.boardId,
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@@ -22,6 +25,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -37,12 +43,19 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .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 throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
let cardLabel = await CardLabel.findOne({ let cardLabel = await CardLabel.findOne({
cardId: inputs.cardId, cardId: inputs.cardId,
labelId: inputs.labelId, labelId: inputs.labelId,
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@@ -25,6 +28,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -43,13 +49,20 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .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 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) { if (!isBoardMember) {
throw Errors.USER_NOT_FOUND; throw Errors.USER_NOT_FOUND;
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@@ -22,6 +25,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -37,12 +43,19 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .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 throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
let cardMembership = await CardMembership.findOne({ let cardMembership = await CardMembership.findOne({
cardId: inputs.cardId, cardId: inputs.cardId,
userId: inputs.userId, userId: inputs.userId,
+15 -2
View File
@@ -1,6 +1,9 @@
const moment = require('moment'); const moment = require('moment');
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
BOARD_NOT_FOUND: { BOARD_NOT_FOUND: {
boardNotFound: 'Board not found', boardNotFound: 'Board not found',
}, },
@@ -67,6 +70,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
boardNotFound: { boardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -88,12 +94,19 @@ module.exports = {
.getProjectPath(inputs.boardId) .getProjectPath(inputs.boardId)
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND); .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 throw Errors.BOARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
let list; let list;
if (!_.isUndefined(inputs.listId)) { if (!_.isUndefined(inputs.listId)) {
list = await List.findOne({ list = await List.findOne({
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@@ -14,6 +17,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -26,12 +32,19 @@ module.exports = {
.getProjectPath(inputs.id) .getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .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 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); card = await sails.helpers.cards.deleteOne(card, this.req);
if (!card) { if (!card) {
+3 -3
View File
@@ -23,7 +23,7 @@ module.exports = {
}, },
}, },
async fn(inputs, exits) { async fn(inputs) {
const { currentUser } = this.req; const { currentUser } = this.req;
const { board } = await sails.helpers.boards const { board } = await sails.helpers.boards
@@ -61,7 +61,7 @@ module.exports = {
const tasks = await sails.helpers.cards.getTasks(cardIds); const tasks = await sails.helpers.cards.getTasks(cardIds);
const attachments = await sails.helpers.cards.getAttachments(cardIds); const attachments = await sails.helpers.cards.getAttachments(cardIds);
return exits.success({ return {
items: cards, items: cards,
included: { included: {
cardMemberships, cardMemberships,
@@ -69,6 +69,6 @@ module.exports = {
tasks, tasks,
attachments, attachments,
}, },
}); };
}, },
}; };
+24 -4
View File
@@ -1,6 +1,9 @@
const moment = require('moment'); const moment = require('moment');
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@@ -83,6 +86,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -110,23 +116,37 @@ module.exports = {
let { card } = path; let { card } = path;
const { list, board } = 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 throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
let nextBoard; let nextBoard;
if (!_.isUndefined(inputs.boardId)) { if (!_.isUndefined(inputs.boardId)) {
({ board: nextBoard } = await sails.helpers.boards ({ board: nextBoard } = await sails.helpers.boards
.getProjectPath(inputs.boardId) .getProjectPath(inputs.boardId)
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND)); .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 throw Errors.BOARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
} }
let nextList; let nextList;
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@@ -18,6 +21,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -30,12 +36,19 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .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 throw Errors.CARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR && !boardMembership.canComment) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = { const values = {
type: Action.Types.COMMENT_CARD, type: Action.Types.COMMENT_CARD,
data: _.pick(inputs, ['text']), data: _.pick(inputs, ['text']),
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
COMMENT_ACTION_NOT_FOUND: { COMMENT_ACTION_NOT_FOUND: {
commentActionNotFound: 'Comment action not found', commentActionNotFound: 'Comment action not found',
}, },
@@ -14,6 +17,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
commentActionNotFound: { commentActionNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -39,11 +45,18 @@ module.exports = {
throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden 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 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); action = await sails.helpers.actions.deleteOne(action, board, this.req);
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
COMMENT_ACTION_NOT_FOUND: { COMMENT_ACTION_NOT_FOUND: {
commentActionNotFound: 'Comment action not found', commentActionNotFound: 'Comment action not found',
}, },
@@ -18,6 +21,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
commentActionNotFound: { commentActionNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -43,11 +49,18 @@ module.exports = {
throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden 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 throw Errors.COMMENT_ACTION_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR && !boardMembership.canComment) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
} }
const values = { const values = {
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
BOARD_NOT_FOUND: { BOARD_NOT_FOUND: {
boardNotFound: 'Board not found', boardNotFound: 'Board not found',
}, },
@@ -24,6 +27,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
boardNotFound: { boardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -36,12 +42,19 @@ module.exports = {
.getProjectPath(inputs.boardId) .getProjectPath(inputs.boardId)
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND); .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 throw Errors.BOARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['name', 'color']); const values = _.pick(inputs, ['name', 'color']);
const label = await sails.helpers.labels.createOne(values, board, this.req); const label = await sails.helpers.labels.createOne(values, board, this.req);
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
LABEL_NOT_FOUND: { LABEL_NOT_FOUND: {
labelNotFound: 'Label not found', labelNotFound: 'Label not found',
}, },
@@ -14,6 +17,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
labelNotFound: { labelNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -26,12 +32,19 @@ module.exports = {
.getProjectPath(inputs.id) .getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.LABEL_NOT_FOUND); .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 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); label = await sails.helpers.labels.deleteOne(label, this.req);
if (!label) { if (!label) {
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
LABEL_NOT_FOUND: { LABEL_NOT_FOUND: {
labelNotFound: 'Label not found', labelNotFound: 'Label not found',
}, },
@@ -24,6 +27,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
labelNotFound: { labelNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -36,12 +42,19 @@ module.exports = {
.getProjectPath(inputs.id) .getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.LABEL_NOT_FOUND); .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 throw Errors.LABEL_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['name', 'color']); const values = _.pick(inputs, ['name', 'color']);
label = await sails.helpers.labels.updateOne(label, values, this.req); label = await sails.helpers.labels.updateOne(label, values, this.req);
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
BOARD_NOT_FOUND: { BOARD_NOT_FOUND: {
boardNotFound: 'Board not found', boardNotFound: 'Board not found',
}, },
@@ -22,6 +25,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
boardNotFound: { boardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -34,12 +40,19 @@ module.exports = {
.getProjectPath(inputs.boardId) .getProjectPath(inputs.boardId)
.intercept('pathNotFound', () => Errors.BOARD_NOT_FOUND); .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 throw Errors.BOARD_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['position', 'name']); const values = _.pick(inputs, ['position', 'name']);
const list = await sails.helpers.lists.createOne(values, board, this.req); const list = await sails.helpers.lists.createOne(values, board, this.req);
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
LIST_NOT_FOUND: { LIST_NOT_FOUND: {
listNotFound: 'List not found', listNotFound: 'List not found',
}, },
@@ -14,6 +17,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
listNotFound: { listNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -26,12 +32,19 @@ module.exports = {
.getProjectPath(inputs.id) .getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); .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 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); list = await sails.helpers.lists.deleteOne(list, this.req);
if (!list) { if (!list) {
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
LIST_NOT_FOUND: { LIST_NOT_FOUND: {
listNotFound: 'List not found', listNotFound: 'List not found',
}, },
@@ -21,6 +24,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
listNotFound: { listNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -33,12 +39,19 @@ module.exports = {
.getProjectPath(inputs.id) .getProjectPath(inputs.id)
.intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); .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 throw Errors.LIST_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['position', 'name']); const values = _.pick(inputs, ['position', 'name']);
list = await sails.helpers.lists.updateOne(list, values, this.req); 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 = { const Errors = {
PROJECT_NOT_FOUND: { PROJECT_NOT_FOUND: {
projectNotFound: '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 = { module.exports = {
@@ -17,6 +27,12 @@ module.exports = {
projectNotFound: { projectNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
noFileWasUploaded: {
responseType: 'unprocessableEntity',
},
fileIsNotImage: {
responseType: 'unprocessableEntity',
},
uploadError: { uploadError: {
responseType: 'unprocessableEntity', responseType: 'unprocessableEntity',
}, },
@@ -37,32 +53,52 @@ module.exports = {
throw Errors.PROJECT_NOT_FOUND; // Forbidden throw Errors.PROJECT_NOT_FOUND; // Forbidden
} }
this.req const upload = util.promisify((options, callback) =>
.file('file') this.req.file('file').upload(options, (error, files) => callback(error, files)),
.upload(sails.helpers.utils.createProjectBackgroundImageReceiver(), async (error, files) => { );
if (error) {
return exits.uploadError(error.message);
}
if (files.length === 0) { let files;
return exits.uploadError('No file was uploaded'); try {
} files = await upload({
saveAs: uuid(),
project = await sails.helpers.projects.updateOne( maxBytes: null,
project,
{
backgroundImageDirname: files[0].extra.dirname,
},
this.req,
);
if (!project) {
throw Errors.PROJECT_NOT_FOUND;
}
return exits.success({
item: project.toJSON(),
});
}); });
} 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,
});
}, },
}; };
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
CARD_NOT_FOUND: { CARD_NOT_FOUND: {
cardNotFound: 'Card not found', cardNotFound: 'Card not found',
}, },
@@ -25,6 +28,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
cardNotFound: { cardNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -37,12 +43,19 @@ module.exports = {
.getProjectPath(inputs.cardId) .getProjectPath(inputs.cardId)
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); .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 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 values = _.pick(inputs, ['position', 'name', 'isCompleted']);
const task = await sails.helpers.tasks.createOne(values, card, this.req); const task = await sails.helpers.tasks.createOne(values, card, this.req);
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
TASK_NOT_FOUND: { TASK_NOT_FOUND: {
taskNotFound: 'Task not found', taskNotFound: 'Task not found',
}, },
@@ -14,6 +17,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
taskNotFound: { taskNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -29,12 +35,19 @@ module.exports = {
let { task } = path; let { task } = path;
const { board } = 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 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); task = await sails.helpers.tasks.deleteOne(task, board, this.req);
if (!task) { if (!task) {
+15 -2
View File
@@ -1,4 +1,7 @@
const Errors = { const Errors = {
NOT_ENOUGH_RIGHTS: {
notEnoughRights: 'Not enough rights',
},
TASK_NOT_FOUND: { TASK_NOT_FOUND: {
taskNotFound: 'Task not found', taskNotFound: 'Task not found',
}, },
@@ -24,6 +27,9 @@ module.exports = {
}, },
exits: { exits: {
notEnoughRights: {
responseType: 'forbidden',
},
taskNotFound: { taskNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
@@ -39,12 +45,19 @@ module.exports = {
let { task } = path; let { task } = path;
const { board } = 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 throw Errors.TASK_NOT_FOUND; // Forbidden
} }
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const values = _.pick(inputs, ['position', 'name', 'isCompleted']); const values = _.pick(inputs, ['position', 'name', 'isCompleted']);
task = await sails.helpers.tasks.updateOne(task, values, board, this.req); task = await sails.helpers.tasks.updateOne(task, values, board, this.req);
+62 -26
View File
@@ -1,7 +1,17 @@
const util = require('util');
const rimraf = require('rimraf');
const { v4: uuid } = require('uuid');
const Errors = { const Errors = {
USER_NOT_FOUND: { USER_NOT_FOUND: {
userNotFound: '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 = { module.exports = {
@@ -17,6 +27,12 @@ module.exports = {
userNotFound: { userNotFound: {
responseType: 'notFound', responseType: 'notFound',
}, },
noFileWasUploaded: {
responseType: 'unprocessableEntity',
},
fileIsNotImage: {
responseType: 'unprocessableEntity',
},
uploadError: { uploadError: {
responseType: 'unprocessableEntity', responseType: 'unprocessableEntity',
}, },
@@ -38,33 +54,53 @@ module.exports = {
user = currentUser; user = currentUser;
} }
this.req const upload = util.promisify((options, callback) =>
.file('file') this.req.file('file').upload(options, (error, files) => callback(error, files)),
.upload(sails.helpers.utils.createUserAvatarReceiver(), async (error, files) => { );
if (error) {
return exits.uploadError(error.message);
}
if (files.length === 0) { let files;
return exits.uploadError('No file was uploaded'); try {
} files = await upload({
saveAs: uuid(),
user = await sails.helpers.users.updateOne( maxBytes: null,
user,
{
avatarDirname: files[0].extra.dirname,
},
currentUser,
this.req,
);
if (!user) {
throw Errors.USER_NOT_FOUND;
}
return exits.success({
item: user.toJSON(),
});
}); });
} 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,
});
}, },
}; };
@@ -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 = { module.exports = {
inputs: { inputs: {
values: {
type: 'json',
required: true,
},
user: { user: {
type: 'ref', type: 'ref',
required: true, required: true,
@@ -18,7 +22,16 @@ module.exports = {
}, },
async fn(inputs) { 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({ const boardMembership = await BoardMembership.create({
...inputs.values,
boardId: inputs.board.id, boardId: inputs.board.id,
userId: inputs.user.id, userId: inputs.user.id,
}) })
@@ -0,0 +1,46 @@
module.exports = {
inputs: {
record: {
type: 'ref',
required: true,
},
values: {
type: 'json',
required: true,
},
request: {
type: 'ref',
},
},
async fn(inputs) {
const role = inputs.values.role || inputs.record.role;
if (role === BoardMembership.Roles.EDITOR) {
inputs.values.canComment = null; // eslint-disable-line no-param-reassign
} else if (role === BoardMembership.Roles.VIEWER) {
const canComment = _.isUndefined(inputs.values.canComment)
? inputs.record.canComment
: inputs.values.canComment;
if (_.isNull(canComment)) {
inputs.values.canComment = false; // eslint-disable-line no-param-reassign
}
}
const boardMembership = await BoardMembership.updateOne(inputs.record.id).set(inputs.values);
if (boardMembership) {
sails.sockets.broadcast(
`board:${boardMembership.boardId}`,
'boardMembershipUpdate',
{
item: boardMembership,
},
inputs.request,
);
}
return boardMembership;
},
};
+1
View File
@@ -57,6 +57,7 @@ module.exports = {
const boardMembership = await BoardMembership.create({ const boardMembership = await BoardMembership.create({
boardId: board.id, boardId: board.id,
userId: inputs.user.id, userId: inputs.user.id,
role: BoardMembership.Roles.EDITOR,
}).fetch(); }).fetch();
managerUserIds.forEach((userId) => { managerUserIds.forEach((userId) => {
@@ -0,0 +1,63 @@
const fs = require('fs');
const path = require('path');
const rimraf = require('rimraf');
const { v4: uuid } = require('uuid');
const sharp = require('sharp');
module.exports = {
inputs: {
file: {
type: 'json',
required: true,
},
},
exits: {
fileIsNotImage: {},
},
async fn(inputs) {
const image = sharp(inputs.file.fd);
try {
await image.metadata();
} catch (error) {
throw 'fileIsNotImage';
}
const dirname = uuid();
const rootPath = path.join(sails.config.custom.projectBackgroundImagesPath, dirname);
fs.mkdirSync(rootPath);
try {
await image.jpeg().toFile(path.join(rootPath, 'original.jpg'));
await image
.resize(336, 200)
.jpeg({
quality: 100,
chromaSubsampling: '4:4:4',
})
.toFile(path.join(rootPath, 'cover-336.jpg'));
} catch (error1) {
try {
rimraf.sync(rootPath);
} catch (error2) {
console.warn(error2.stack); // eslint-disable-line no-console
}
throw 'fileIsNotImage';
}
try {
rimraf.sync(inputs.file.fd);
} catch (error) {
console.warn(error.stack); // eslint-disable-line no-console
}
return {
dirname,
};
},
};
@@ -0,0 +1,68 @@
const fs = require('fs');
const path = require('path');
const rimraf = require('rimraf');
const { v4: uuid } = require('uuid');
const sharp = require('sharp');
module.exports = {
inputs: {
file: {
type: 'json',
required: true,
},
},
exits: {
fileIsNotImage: {},
},
async fn(inputs) {
const image = sharp(inputs.file.fd);
try {
await image.metadata();
} catch (error) {
throw 'fileIsNotImage';
}
const dirname = uuid();
const rootPath = path.join(sails.config.custom.userAvatarsPath, dirname);
fs.mkdirSync(rootPath);
try {
await image
.jpeg({
quality: 100,
chromaSubsampling: '4:4:4',
})
.toFile(path.join(rootPath, 'original.jpg'));
await image
.resize(100, 100)
.jpeg({
quality: 100,
chromaSubsampling: '4:4:4',
})
.toFile(path.join(rootPath, 'square-100.jpg'));
} catch (error1) {
try {
rimraf.sync(rootPath);
} catch (error2) {
console.warn(error2.stack); // eslint-disable-line no-console
}
throw 'fileIsNotImage';
}
try {
rimraf.sync(inputs.file.fd);
} catch (error) {
console.warn(error.stack); // eslint-disable-line no-console
}
return {
dirname,
};
},
};
@@ -1,102 +0,0 @@
const fs = require('fs');
const path = require('path');
const util = require('util');
const stream = require('stream');
const streamToArray = require('stream-to-array');
const filenamify = require('filenamify');
const { v4: uuid } = require('uuid');
const sharp = require('sharp');
const writeFile = util.promisify(fs.writeFile);
module.exports = {
sync: true,
fn() {
const receiver = stream.Writable({
objectMode: true,
});
let firstFileHandled = false;
// eslint-disable-next-line no-underscore-dangle
receiver._write = async (file, receiverEncoding, done) => {
if (firstFileHandled) {
file.pipe(new stream.Writable());
return done();
}
firstFileHandled = true;
const buffer = await streamToArray(file).then((parts) =>
Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part)))),
);
try {
const dirname = uuid();
// FIXME: https://github.com/sindresorhus/filenamify/issues/13
const filename = filenamify(file.filename);
const rootPath = path.join(sails.config.custom.attachmentsPath, dirname);
fs.mkdirSync(rootPath);
await writeFile(path.join(rootPath, filename), buffer);
const image = sharp(buffer);
let metadata;
try {
metadata = await image.metadata();
} catch (error) {} // eslint-disable-line no-empty
const extra = {
dirname,
name: file.filename,
};
if (metadata && !['svg', 'pdf'].includes(metadata.format)) {
let cover256Buffer;
if (metadata.height > metadata.width) {
cover256Buffer = await image
.resize(256, 320)
.jpeg({
quality: 100,
chromaSubsampling: '4:4:4',
})
.toBuffer();
} else {
cover256Buffer = await image
.resize({
width: 256,
})
.jpeg({
quality: 100,
chromaSubsampling: '4:4:4',
})
.toBuffer();
}
const thumbnailsPath = path.join(rootPath, 'thumbnails');
fs.mkdirSync(thumbnailsPath);
await writeFile(path.join(thumbnailsPath, 'cover-256.jpg'), cover256Buffer);
extra.image = _.pick(metadata, ['width', 'height']);
} else {
extra.image = null;
}
Object.assign(file, {
filename,
extra,
});
return done();
} catch (error) {
return done(error);
}
};
return receiver;
},
};
@@ -1,65 +0,0 @@
const fs = require('fs');
const path = require('path');
const util = require('util');
const stream = require('stream');
const streamToArray = require('stream-to-array');
const { v4: uuid } = require('uuid');
const sharp = require('sharp');
const writeFile = util.promisify(fs.writeFile);
module.exports = {
sync: true,
fn() {
const receiver = stream.Writable({
objectMode: true,
});
let firstFileHandled = false;
// eslint-disable-next-line no-underscore-dangle
receiver._write = async (file, receiverEncoding, done) => {
if (firstFileHandled) {
file.pipe(new stream.Writable());
return done();
}
firstFileHandled = true;
const buffer = await streamToArray(file).then((parts) =>
Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part)))),
);
try {
const originalBuffer = await sharp(buffer).jpeg().toBuffer();
const cover336Buffer = await sharp(buffer)
.resize(336, 200)
.jpeg({
quality: 100,
chromaSubsampling: '4:4:4',
})
.toBuffer();
const dirname = uuid();
const rootPath = path.join(sails.config.custom.projectBackgroundImagesPath, dirname);
fs.mkdirSync(rootPath);
await writeFile(path.join(rootPath, 'original.jpg'), originalBuffer);
await writeFile(path.join(rootPath, 'cover-336.jpg'), cover336Buffer);
// eslint-disable-next-line no-param-reassign
file.extra = {
dirname,
};
return done();
} catch (error) {
return done(error);
}
};
return receiver;
},
};
@@ -1,70 +0,0 @@
const fs = require('fs');
const path = require('path');
const util = require('util');
const stream = require('stream');
const streamToArray = require('stream-to-array');
const { v4: uuid } = require('uuid');
const sharp = require('sharp');
const writeFile = util.promisify(fs.writeFile);
module.exports = {
sync: true,
fn() {
const receiver = stream.Writable({
objectMode: true,
});
let firstFileHandled = false;
// eslint-disable-next-line no-underscore-dangle
receiver._write = async (file, receiverEncoding, done) => {
if (firstFileHandled) {
file.pipe(new stream.Writable());
return done();
}
firstFileHandled = true;
const buffer = await streamToArray(file).then((parts) =>
Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part)))),
);
try {
const originalBuffer = await sharp(buffer)
.jpeg({
quality: 100,
chromaSubsampling: '4:4:4',
})
.toBuffer();
const square100Buffer = await sharp(buffer)
.resize(100, 100)
.jpeg({
quality: 100,
chromaSubsampling: '4:4:4',
})
.toBuffer();
const dirname = uuid();
const rootPath = path.join(sails.config.custom.userAvatarsPath, dirname);
fs.mkdirSync(rootPath);
await writeFile(path.join(rootPath, 'original.jpg'), originalBuffer);
await writeFile(path.join(rootPath, 'square-100.jpg'), square100Buffer);
// eslint-disable-next-line no-param-reassign
file.extra = {
dirname,
};
return done();
} catch (error) {
return done(error);
}
};
return receiver;
},
};
+18
View File
@@ -5,12 +5,30 @@
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models * @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
*/ */
const Roles = {
EDITOR: 'editor',
VIEWER: 'viewer',
};
module.exports = { module.exports = {
Roles,
attributes: { attributes: {
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗ // ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗ // ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝ // ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
role: {
type: 'string',
isIn: Object.values(Roles),
required: true,
},
canComment: {
type: 'boolean',
allowNull: true,
columnName: 'can_comment',
},
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗ // ║╣ ║║║╠╩╗║╣ ║║╚═╗
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝ // ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
+18 -1
View File
@@ -21,6 +21,8 @@
const url = require('url'); const url = require('url');
const { customLogger } = require('../../utils/logger');
module.exports = { module.exports = {
/** /**
* *
@@ -244,7 +246,22 @@ module.exports = {
*/ */
log: { log: {
level: 'debug', /**
* Passthrough plain log message(s) to
* custom Winston console and file logger.
*
* Note that Winston's log levels override Sails' log levels.
* Refer: https://github.com/winstonjs/winston#logging
*/
inspect: false,
custom: customLogger,
/**
* Removes the Sail.js init success logs (ASCII ship art).
*/
noShip: true,
}, },
http: { http: {
+1
View File
@@ -37,6 +37,7 @@ module.exports.routes = {
'DELETE /api/boards/:id': 'boards/delete', 'DELETE /api/boards/:id': 'boards/delete',
'POST /api/boards/:boardId/memberships': 'board-memberships/create', 'POST /api/boards/:boardId/memberships': 'board-memberships/create',
'PATCH /api/board-memberships/:id': 'board-memberships/update',
'DELETE /api/board-memberships/:id': 'board-memberships/delete', 'DELETE /api/board-memberships/:id': 'board-memberships/delete',
'POST /api/boards/:boardId/labels': 'labels/create', 'POST /api/boards/:boardId/labels': 'labels/create',
@@ -1,11 +1,11 @@
module.exports.up = async (knex) => module.exports.up = (knex) =>
knex.schema.table('user_account', (table) => { knex.schema.table('user_account', (table) => {
/* Columns */ /* Columns */
table.text('language'); table.text('language');
}); });
module.exports.down = async (knex) => module.exports.down = (knex) =>
knex.schema.table('user_account', (table) => { knex.schema.table('user_account', (table) => {
table.dropColumn('language'); table.dropColumn('language');
}); });
@@ -1,11 +1,11 @@
module.exports.up = async (knex) => module.exports.up = (knex) =>
knex.schema.table('user_account', (table) => { knex.schema.table('user_account', (table) => {
/* Columns */ /* Columns */
table.timestamp('password_changed_at', true); table.timestamp('password_changed_at', true);
}); });
module.exports.down = async (knex) => module.exports.down = (knex) =>
knex.schema.table('user_account', (table) => { knex.schema.table('user_account', (table) => {
table.dropColumn('password_changed_at'); table.dropColumn('password_changed_at');
}); });
@@ -0,0 +1,18 @@
module.exports.up = async (knex) => {
await knex.schema.table('board_membership', (table) => {
/* Columns */
table.text('role').notNullable().defaultTo('editor');
table.boolean('can_comment');
});
return knex.schema.alterTable('board_membership', (table) => {
table.text('role').notNullable().alter();
});
};
module.exports.down = (knex) =>
knex.schema.table('board_membership', (table) => {
table.dropColumn('role');
table.dropColumn('can_comment');
});
+368 -31
View File
@@ -14,6 +14,7 @@
"knex": "^2.2.0", "knex": "^2.2.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"move-file": "^2.1.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sails": "^1.5.2", "sails": "^1.5.2",
"sails-hook-orm": "^4.0.1", "sails-hook-orm": "^4.0.1",
@@ -22,7 +23,8 @@
"sharp": "^0.30.7", "sharp": "^0.30.7",
"stream-to-array": "^2.3.0", "stream-to-array": "^2.3.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"validator": "^13.7.0" "validator": "^13.7.0",
"winston": "^3.8.1"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.3.6", "chai": "^4.3.6",
@@ -40,6 +42,24 @@
"node": "^12.10" "node": "^12.10"
} }
}, },
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/@dabh/diagnostics": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
"dependencies": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
@@ -901,6 +921,37 @@
"node": ">=0.1.90" "node": ">=0.1.90"
} }
}, },
"node_modules/colorspace": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
"dependencies": {
"color": "^3.1.3",
"text-hex": "1.0.x"
}
},
"node_modules/colorspace/node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"dependencies": {
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"node_modules/colorspace/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/colorspace/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1402,6 +1453,11 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
}, },
"node_modules/enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -2223,6 +2279,11 @@
"pend": "~1.2.0" "pend": "~1.2.0"
} }
}, },
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -2406,6 +2467,11 @@
"@sailshq/lodash": "^3.10.2" "@sailshq/lodash": "^3.10.2"
} }
}, },
"node_modules/fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"node_modules/for-in": { "node_modules/for-in": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -3380,6 +3446,17 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-string": { "node_modules/is-string": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
@@ -3650,6 +3727,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
},
"node_modules/levn": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -3777,6 +3859,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/logform": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz",
"integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==",
"dependencies": {
"@colors/colors": "1.5.0",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"safe-stable-stringify": "^2.3.1",
"triple-beam": "^1.3.0"
}
},
"node_modules/loupe": { "node_modules/loupe": {
"version": "2.3.4", "version": "2.3.4",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz",
@@ -4305,6 +4399,28 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/move-file": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/move-file/-/move-file-2.1.0.tgz",
"integrity": "sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA==",
"dependencies": {
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=10.17"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/move-file/node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"engines": {
"node": ">=8"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -4608,6 +4724,14 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"dependencies": {
"fn.name": "1.x.x"
}
},
"node_modules/opn": { "node_modules/opn": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
@@ -5043,6 +5167,35 @@
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
"integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==" "integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw=="
}, },
"node_modules/prompt/node_modules/winston": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz",
"integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==",
"dependencies": {
"async": "^3.2.3",
"colors": "1.0.x",
"cycle": "1.0.x",
"eyes": "0.1.x",
"isstream": "0.1.x",
"stack-trace": "0.0.x"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/prompt/node_modules/winston/node_modules/async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
},
"node_modules/prompt/node_modules/winston/node_modules/colors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
"integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -5507,6 +5660,14 @@
} }
] ]
}, },
"node_modules/safe-stable-stringify": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz",
"integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -6913,6 +7074,11 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"node_modules/text-table": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -7008,6 +7174,11 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/triple-beam": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
},
"node_modules/tsconfig-paths": { "node_modules/tsconfig-paths": {
"version": "3.14.1", "version": "3.14.1",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
@@ -7623,19 +7794,36 @@
} }
}, },
"node_modules/winston": { "node_modules/winston": {
"version": "2.4.6", "version": "3.8.1",
"resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.1.tgz",
"integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==", "integrity": "sha512-r+6YAiCR4uI3N8eQNOg8k3P3PqwAm20cLKlzVD9E66Ch39+LZC+VH1UKf9JemQj2B3QoUHfKD7Poewn0Pr3Y1w==",
"dependencies": { "dependencies": {
"@dabh/diagnostics": "^2.0.2",
"async": "^3.2.3", "async": "^3.2.3",
"colors": "1.0.x", "is-stream": "^2.0.0",
"cycle": "1.0.x", "logform": "^2.4.0",
"eyes": "0.1.x", "one-time": "^1.0.0",
"isstream": "0.1.x", "readable-stream": "^3.4.0",
"stack-trace": "0.0.x" "safe-stable-stringify": "^2.3.1",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.5.0"
}, },
"engines": { "engines": {
"node": ">= 0.10.0" "node": ">= 12.0.0"
}
},
"node_modules/winston-transport": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
"integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
"dependencies": {
"logform": "^2.3.2",
"readable-stream": "^3.6.0",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 6.4.0"
} }
}, },
"node_modules/winston/node_modules/async": { "node_modules/winston/node_modules/async": {
@@ -7643,14 +7831,6 @@
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
}, },
"node_modules/winston/node_modules/colors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
"integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
@@ -7807,6 +7987,21 @@
} }
}, },
"dependencies": { "dependencies": {
"@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="
},
"@dabh/diagnostics": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
"requires": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"@eslint/eslintrc": { "@eslint/eslintrc": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
@@ -8473,6 +8668,39 @@
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
}, },
"colorspace": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
"requires": {
"color": "^3.1.3",
"text-hex": "1.0.x"
},
"dependencies": {
"color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"requires": {
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
}
}
},
"combined-stream": { "combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -8874,6 +9102,11 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
}, },
"enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
"encodeurl": { "encodeurl": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -9548,6 +9781,11 @@
"pend": "~1.2.0" "pend": "~1.2.0"
} }
}, },
"fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
},
"file-entry-cache": { "file-entry-cache": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -9690,6 +9928,11 @@
"@sailshq/lodash": "^3.10.2" "@sailshq/lodash": "^3.10.2"
} }
}, },
"fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"for-in": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -10407,6 +10650,11 @@
"call-bind": "^1.0.2" "call-bind": "^1.0.2"
} }
}, },
"is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
},
"is-string": { "is-string": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
@@ -10605,6 +10853,11 @@
} }
} }
}, },
"kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
},
"levn": { "levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -10716,6 +10969,18 @@
"is-unicode-supported": "^0.1.0" "is-unicode-supported": "^0.1.0"
} }
}, },
"logform": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz",
"integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==",
"requires": {
"@colors/colors": "1.5.0",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"safe-stable-stringify": "^2.3.1",
"triple-beam": "^1.3.0"
}
},
"loupe": { "loupe": {
"version": "2.3.4", "version": "2.3.4",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz",
@@ -11128,6 +11393,21 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
}, },
"move-file": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/move-file/-/move-file-2.1.0.tgz",
"integrity": "sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA==",
"requires": {
"path-exists": "^4.0.0"
},
"dependencies": {
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
}
}
},
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -11345,6 +11625,14 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"requires": {
"fn.name": "1.x.x"
}
},
"opn": { "opn": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz",
@@ -11669,6 +11957,31 @@
"version": "0.9.2", "version": "0.9.2",
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
"integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==" "integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw=="
},
"winston": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz",
"integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==",
"requires": {
"async": "^3.2.3",
"colors": "1.0.x",
"cycle": "1.0.x",
"eyes": "0.1.x",
"isstream": "0.1.x",
"stack-trace": "0.0.x"
},
"dependencies": {
"async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
},
"colors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
"integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw=="
}
}
} }
} }
}, },
@@ -12021,6 +12334,11 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}, },
"safe-stable-stringify": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz",
"integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg=="
},
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -13206,6 +13524,11 @@
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz",
"integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==" "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="
}, },
"text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"text-table": { "text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -13281,6 +13604,11 @@
} }
} }
}, },
"triple-beam": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
},
"tsconfig-paths": { "tsconfig-paths": {
"version": "3.14.1", "version": "3.14.1",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
@@ -13780,30 +14108,39 @@
"integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==" "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg=="
}, },
"winston": { "winston": {
"version": "2.4.6", "version": "3.8.1",
"resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.1.tgz",
"integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==", "integrity": "sha512-r+6YAiCR4uI3N8eQNOg8k3P3PqwAm20cLKlzVD9E66Ch39+LZC+VH1UKf9JemQj2B3QoUHfKD7Poewn0Pr3Y1w==",
"requires": { "requires": {
"@dabh/diagnostics": "^2.0.2",
"async": "^3.2.3", "async": "^3.2.3",
"colors": "1.0.x", "is-stream": "^2.0.0",
"cycle": "1.0.x", "logform": "^2.4.0",
"eyes": "0.1.x", "one-time": "^1.0.0",
"isstream": "0.1.x", "readable-stream": "^3.4.0",
"stack-trace": "0.0.x" "safe-stable-stringify": "^2.3.1",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.5.0"
}, },
"dependencies": { "dependencies": {
"async": { "async": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
},
"colors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
"integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw=="
} }
} }
}, },
"winston-transport": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
"integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
"requires": {
"logform": "^2.3.2",
"readable-stream": "^3.6.0",
"triple-beam": "^1.3.0"
}
},
"word-wrap": { "word-wrap": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",

Some files were not shown because too many files have changed in this diff Show More