Compare commits

..
34 Commits
Author SHA1 Message Date
Maksim Eltyshev a3f33d0361 fix: Fix core locale loading 2022-07-24 17:13:28 +02:00
Maksim Eltyshev fe62180cda chore: Update dependencies 2022-07-24 17:04:26 +02:00
Maksim Eltyshev 58abd0691f chore: Update version 2022-07-21 11:42:56 +02:00
Maksim Eltyshev ad67b9ba24 feat: Add tasks reordering
Closes #50, closes #232
2022-07-21 11:31:05 +02:00
Maksim Eltyshev ddf9a32ea9 chore: Update version 2022-06-24 17:20:44 +02:00
Maksim Eltyshev e01a2eafba fix: Fix crash when uploading attachment 2022-06-24 17:17:42 +02:00
Maksim Eltyshev 3b3bff1b6b fix: Allow to use endpoints via http 2022-06-24 11:59:04 +02:00
Maksim Eltyshev ecc45c7a9e chore: Update version 2022-06-22 18:19:34 +02:00
Maksim Eltyshev 3497e09703 feat: Add pdf viewer to attachments gallery 2022-06-22 17:01:35 +02:00
Maksim Eltyshev dfc52191fc chore: Update version 2022-06-22 09:09:44 +02:00
Maksim Eltyshev 6268ced4cb ref: Collapse image data into one column 2022-06-21 12:01:41 +02:00
Maksim Eltyshev e7495e0067 fix: Fix routes again 2022-06-20 18:48:13 +02:00
Maksim Eltyshev b8c065e0c8 fix: Fix routes 2022-06-20 18:46:03 +02:00
Maksim Eltyshev 86e4864d1b feat: Add gallery for attachments 2022-06-20 18:27:39 +02:00
Maksim Eltyshev 0be598ca9e chore: Update version 2022-06-17 18:17:08 +02:00
Dominik Dancs ba850801c5 feat: Add Slovak translation (#265) 2022-06-17 20:16:15 +05:00
Maksim Eltyshev c9b23652d1 feat: Add ability to edit users
Closes #60
2022-06-15 14:13:22 +02:00
Maksim Eltyshev 8490629e4c fix: Fix password edit popup styles 2022-06-13 17:33:26 +02:00
ejo090 76fcd79203 fix: Fix markdown line breaks
Closes #257
2022-06-06 17:37:43 +05:00
Maksim Eltyshev 3ce1bc0419 fix: Fix dependencies installation 2022-05-21 23:52:12 +05:00
Maksim Eltyshev 540fbfceb5 chore: Update version 2022-05-21 21:49:36 +05:00
Maksim Eltyshev 48a56648e2 chore: Update version 2022-05-21 21:30:38 +05:00
Maksim Eltyshev 6d002c311c fix: Fix Swedish language code 2022-05-21 21:29:33 +05:00
Maksim Eltyshev ede707ed24 fix: Fix dependencies installation for multi-arch images 2022-05-21 21:01:43 +05:00
André Wahlberg 81052e1ce6 feat: Add Swedish translation (#246) 2022-05-21 20:39:17 +05:00
Maksim Eltyshev 315a9a4c3d ci: Fix build-args input name 2022-05-05 02:46:30 +05:00
Maksim Eltyshev 8112bcbb84 ci: Fix build_args 2022-05-05 00:34:26 +05:00
Maksim Eltyshev fc69b7d6f4 ci: Add manual trigger to build-and-push-docker-base-image action 2022-05-05 00:18:00 +05:00
Aliaksandr Shulyak 027e911907 Improvements for docker build process (#237) 2022-05-05 00:04:19 +05:00
Maksim Eltyshev afc2a4628d chore: Update version 2022-04-30 20:34:56 +05:00
Maksim Eltyshev b0b6587eee fix: Fix attachment headers
Closes #231
2022-04-29 17:21:39 +05:00
Aliaksandr Shulyak e29c9e6dda ci: GitHub actions config for docker builds (#221) 2022-04-29 17:15:59 +05:00
Maksim Eltyshev caf9602557 chore: Update version 2022-04-28 14:23:11 +05:00
Maksim Eltyshev 445b0fc682 fix: Change way to fix application crash when deleting project 2022-04-28 14:15:36 +05:00
99 changed files with 13158 additions and 9095 deletions
@@ -0,0 +1,44 @@
name: Build and push Docker base image
on:
push:
paths:
- ./Dockerfile.base
branches:
- master
workflow_dispatch:
env:
ALPINE_VERSION: 3.15
jobs:
build-and-push-docker-base-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: Dockerfile.base
build-args: ALPINE_VERSION=${{ env.ALPINE_VERSION }}
push: true
tags: |
ghcr.io/plankanban/planka:base-latest
ghcr.io/plankanban/planka:base-${{ env.ALPINE_VERSION }}
@@ -0,0 +1,42 @@
name: Build and push Docker image
on:
release:
types: [created]
jobs:
build-and-push-docker-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set version
uses: actions/github-script@v6
id: set-version
with:
result-encoding: string
script: return context.payload.release.tag_name.replace('v', '')
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
ghcr.io/plankanban/planka:latest
ghcr.io/plankanban/planka:${{ steps.set-version.outputs.result }}
+1
View File
@@ -1,5 +1,6 @@
node_modules
docker-compose.override.yml
.idea
.vscode
.DS_Store
+3 -30
View File
@@ -10,47 +10,20 @@ RUN npm install npm@latest --global \
COPY client .
RUN npm run build
FROM node:lts-alpine
FROM ghcr.io/plankanban/planka:base-latest
WORKDIR /app
COPY server/.npmrc server/package.json server/package-lock.json ./
ARG ALPINE_VERSION=3.15
ARG VIPS_VERSION=8.12.2
RUN apk -U upgrade \
&& apk add \
bash giflib glib lcms2 libexif \
libgsf libjpeg-turbo libpng librsvg libwebp \
orc pango tiff \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
--no-cache \
&& apk add \
build-base giflib-dev glib-dev lcms2-dev libexif-dev \
libgsf-dev libjpeg-turbo-dev libpng-dev librsvg-dev libwebp-dev \
orc-dev pango-dev tiff-dev \
--virtual vips-dependencies \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
--no-cache \
&& wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.gz | tar xzC /tmp \
&& cd /tmp/vips-${VIPS_VERSION} \
&& ./configure \
&& make \
&& make install-strip \
&& cd $OLDPWD \
&& rm -rf /tmp/vips-${VIPS_VERSION} \
&& npm install npm@latest --global \
RUN npm install npm@latest --global \
&& npm install --production \
&& apk del vips-dependencies --purge
COPY docker-start.sh start.sh
COPY server .
RUN chmod +x start.sh \
&& cp .env.sample .env
RUN cp .env.sample .env
COPY --from=client-builder /app/build public
COPY --from=client-builder /app/build/index.html views
+27
View File
@@ -0,0 +1,27 @@
FROM node:lts-alpine
ARG ALPINE_VERSION=3.15
ARG VIPS_VERSION=8.12.2
RUN apk -U upgrade \
&& apk add \
bash giflib glib lcms2 libexif \
libgsf libjpeg-turbo libpng librsvg libwebp \
orc pango tiff \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
--no-cache \
&& apk add \
build-base giflib-dev glib-dev lcms2-dev libexif-dev \
libgsf-dev libjpeg-turbo-dev libpng-dev librsvg-dev libwebp-dev \
orc-dev pango-dev tiff-dev \
--virtual vips-dependencies \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/community/ \
--repository https://alpine.global.ssl.fastly.net/alpine/v${ALPINE_VERSION}/main/ \
--no-cache \
&& wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.gz | tar xzC /tmp \
&& cd /tmp/vips-${VIPS_VERSION} \
&& ./configure \
&& make \
&& make install-strip \
&& rm -rf /tmp/vips-${VIPS_VERSION}
+9695 -6062
View File
File diff suppressed because it is too large Load Diff
+25 -22
View File
@@ -63,57 +63,60 @@
}
},
"dependencies": {
"classnames": "^2.2.6",
"connected-react-router": "^6.9.2",
"date-fns": "^2.28.0",
"dequal": "^2.0.2",
"classnames": "^2.3.1",
"connected-react-router": "^6.9.3",
"date-fns": "^2.29.1",
"dequal": "^2.0.3",
"history": "^4.10.1",
"i18next": "^21.6.16",
"i18next": "^21.8.14",
"i18next-browser-languagedetector": "^6.1.4",
"initials": "^3.1.2",
"js-cookie": "^3.0.1",
"lodash": "^4.17.20",
"lodash": "^4.17.21",
"node-sass": "^7.0.1",
"photoswipe": "^5.3.0",
"prop-types": "^15.8.1",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.0.0",
"react-datepicker": "^4.7.0",
"react-beautiful-dnd": "^13.1.0",
"react-datepicker": "^4.8.0",
"react-dom": "^17.0.2",
"react-dropzone": "^12.0.5",
"react-i18next": "^11.16.6",
"react-dropzone": "^14.2.2",
"react-i18next": "^11.18.1",
"react-input-mask": "^2.0.4",
"react-markdown": "^8.0.2",
"react-markdown": "^8.0.3",
"react-photoswipe-gallery": "^2.2.1",
"react-redux": "^7.2.8",
"react-router-dom": "^5.3.1",
"react-scripts": "5.0.1",
"react-textarea-autosize": "^8.3.3",
"redux": "^4.1.2",
"react-textarea-autosize": "^8.3.4",
"redux": "^4.2.0",
"redux-logger": "^3.0.6",
"redux-orm": "^0.16.2",
"redux-saga": "^1.1.3",
"remark-breaks": "^3.0.2",
"remark-gfm": "^3.0.1",
"reselect": "^4.1.5",
"reselect": "^4.1.6",
"sails.io.js": "^1.2.1",
"semantic-ui-react": "^2.1.2",
"semantic-ui-react": "^2.1.3",
"socket.io-client": "^2.3.1",
"validator": "^13.7.0",
"whatwg-fetch": "^3.5.0"
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
"babel-preset-airbnb": "^5.0.0",
"chai": "^4.3.6",
"enzyme": "^3.11.0",
"eslint": "^8.13.0",
"eslint": "^8.20.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0",
"jest-enzyme": "^7.1.2",
"prettier": "2.6.2",
"prettier": "2.7.1",
"react-test-renderer": "^17.0.2"
}
}
+8
View File
@@ -29,6 +29,14 @@ export const handleTaskUpdate = (task) => ({
},
});
export const moveTask = (id, index) => ({
type: EntryActionTypes.TASK_MOVE,
payload: {
id,
index,
},
});
export const deleteTask = (id) => ({
type: EntryActionTypes.TASK_DELETE,
payload: {
+45
View File
@@ -41,6 +41,14 @@ export const handleUserUpdate = (user) => ({
},
});
export const updateUserEmail = (id, data) => ({
type: EntryActionTypes.USER_EMAIL_UPDATE,
payload: {
id,
data,
},
});
export const updateCurrentUserEmail = (data) => ({
type: EntryActionTypes.CURRENT_USER_EMAIL_UPDATE,
payload: {
@@ -48,11 +56,26 @@ export const updateCurrentUserEmail = (data) => ({
},
});
export const clearUserEmailUpdateError = (id) => ({
type: EntryActionTypes.USER_EMAIL_UPDATE_ERROR_CLEAR,
payload: {
id,
},
});
export const clearCurrentUserEmailUpdateError = () => ({
type: EntryActionTypes.CURRENT_USER_EMAIL_UPDATE_ERROR_CLEAR,
payload: {},
});
export const updateUserPassword = (id, data) => ({
type: EntryActionTypes.USER_PASSWORD_UPDATE,
payload: {
id,
data,
},
});
export const updateCurrentUserPassword = (data) => ({
type: EntryActionTypes.CURRENT_USER_PASSWORD_UPDATE,
payload: {
@@ -60,11 +83,26 @@ export const updateCurrentUserPassword = (data) => ({
},
});
export const clearUserPasswordUpdateError = (id) => ({
type: EntryActionTypes.USER_PASSWORD_UPDATE_ERROR_CLEAR,
payload: {
id,
},
});
export const clearCurrentUserPasswordUpdateError = () => ({
type: EntryActionTypes.CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR,
payload: {},
});
export const updateUserUsername = (id, data) => ({
type: EntryActionTypes.USER_USERNAME_UPDATE,
payload: {
id,
data,
},
});
export const updateCurrentUserUsername = (data) => ({
type: EntryActionTypes.CURRENT_USER_USERNAME_UPDATE,
payload: {
@@ -72,6 +110,13 @@ export const updateCurrentUserUsername = (data) => ({
},
});
export const clearUserUsernameUpdateError = (id) => ({
type: EntryActionTypes.USER_USERNAME_UPDATE_ERROR_CLEAR,
payload: {
id,
},
});
export const clearCurrentUserUsernameUpdateError = () => ({
type: EntryActionTypes.CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR,
payload: {},
@@ -114,6 +114,7 @@
}
.wrapper {
cursor: auto;
display: block;
margin-bottom: 8px;
}
@@ -1,6 +1,8 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Gallery, Item as GalleryItem } from 'react-photoswipe-gallery';
import { Button } from 'semantic-ui-react';
import { useToggle } from '../../../lib/hooks';
@@ -8,81 +10,152 @@ import Item from './Item';
import styles from './Attachments.module.scss';
const Attachments = React.memo(({ items, onUpdate, onDelete, onCoverUpdate }) => {
const [t] = useTranslation();
const [isOpened, toggleOpened] = useToggle();
const INITIALLY_VISIBLE = 4;
const handleToggleClick = useCallback(() => {
toggleOpened();
}, [toggleOpened]);
const Attachments = React.memo(
({ items, onUpdate, onDelete, onCoverUpdate, onGalleryOpen, onGalleryClose }) => {
const [t] = useTranslation();
const [isAllVisible, toggleAllVisible] = useToggle();
const handleCoverSelect = useCallback(
(id) => {
onCoverUpdate(id);
},
[onCoverUpdate],
);
const handleCoverSelect = useCallback(
(id) => {
onCoverUpdate(id);
},
[onCoverUpdate],
);
const handleCoverDeselect = useCallback(() => {
onCoverUpdate(null);
}, [onCoverUpdate]);
const handleCoverDeselect = useCallback(() => {
onCoverUpdate(null);
}, [onCoverUpdate]);
const handleUpdate = useCallback(
(id, data) => {
onUpdate(id, data);
},
[onUpdate],
);
const handleUpdate = useCallback(
(id, data) => {
onUpdate(id, data);
},
[onUpdate],
);
const handleDelete = useCallback(
(id) => {
onDelete(id);
},
[onDelete],
);
const handleDelete = useCallback(
(id) => {
onDelete(id);
},
[onDelete],
);
const visibleItems = isOpened ? items : items.slice(0, 4);
const handleBeforeGalleryOpen = useCallback(
(gallery) => {
onGalleryOpen();
return (
<>
{visibleItems.map((item) => (
<Item
gallery.on('destroy', () => {
onGalleryClose();
});
},
[onGalleryOpen, onGalleryClose],
);
const handleToggleAllVisibleClick = useCallback(() => {
toggleAllVisible();
}, [toggleAllVisible]);
const galleryItemsNode = items.map((item, index) => {
const isPdf = item.url && item.url.endsWith('.pdf');
let props;
if (item.image) {
props = item.image;
} else {
props = {
content: isPdf ? (
// eslint-disable-next-line jsx-a11y/alt-text
<object
data={item.url}
type="application/pdf"
className={classNames(styles.content, styles.contentPdf)}
/>
) : (
<span className={classNames(styles.content, styles.contentError)}>
{t('common.thereIsNoPreviewAvailableForThisAttachment')}
</span>
),
};
}
const isVisible = isAllVisible || index < INITIALLY_VISIBLE;
return (
<GalleryItem
{...props} // eslint-disable-line react/jsx-props-no-spreading
key={item.id}
name={item.name}
url={item.url}
coverUrl={item.coverUrl}
createdAt={item.createdAt}
isCover={item.isCover}
isPersisted={item.isPersisted}
onCoverSelect={() => handleCoverSelect(item.id)}
onCoverDeselect={handleCoverDeselect}
onUpdate={(data) => handleUpdate(item.id, data)}
onDelete={() => handleDelete(item.id)}
/>
))}
{items.length > 4 && (
<Button
fluid
content={
isOpened
? t('action.showFewerAttachments')
: t('action.showAllAttachments', {
hidden: items.length - visibleItems.length,
})
original={item.url}
caption={item.name}
>
{({ ref, open }) =>
isVisible ? (
<Item
ref={ref}
name={item.name}
url={item.url}
coverUrl={item.coverUrl}
createdAt={item.createdAt}
isCover={item.isCover}
isPersisted={item.isPersisted}
onClick={item.image || isPdf ? open : undefined}
onCoverSelect={() => handleCoverSelect(item.id)}
onCoverDeselect={handleCoverDeselect}
onUpdate={(data) => handleUpdate(item.id, data)}
onDelete={() => handleDelete(item.id)}
/>
) : (
<span ref={ref} />
)
}
className={styles.toggleButton}
onClick={handleToggleClick}
/>
)}
</>
);
});
</GalleryItem>
);
});
return (
<>
<Gallery
withCaption
withDownloadButton
options={{
showHideAnimationType: 'none',
closeTitle: '',
zoomTitle: '',
arrowPrevTitle: '',
arrowNextTitle: '',
errorMsg: '',
}}
onBeforeOpen={handleBeforeGalleryOpen}
>
{galleryItemsNode}
</Gallery>
{items.length > INITIALLY_VISIBLE && (
<Button
fluid
content={
isAllVisible
? t('action.showFewerAttachments')
: t('action.showAllAttachments', {
hidden: items.length - INITIALLY_VISIBLE,
})
}
className={styles.toggleButton}
onClick={handleToggleAllVisibleClick}
/>
)}
</>
);
},
);
Attachments.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onCoverUpdate: PropTypes.func.isRequired,
onGalleryOpen: PropTypes.func.isRequired,
onGalleryClose: PropTypes.func.isRequired,
};
export default Attachments;
@@ -1,4 +1,26 @@
:global(#app) {
.content {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
}
.contentPdf {
height: 100%;
width: 1120px;
}
.contentError {
color: #fff;
font-size: 20px;
font-weight: 700;
height: 20px;
width: 470px;
}
.toggleButton {
background: transparent;
box-shadow: none;
@@ -8,24 +8,32 @@ import EditPopup from './EditPopup';
import styles from './Item.module.scss';
const Item = React.memo(
({
name,
url,
coverUrl,
createdAt,
isCover,
isPersisted,
onCoverSelect,
onCoverDeselect,
onUpdate,
onDelete,
}) => {
const Item = React.forwardRef(
(
{
name,
url,
coverUrl,
createdAt,
isCover,
isPersisted,
onCoverSelect,
onCoverDeselect,
onClick,
onUpdate,
onDelete,
},
ref,
) => {
const [t] = useTranslation();
const handleClick = useCallback(() => {
window.open(url, '_blank');
}, [url]);
if (onClick) {
onClick();
} else {
window.open(url, '_blank');
}
}, [url, onClick]);
const handleToggleCoverClick = useCallback(
(event) => {
@@ -54,7 +62,7 @@ const Item = React.memo(
return (
/* eslint-disable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */
<div className={styles.wrapper} onClick={handleClick}>
<div ref={ref} className={styles.wrapper} onClick={handleClick}>
{/* eslint-enable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<div
@@ -133,6 +141,7 @@ Item.propTypes = {
createdAt: PropTypes.instanceOf(Date),
isCover: PropTypes.bool.isRequired,
isPersisted: PropTypes.bool.isRequired,
onClick: PropTypes.func,
onCoverSelect: PropTypes.func.isRequired,
onCoverDeselect: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
@@ -143,6 +152,7 @@ Item.defaultProps = {
url: undefined,
coverUrl: undefined,
createdAt: undefined,
onClick: undefined,
};
export default Item;
export default React.memo(Item);
+25 -2
View File
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
@@ -61,6 +61,7 @@ const CardModal = React.memo(
onLabelDelete,
onTaskCreate,
onTaskUpdate,
onTaskMove,
onTaskDelete,
onAttachmentCreate,
onAttachmentUpdate,
@@ -73,6 +74,8 @@ const CardModal = React.memo(
}) => {
const [t] = useTranslation();
const isGalleryOpened = useRef(false);
const handleNameUpdate = useCallback(
(newName) => {
onUpdate({
@@ -124,6 +127,22 @@ const CardModal = React.memo(
});
}, [isSubscribed, onUpdate]);
const handleGalleryOpen = useCallback(() => {
isGalleryOpened.current = true;
}, []);
const handleGalleryClose = useCallback(() => {
isGalleryOpened.current = false;
}, []);
const handleClose = useCallback(() => {
if (isGalleryOpened.current) {
return;
}
onClose();
}, [onClose]);
const userIds = users.map((user) => user.id);
const labelIds = labels.map((label) => label.id);
@@ -313,6 +332,7 @@ const CardModal = React.memo(
canEdit={canEdit}
onCreate={onTaskCreate}
onUpdate={onTaskUpdate}
onMove={onTaskMove}
onDelete={onTaskDelete}
/>
</div>
@@ -328,6 +348,8 @@ const CardModal = React.memo(
onUpdate={onAttachmentUpdate}
onDelete={onAttachmentDelete}
onCoverUpdate={handleCoverUpdate}
onGalleryOpen={handleGalleryOpen}
onGalleryClose={handleGalleryClose}
/>
</div>
</div>
@@ -445,7 +467,7 @@ const CardModal = React.memo(
);
return (
<Modal open closeIcon size="small" centered={false} onClose={onClose}>
<Modal open closeIcon size="small" centered={false} onClose={handleClose}>
{canEdit ? (
<AttachmentAddZone onCreate={onAttachmentCreate}>{contentNode}</AttachmentAddZone>
) : (
@@ -493,6 +515,7 @@ CardModal.propTypes = {
onLabelDelete: PropTypes.func.isRequired,
onTaskCreate: PropTypes.func.isRequired,
onTaskUpdate: PropTypes.func.isRequired,
onTaskMove: PropTypes.func.isRequired,
onTaskDelete: PropTypes.func.isRequired,
onAttachmentCreate: PropTypes.func.isRequired,
onAttachmentUpdate: PropTypes.func.isRequired,
+72 -57
View File
@@ -1,6 +1,8 @@
import React, { useCallback, useRef } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Draggable } from 'react-beautiful-dnd';
import { Button, Checkbox, Icon } from 'semantic-ui-react';
import NameEdit from './NameEdit';
@@ -8,72 +10,85 @@ import ActionsPopup from './ActionsPopup';
import styles from './Item.module.scss';
const Item = React.memo(({ name, isCompleted, isPersisted, canEdit, onUpdate, onDelete }) => {
const nameEdit = useRef(null);
const Item = React.memo(
({ id, index, name, isCompleted, isPersisted, canEdit, onUpdate, onDelete }) => {
const nameEdit = useRef(null);
const handleClick = useCallback(() => {
if (isPersisted && canEdit) {
nameEdit.current.open();
}
}, [isPersisted, canEdit]);
const handleClick = useCallback(() => {
if (isPersisted && canEdit) {
nameEdit.current.open();
}
}, [isPersisted, canEdit]);
const handleNameUpdate = useCallback(
(newName) => {
const handleNameUpdate = useCallback(
(newName) => {
onUpdate({
name: newName,
});
},
[onUpdate],
);
const handleToggleChange = useCallback(() => {
onUpdate({
name: newName,
isCompleted: !isCompleted,
});
},
[onUpdate],
);
}, [isCompleted, onUpdate]);
const handleToggleChange = useCallback(() => {
onUpdate({
isCompleted: !isCompleted,
});
}, [isCompleted, onUpdate]);
const handleNameEdit = useCallback(() => {
nameEdit.current.open();
}, []);
const handleNameEdit = useCallback(() => {
nameEdit.current.open();
}, []);
return (
<Draggable draggableId={id} index={index} isDragDisabled={!isPersisted || !canEdit}>
{({ innerRef, draggableProps, dragHandleProps }, { isDragging }) => {
const contentNode = (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...draggableProps} {...dragHandleProps} ref={innerRef} className={styles.wrapper}>
<span className={styles.checkboxWrapper}>
<Checkbox
checked={isCompleted}
disabled={!isPersisted || !canEdit}
className={styles.checkbox}
onChange={handleToggleChange}
/>
</span>
<NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}>
<div className={classNames(canEdit && styles.contentHoverable)}>
{/* eslint-disable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<span
className={classNames(styles.text, canEdit && styles.textEditable)}
onClick={handleClick}
>
{/* eslint-enable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<span className={classNames(styles.task, isCompleted && styles.taskCompleted)}>
{name}
</span>
</span>
{isPersisted && canEdit && (
<ActionsPopup onNameEdit={handleNameEdit} onDelete={onDelete}>
<Button className={classNames(styles.button, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</ActionsPopup>
)}
</div>
</NameEdit>
</div>
);
return (
<div className={styles.wrapper}>
<span className={styles.checkboxWrapper}>
<Checkbox
checked={isCompleted}
disabled={!isPersisted || !canEdit}
className={styles.checkbox}
onChange={handleToggleChange}
/>
</span>
<NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}>
<div className={classNames(canEdit && styles.contentHoverable)}>
{/* eslint-disable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<span
className={classNames(styles.text, canEdit && styles.textEditable)}
onClick={handleClick}
>
{/* eslint-enable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<span className={classNames(styles.task, isCompleted && styles.taskCompleted)}>
{name}
</span>
</span>
{isPersisted && canEdit && (
<ActionsPopup onNameEdit={handleNameEdit} onDelete={onDelete}>
<Button className={classNames(styles.button, styles.target)}>
<Icon fitted name="pencil" size="small" />
</Button>
</ActionsPopup>
)}
</div>
</NameEdit>
</div>
);
});
return isDragging ? ReactDOM.createPortal(contentNode, document.body) : contentNode;
}}
</Draggable>
);
},
);
Item.propTypes = {
id: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
isCompleted: PropTypes.bool.isRequired,
isPersisted: PropTypes.bool.isRequired,
@@ -27,7 +27,7 @@
vertical-align: top;
z-index: 2000;
line-height: 1;
height: 32px;
height: 100%;
}
.contentHoverable:hover {
@@ -69,10 +69,10 @@
.wrapper {
border-radius: 3px;
cursor: auto;
margin-left: -40px;
min-height: 32px;
position: relative;
transition: all 0.14s ease-in;
width: calc(100% + 40px);
}
}
+52 -21
View File
@@ -1,16 +1,34 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { Progress } from 'semantic-ui-react';
import { closePopup } from '../../../lib/popup';
import DroppableTypes from '../../../constants/DroppableTypes';
import Item from './Item';
import Add from './Add';
import styles from './Tasks.module.scss';
const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onDelete }) => {
const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onMove, onDelete }) => {
const [t] = useTranslation();
const handleDragStart = useCallback(() => {
closePopup();
}, []);
const handleDragEnd = useCallback(
({ draggableId, source, destination }) => {
if (!destination || source.index === destination.index) {
return;
}
onMove(draggableId, destination.index);
},
[onMove],
);
const handleUpdate = useCallback(
(id, data) => {
onUpdate(id, data);
@@ -39,26 +57,38 @@ const Tasks = React.memo(({ items, canEdit, onCreate, onUpdate, onDelete }) => {
className={styles.progress}
/>
)}
{items.map((item) => (
<Item
key={item.id}
name={item.name}
isCompleted={item.isCompleted}
isPersisted={item.isPersisted}
canEdit={canEdit}
onUpdate={(data) => handleUpdate(item.id, data)}
onDelete={() => handleDelete(item.id)}
/>
))}
{canEdit && (
<Add onCreate={onCreate}>
<button type="button" className={styles.taskButton}>
<span className={styles.taskButtonText}>
{items.length > 0 ? t('action.addAnotherTask') : t('action.addTask')}
</span>
</button>
</Add>
)}
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="tasks" type={DroppableTypes.TASK}>
{({ innerRef, droppableProps, placeholder }) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...droppableProps} ref={innerRef}>
{items.map((item, index) => (
<Item
key={item.id}
id={item.id}
index={index}
name={item.name}
isCompleted={item.isCompleted}
isPersisted={item.isPersisted}
canEdit={canEdit}
onUpdate={(data) => handleUpdate(item.id, data)}
onDelete={() => handleDelete(item.id)}
/>
))}
{placeholder}
{canEdit && (
<Add onCreate={onCreate}>
<button type="button" className={styles.taskButton}>
<span className={styles.taskButtonText}>
{items.length > 0 ? t('action.addAnotherTask') : t('action.addTask')}
</span>
</button>
</Add>
)}
</div>
)}
</Droppable>
</DragDropContext>
</>
);
});
@@ -68,6 +98,7 @@ Tasks.propTypes = {
canEdit: PropTypes.bool.isRequired,
onCreate: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
+1
View File
@@ -10,6 +10,7 @@ import CoreWrapperContainer from '../containers/CoreWrapperContainer';
import NotFound from './NotFound';
import 'react-datepicker/dist/react-datepicker.css';
import 'photoswipe/dist/photoswipe.css';
import '../lib/custom-ui/styles.css';
import '../styles.module.scss';
@@ -32,7 +32,7 @@ const createData = (timer) => {
const TimerEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose }) => {
const [t] = useTranslation();
const [data, handleFieldChange, setData] = useForm(() => createData(defaultValue));
const [isEditing, toggleEdit] = useToggle();
const [isEditing, toggleEditing] = useToggle();
const hoursField = useRef(null);
const minutesField = useRef(null);
@@ -55,10 +55,10 @@ const TimerEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose }) =
onClose();
}, [defaultValue, onUpdate, onClose]);
const handleToggleEditClick = useCallback(() => {
const handleToggleEditingClick = useCallback(() => {
setData(createData(defaultValue));
toggleEdit();
}, [defaultValue, setData, toggleEdit]);
toggleEditing();
}, [defaultValue, setData, toggleEditing]);
const handleSubmit = useCallback(() => {
const parts = {
@@ -149,7 +149,7 @@ const TimerEditStep = React.memo(({ defaultValue, onUpdate, onBack, onClose }) =
type="button"
icon={isEditing ? 'close' : 'edit'}
className={styles.iconButton}
onClick={handleToggleEditClick}
onClick={handleToggleEditingClick}
/>
</div>
{isEditing && <Button positive content={t('action.save')} />}
@@ -0,0 +1,5 @@
import { withPopup } from '../lib/popup';
import UserEmailEditStep from './UserEmailEditStep';
export default withPopup(UserEmailEditStep);
@@ -1,15 +1,15 @@
import omit from 'lodash/omit';
import isEmail from 'validator/lib/isEmail';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
import { Input, Popup } from '../../lib/custom-ui';
import { useForm } from '../../../hooks';
import { useForm } from '../../hooks';
import styles from './EmailEditPopup.module.scss';
import styles from './UserEmailEditStep.module.scss';
const createMessage = (error) => {
if (!error) {
@@ -35,8 +35,18 @@ const createMessage = (error) => {
}
};
const EmailEditStep = React.memo(
({ defaultData, email, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const UserEmailEditStep = React.memo(
({
defaultData,
email,
isSubmitting,
error,
usePasswordConfirmation,
onUpdate,
onMessageDismiss,
onBack,
onClose,
}) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
@@ -68,13 +78,13 @@ const EmailEditStep = React.memo(
return;
}
if (!cleanData.currentPassword) {
if (usePasswordConfirmation && !cleanData.currentPassword) {
currentPasswordField.current.focus();
return;
}
onUpdate(cleanData);
}, [email, onUpdate, onClose, data]);
onUpdate(usePasswordConfirmation ? cleanData : omit(cleanData, 'currentPassword'));
}, [email, usePasswordConfirmation, onUpdate, onClose, data]);
useEffect(() => {
emailField.current.select();
@@ -110,7 +120,7 @@ const EmailEditStep = React.memo(
return (
<>
<Popup.Header>
<Popup.Header onBack={onBack}>
{t('common.editEmail', {
context: 'title',
})}
@@ -138,15 +148,19 @@ const EmailEditStep = React.memo(
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
{usePasswordConfirmation && (
<>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
</>
)}
<Button
positive
content={t('action.save')}
@@ -160,18 +174,22 @@ const EmailEditStep = React.memo(
},
);
EmailEditStep.propTypes = {
UserEmailEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
email: PropTypes.string.isRequired,
isSubmitting: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
usePasswordConfirmation: PropTypes.bool,
onUpdate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
EmailEditStep.defaultProps = {
UserEmailEditStep.defaultProps = {
error: undefined,
usePasswordConfirmation: false,
onBack: undefined,
};
export default withPopup(EmailEditStep);
export default UserEmailEditStep;
@@ -0,0 +1,3 @@
import UserEmailEditStep from './UserEmailEditStep';
export default UserEmailEditStep;
@@ -5,11 +5,11 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Input } from 'semantic-ui-react';
import { useForm } from '../../../hooks';
import { useForm } from '../../hooks';
import styles from './InformationEdit.module.scss';
import styles from './UserInformationEdit.module.scss';
const InformationEdit = React.memo(({ defaultData, onUpdate }) => {
const UserInformationEdit = React.memo(({ defaultData, onUpdate }) => {
const [t] = useTranslation();
const [data, handleFieldChange] = useForm(() => ({
@@ -72,9 +72,9 @@ const InformationEdit = React.memo(({ defaultData, onUpdate }) => {
);
});
InformationEdit.propTypes = {
UserInformationEdit.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
};
export default InformationEdit;
export default UserInformationEdit;
@@ -0,0 +1,3 @@
import UserInformationEdit from './UserInformationEdit';
export default UserInformationEdit;
@@ -0,0 +1,44 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Popup } from '../lib/custom-ui';
import UserInformationEdit from './UserInformationEdit';
const UserInformationEditStep = React.memo(({ defaultData, onUpdate, onBack, onClose }) => {
const [t] = useTranslation();
const handleUpdate = useCallback(
(data) => {
onUpdate(data);
onClose();
},
[onUpdate, onClose],
);
return (
<>
<Popup.Header onBack={onBack}>
{t('common.editInformation', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<UserInformationEdit defaultData={defaultData} onUpdate={handleUpdate} />
</Popup.Content>
</>
);
});
UserInformationEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
UserInformationEditStep.defaultProps = {
onBack: undefined,
};
export default UserInformationEditStep;
@@ -0,0 +1,5 @@
import { withPopup } from '../lib/popup';
import UserPasswordEditStep from './UserPasswordEditStep';
export default withPopup(UserPasswordEditStep);
@@ -1,14 +1,14 @@
import omit from 'lodash/omit';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
import { Input, Popup } from '../../lib/custom-ui';
import { useForm } from '../../../hooks';
import { useForm } from '../../hooks';
import styles from './PasswordEditPopup.module.scss';
import styles from './UserPasswordEditStep.module.scss';
const createMessage = (error) => {
if (!error) {
@@ -29,8 +29,17 @@ const createMessage = (error) => {
}
};
const PasswordEditStep = React.memo(
({ defaultData, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const UserPasswordEditStep = React.memo(
({
defaultData,
isSubmitting,
error,
usePasswordConfirmation,
onUpdate,
onMessageDismiss,
onBack,
onClose,
}) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
@@ -52,13 +61,13 @@ const PasswordEditStep = React.memo(
return;
}
if (!data.currentPassword) {
if (usePasswordConfirmation && !data.currentPassword) {
currentPasswordField.current.focus();
return;
}
onUpdate(data);
}, [onUpdate, data]);
onUpdate(usePasswordConfirmation ? data : omit(data, 'currentPassword'));
}, [usePasswordConfirmation, onUpdate, data]);
useEffect(() => {
passwordField.current.select();
@@ -84,7 +93,7 @@ const PasswordEditStep = React.memo(
return (
<>
<Popup.Header>
<Popup.Header onBack={onBack}>
{t('common.editPassword', {
context: 'title',
})}
@@ -111,15 +120,19 @@ const PasswordEditStep = React.memo(
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
{usePasswordConfirmation && (
<>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
</>
)}
<Button
positive
content={t('action.save')}
@@ -133,17 +146,21 @@ const PasswordEditStep = React.memo(
},
);
PasswordEditStep.propTypes = {
UserPasswordEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isSubmitting: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
usePasswordConfirmation: PropTypes.bool,
onUpdate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
PasswordEditStep.defaultProps = {
UserPasswordEditStep.defaultProps = {
error: undefined,
usePasswordConfirmation: false,
onBack: undefined,
};
export default withPopup(PasswordEditStep);
export default UserPasswordEditStep;
@@ -0,0 +1,3 @@
import UserPasswordEditStep from './UserPasswordEditStep';
export default UserPasswordEditStep;
@@ -3,12 +3,12 @@ import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Divider, Header, Tab } from 'semantic-ui-react';
import InformationEdit from './InformationEdit';
import AvatarEditPopup from './AvatarEditPopup';
import UsernameEditPopup from './UsernameEditPopup';
import EmailEditPopup from './EmailEditPopup';
import PasswordEditPopup from './PasswordEditPopup';
import User from '../../User';
import UserInformationEdit from '../../UserInformationEdit';
import UserUsernameEditPopup from '../../UserUsernameEditPopup';
import UserEmailEditPopup from '../../UserEmailEditPopup';
import UserPasswordEditPopup from '../../UserPasswordEditPopup';
import styles from './AccountPane.module.scss';
@@ -52,7 +52,7 @@ const AccountPane = React.memo(
</AvatarEditPopup>
<br />
<br />
<InformationEdit
<UserInformationEdit
defaultData={{
name,
phone,
@@ -68,7 +68,8 @@ const AccountPane = React.memo(
</Header>
</Divider>
<div className={styles.action}>
<UsernameEditPopup
<UserUsernameEditPopup
usePasswordConfirmation
defaultData={usernameUpdateForm.data}
username={username}
isSubmitting={usernameUpdateForm.isSubmitting}
@@ -81,10 +82,11 @@ const AccountPane = React.memo(
context: 'title',
})}
</Button>
</UsernameEditPopup>
</UserUsernameEditPopup>
</div>
<div className={styles.action}>
<EmailEditPopup
<UserEmailEditPopup
usePasswordConfirmation
defaultData={emailUpdateForm.data}
email={email}
isSubmitting={emailUpdateForm.isSubmitting}
@@ -97,10 +99,11 @@ const AccountPane = React.memo(
context: 'title',
})}
</Button>
</EmailEditPopup>
</UserEmailEditPopup>
</div>
<div className={styles.action}>
<PasswordEditPopup
<UserPasswordEditPopup
usePasswordConfirmation
defaultData={passwordUpdateForm.data}
isSubmitting={passwordUpdateForm.isSubmitting}
error={passwordUpdateForm.error}
@@ -112,7 +115,7 @@ const AccountPane = React.memo(
context: 'title',
})}
</Button>
</PasswordEditPopup>
</UserPasswordEditPopup>
</div>
</Tab.Pane>
);
@@ -1,5 +0,0 @@
:global(#app) {
.field {
margin-bottom: 8px;
}
}
@@ -0,0 +1,5 @@
import { withPopup } from '../lib/popup';
import UserUsernameEditStep from './UserUsernameEditStep';
export default withPopup(UserUsernameEditStep);
@@ -1,15 +1,15 @@
import omit from 'lodash/omit';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { withPopup } from '../../../lib/popup';
import { Input, Popup } from '../../../lib/custom-ui';
import { useDidUpdate, usePrevious, useToggle } from '../../lib/hooks';
import { Input, Popup } from '../../lib/custom-ui';
import { useForm } from '../../../hooks';
import { isUsername } from '../../../utils/validator';
import { useForm } from '../../hooks';
import { isUsername } from '../../utils/validator';
import styles from './UsernameEditPopup.module.scss';
import styles from './UserUsernameEditStep.module.scss';
const createMessage = (error) => {
if (!error) {
@@ -35,8 +35,18 @@ const createMessage = (error) => {
}
};
const UsernameEditStep = React.memo(
({ defaultData, username, isSubmitting, error, onUpdate, onMessageDismiss, onClose }) => {
const UserUsernameEditStep = React.memo(
({
defaultData,
username,
isSubmitting,
error,
usePasswordConfirmation,
onUpdate,
onMessageDismiss,
onBack,
onClose,
}) => {
const [t] = useTranslation();
const wasSubmitting = usePrevious(isSubmitting);
@@ -58,7 +68,7 @@ const UsernameEditStep = React.memo(
username: data.username.trim() || null,
};
if (cleanData.username && !isUsername(cleanData.username)) {
if (!cleanData.username || !isUsername(cleanData.username)) {
usernameField.current.select();
return;
}
@@ -68,13 +78,13 @@ const UsernameEditStep = React.memo(
return;
}
if (!cleanData.currentPassword) {
if (usePasswordConfirmation && !cleanData.currentPassword) {
currentPasswordField.current.focus();
return;
}
onUpdate(cleanData);
}, [username, onUpdate, onClose, data]);
onUpdate(usePasswordConfirmation ? cleanData : omit(cleanData, 'currentPassword'));
}, [username, usePasswordConfirmation, onUpdate, onClose, data]);
useEffect(() => {
usernameField.current.select();
@@ -110,7 +120,7 @@ const UsernameEditStep = React.memo(
return (
<>
<Popup.Header>
<Popup.Header onBack={onBack}>
{t('common.editUsername', {
context: 'title',
})}
@@ -138,15 +148,19 @@ const UsernameEditStep = React.memo(
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
{usePasswordConfirmation && (
<>
<div className={styles.text}>{t('common.currentPassword')}</div>
<Input.Password
fluid
ref={currentPasswordField}
name="currentPassword"
value={data.currentPassword}
className={styles.field}
onChange={handleFieldChange}
/>
</>
)}
<Button
positive
content={t('action.save')}
@@ -160,19 +174,23 @@ const UsernameEditStep = React.memo(
},
);
UsernameEditStep.propTypes = {
UserUsernameEditStep.propTypes = {
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
username: PropTypes.string,
isSubmitting: PropTypes.bool.isRequired,
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
usePasswordConfirmation: PropTypes.bool,
onUpdate: PropTypes.func.isRequired,
onMessageDismiss: PropTypes.func.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
UsernameEditStep.defaultProps = {
UserUsernameEditStep.defaultProps = {
username: undefined,
error: undefined,
usePasswordConfirmation: false,
onBack: undefined,
};
export default withPopup(UsernameEditStep);
export default UserUsernameEditStep;
@@ -0,0 +1,12 @@
:global(#app) {
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}
}
@@ -0,0 +1,3 @@
import UserUsernameEditStep from './UserUsernameEditStep';
export default UserUsernameEditStep;
-56
View File
@@ -1,56 +0,0 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Radio, Table } from 'semantic-ui-react';
import DeletePopup from '../DeletePopup';
import styles from './Item.module.scss';
const Item = React.memo(({ name, username, email, isAdmin, onUpdate, onDelete }) => {
const [t] = useTranslation();
const handleIsAdminChange = useCallback(() => {
onUpdate({
isAdmin: !isAdmin,
});
}, [isAdmin, onUpdate]);
return (
<Table.Row>
<Table.Cell>{name}</Table.Cell>
<Table.Cell>{username || '-'}</Table.Cell>
<Table.Cell>{email}</Table.Cell>
<Table.Cell collapsing>
<Radio toggle checked={isAdmin} onChange={handleIsAdminChange} />
</Table.Cell>
<Table.Cell collapsing>
<DeletePopup
title={t('common.deleteUser', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisUser')}
buttonContent={t('action.deleteUser')}
onConfirm={onDelete}
>
<Button content={t('action.delete')} className={styles.button} />
</DeletePopup>
</Table.Cell>
</Table.Row>
);
});
Item.propTypes = {
name: PropTypes.string.isRequired,
username: PropTypes.string,
email: PropTypes.string.isRequired,
isAdmin: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
Item.defaultProps = {
username: undefined,
};
export default Item;
@@ -0,0 +1,181 @@
import pick from 'lodash/pick';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Menu } from 'semantic-ui-react';
import { withPopup } from '../../../lib/popup';
import { Popup } from '../../../lib/custom-ui';
import { useSteps } from '../../../hooks';
import UserInformationEditStep from '../../UserInformationEditStep';
import UserUsernameEditStep from '../../UserUsernameEditStep';
import UserEmailEditStep from '../../UserEmailEditStep';
import UserPasswordEditStep from '../../UserPasswordEditStep';
import DeleteStep from '../../DeleteStep';
import styles from './ActionsPopup.module.scss';
const StepTypes = {
EDIT_INFORMATION: 'EDIT_INFORMATION',
EDIT_USERNAME: 'EDIT_USERNAME',
EDIT_EMAIL: 'EDIT_EMAIL',
EDIT_PASSWORD: 'EDIT_PASSWORD',
DELETE: 'DELETE',
};
const ActionsStep = React.memo(
({
user,
onUpdate,
onUsernameUpdate,
onUsernameUpdateMessageDismiss,
onEmailUpdate,
onEmailUpdateMessageDismiss,
onPasswordUpdate,
onPasswordUpdateMessageDismiss,
onDelete,
onClose,
}) => {
const [t] = useTranslation();
const [step, openStep, handleBack] = useSteps();
const handleEditInformationClick = useCallback(() => {
openStep(StepTypes.EDIT_INFORMATION);
}, [openStep]);
const handleEditUsernameClick = useCallback(() => {
openStep(StepTypes.EDIT_USERNAME);
}, [openStep]);
const handleEditEmailClick = useCallback(() => {
openStep(StepTypes.EDIT_EMAIL);
}, [openStep]);
const handleEditPasswordClick = useCallback(() => {
openStep(StepTypes.EDIT_PASSWORD);
}, [openStep]);
const handleDeleteClick = useCallback(() => {
openStep(StepTypes.DELETE);
}, [openStep]);
if (step) {
switch (step.type) {
case StepTypes.EDIT_INFORMATION:
return (
<UserInformationEditStep
defaultData={pick(user, ['name', 'phone', 'organization'])}
onUpdate={onUpdate}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.EDIT_USERNAME:
return (
<UserUsernameEditStep
defaultData={user.usernameUpdateForm.data}
username={user.username}
isSubmitting={user.usernameUpdateForm.isSubmitting}
error={user.usernameUpdateForm.error}
onUpdate={onUsernameUpdate}
onMessageDismiss={onUsernameUpdateMessageDismiss}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.EDIT_EMAIL:
return (
<UserEmailEditStep
defaultData={user.emailUpdateForm.data}
email={user.email}
isSubmitting={user.emailUpdateForm.isSubmitting}
error={user.emailUpdateForm.error}
onUpdate={onEmailUpdate}
onMessageDismiss={onEmailUpdateMessageDismiss}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.EDIT_PASSWORD:
return (
<UserPasswordEditStep
defaultData={user.passwordUpdateForm.data}
isSubmitting={user.passwordUpdateForm.isSubmitting}
error={user.emailUpdateForm.error}
onUpdate={onPasswordUpdate}
onMessageDismiss={onPasswordUpdateMessageDismiss}
onBack={handleBack}
onClose={onClose}
/>
);
case StepTypes.DELETE:
return (
<DeleteStep
title={t('common.deleteUser', {
context: 'title',
})}
content={t('common.areYouSureYouWantToDeleteThisUser')}
buttonContent={t('action.deleteUser')}
onConfirm={onDelete}
onBack={handleBack}
/>
);
default:
}
}
return (
<>
<Popup.Header>
{t('common.userActions', {
context: 'title',
})}
</Popup.Header>
<Popup.Content>
<Menu secondary vertical className={styles.menu}>
<Menu.Item className={styles.menuItem} onClick={handleEditInformationClick}>
{t('action.editInformation', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
{t('action.editUsername', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditEmailClick}>
{t('action.editEmail', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditPasswordClick}>
{t('action.editPassword', {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
{t('action.deleteUser', {
context: 'title',
})}
</Menu.Item>
</Menu>
</Popup.Content>
</>
);
},
);
ActionsStep.propTypes = {
user: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onUsernameUpdate: PropTypes.func.isRequired,
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
onEmailUpdate: PropTypes.func.isRequired,
onEmailUpdateMessageDismiss: PropTypes.func.isRequired,
onPasswordUpdate: PropTypes.func.isRequired,
onPasswordUpdateMessageDismiss: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default withPopup(ActionsStep);
@@ -0,0 +1,11 @@
:global(#app) {
.menu {
margin: -7px -12px -5px;
width: calc(100% + 24px);
}
.menuItem {
margin: 0;
padding-left: 14px;
}
}
+103
View File
@@ -0,0 +1,103 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { Button, Icon, Radio, Table } from 'semantic-ui-react';
import ActionsPopup from './ActionsPopup';
import styles from './Item.module.scss';
const Item = React.memo(
({
email,
username,
name,
organization,
phone,
isAdmin,
emailUpdateForm,
passwordUpdateForm,
usernameUpdateForm,
onUpdate,
onUsernameUpdate,
onUsernameUpdateMessageDismiss,
onEmailUpdate,
onEmailUpdateMessageDismiss,
onPasswordUpdate,
onPasswordUpdateMessageDismiss,
onDelete,
}) => {
const handleIsAdminChange = useCallback(() => {
onUpdate({
isAdmin: !isAdmin,
});
}, [isAdmin, onUpdate]);
return (
<Table.Row>
<Table.Cell>{name}</Table.Cell>
<Table.Cell>{username || '-'}</Table.Cell>
<Table.Cell>{email}</Table.Cell>
<Table.Cell collapsing>
<Radio toggle checked={isAdmin} onChange={handleIsAdminChange} />
</Table.Cell>
<Table.Cell collapsing>
<ActionsPopup
user={{
email,
username,
name,
organization,
phone,
isAdmin,
emailUpdateForm,
passwordUpdateForm,
usernameUpdateForm,
}}
onUpdate={onUpdate}
onUsernameUpdate={onUsernameUpdate}
onUsernameUpdateMessageDismiss={onUsernameUpdateMessageDismiss}
onEmailUpdate={onEmailUpdate}
onEmailUpdateMessageDismiss={onEmailUpdateMessageDismiss}
onPasswordUpdate={onPasswordUpdate}
onPasswordUpdateMessageDismiss={onPasswordUpdateMessageDismiss}
onDelete={onDelete}
>
<Button className={styles.button}>
<Icon fitted name="pencil" />
</Button>
</ActionsPopup>
</Table.Cell>
</Table.Row>
);
},
);
Item.propTypes = {
email: PropTypes.string.isRequired,
username: PropTypes.string,
name: PropTypes.string.isRequired,
organization: PropTypes.string,
phone: PropTypes.string,
isAdmin: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */
emailUpdateForm: PropTypes.object.isRequired,
passwordUpdateForm: PropTypes.object.isRequired,
usernameUpdateForm: PropTypes.object.isRequired,
/* eslint-enable react/forbid-prop-types */
onUpdate: PropTypes.func.isRequired,
onUsernameUpdate: PropTypes.func.isRequired,
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
onEmailUpdate: PropTypes.func.isRequired,
onEmailUpdateMessageDismiss: PropTypes.func.isRequired,
onPasswordUpdate: PropTypes.func.isRequired,
onPasswordUpdateMessageDismiss: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
Item.defaultProps = {
username: undefined,
organization: undefined,
phone: undefined,
};
export default Item;
@@ -0,0 +1,3 @@
import Item from './Item';
export default Item;
+127 -55
View File
@@ -6,68 +6,140 @@ import { Button, Modal, Table } from 'semantic-ui-react';
import UserAddPopupContainer from '../../containers/UserAddPopupContainer';
import Item from './Item';
const UsersModal = React.memo(({ items, onUpdate, onDelete, onClose }) => {
const [t] = useTranslation();
const UsersModal = React.memo(
({
items,
onUpdate,
onUsernameUpdate,
onUsernameUpdateMessageDismiss,
onEmailUpdate,
onEmailUpdateMessageDismiss,
onPasswordUpdate,
onPasswordUpdateMessageDismiss,
onDelete,
onClose,
}) => {
const [t] = useTranslation();
const handleUpdate = useCallback(
(id, data) => {
onUpdate(id, data);
},
[onUpdate],
);
const handleUpdate = useCallback(
(id, data) => {
onUpdate(id, data);
},
[onUpdate],
);
const handleDelete = useCallback(
(id) => {
onDelete(id);
},
[onDelete],
);
const handleUsernameUpdate = useCallback(
(id, data) => {
onUsernameUpdate(id, data);
},
[onUsernameUpdate],
);
return (
<Modal open closeIcon size="large" centered={false} onClose={onClose}>
<Modal.Header>
{t('common.users', {
context: 'title',
})}
</Modal.Header>
<Modal.Content>
<Table basic="very">
<Table.Header>
<Table.Row>
<Table.HeaderCell width={4}>{t('common.name')}</Table.HeaderCell>
<Table.HeaderCell width={4}>{t('common.username')}</Table.HeaderCell>
<Table.HeaderCell width={4}>{t('common.email')}</Table.HeaderCell>
<Table.HeaderCell>{t('common.administrator')}</Table.HeaderCell>
<Table.HeaderCell />
</Table.Row>
</Table.Header>
<Table.Body>
{items.map((item) => (
<Item
key={item.id}
name={item.name}
username={item.username}
email={item.email}
isAdmin={item.isAdmin}
onUpdate={(data) => handleUpdate(item.id, data)}
onDelete={() => handleDelete(item.id)}
/>
))}
</Table.Body>
</Table>
</Modal.Content>
<Modal.Actions>
<UserAddPopupContainer>
<Button positive content={t('action.addUser')} />
</UserAddPopupContainer>
</Modal.Actions>
</Modal>
);
});
const handleUsernameUpdateMessageDismiss = useCallback(
(id) => {
onUsernameUpdateMessageDismiss(id);
},
[onUsernameUpdateMessageDismiss],
);
const handleEmailUpdate = useCallback(
(id, data) => {
onEmailUpdate(id, data);
},
[onEmailUpdate],
);
const handleEmailUpdateMessageDismiss = useCallback(
(id) => {
onEmailUpdateMessageDismiss(id);
},
[onEmailUpdateMessageDismiss],
);
const handlePasswordUpdate = useCallback(
(id, data) => {
onPasswordUpdate(id, data);
},
[onPasswordUpdate],
);
const handlePasswordUpdateMessageDismiss = useCallback(
(id) => {
onPasswordUpdateMessageDismiss(id);
},
[onPasswordUpdateMessageDismiss],
);
const handleDelete = useCallback(
(id) => {
onDelete(id);
},
[onDelete],
);
return (
<Modal open closeIcon size="large" centered={false} onClose={onClose}>
<Modal.Header>
{t('common.users', {
context: 'title',
})}
</Modal.Header>
<Modal.Content>
<Table basic="very">
<Table.Header>
<Table.Row>
<Table.HeaderCell width={4}>{t('common.name')}</Table.HeaderCell>
<Table.HeaderCell width={4}>{t('common.username')}</Table.HeaderCell>
<Table.HeaderCell width={4}>{t('common.email')}</Table.HeaderCell>
<Table.HeaderCell>{t('common.administrator')}</Table.HeaderCell>
<Table.HeaderCell />
</Table.Row>
</Table.Header>
<Table.Body>
{items.map((item) => (
<Item
key={item.id}
email={item.email}
username={item.username}
name={item.name}
organization={item.organization}
phone={item.phone}
isAdmin={item.isAdmin}
emailUpdateForm={item.emailUpdateForm}
passwordUpdateForm={item.passwordUpdateForm}
usernameUpdateForm={item.usernameUpdateForm}
onUpdate={(data) => handleUpdate(item.id, data)}
onUsernameUpdate={(data) => handleUsernameUpdate(item.id, data)}
onUsernameUpdateMessageDismiss={() => handleUsernameUpdateMessageDismiss(item.id)}
onEmailUpdate={(data) => handleEmailUpdate(item.id, data)}
onEmailUpdateMessageDismiss={() => handleEmailUpdateMessageDismiss(item.id)}
onPasswordUpdate={(data) => handlePasswordUpdate(item.id, data)}
onPasswordUpdateMessageDismiss={() => handlePasswordUpdateMessageDismiss(item.id)}
onDelete={() => handleDelete(item.id)}
/>
))}
</Table.Body>
</Table>
</Modal.Content>
<Modal.Actions>
<UserAddPopupContainer>
<Button positive content={t('action.addUser')} />
</UserAddPopupContainer>
</Modal.Actions>
</Modal>
);
},
);
UsersModal.propTypes = {
items: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
onUsernameUpdate: PropTypes.func.isRequired,
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
onEmailUpdate: PropTypes.func.isRequired,
onEmailUpdateMessageDismiss: PropTypes.func.isRequired,
onPasswordUpdate: PropTypes.func.isRequired,
onPasswordUpdateMessageDismiss: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
+2
View File
@@ -1,9 +1,11 @@
const BOARD = 'BOARD';
const LIST = 'LIST';
const CARD = 'CARD';
const TASK = 'TASK';
export default {
BOARD,
LIST,
CARD,
TASK,
};
+7
View File
@@ -31,11 +31,17 @@ export default {
USER_UPDATE: `${PREFIX}/USER_UPDATE`,
CURRENT_USER_UPDATE: `${PREFIX}/CURRENT_USER_UPDATE`,
USER_UPDATE_HANDLE: `${PREFIX}/USER_UPDATE_HANDLE`,
USER_EMAIL_UPDATE: `${PREFIX}/USER_EMAIL_UPDATE`,
CURRENT_USER_EMAIL_UPDATE: `${PREFIX}/CURRENT_USER_EMAIL_UPDATE`,
USER_EMAIL_UPDATE_ERROR_CLEAR: `${PREFIX}/USER_EMAIL_UPDATE_ERROR_CLEAR`,
CURRENT_USER_EMAIL_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_EMAIL_UPDATE_ERROR_CLEAR`,
USER_PASSWORD_UPDATE: `${PREFIX}/USER_PASSWORD_UPDATE`,
CURRENT_USER_PASSWORD_UPDATE: `${PREFIX}/CURRENT_USER_PASSWORD_UPDATE`,
USER_PASSWORD_UPDATE_ERROR_CLEAR: `${PREFIX}/USER_PASSWORD_UPDATE_ERROR_CLEAR`,
CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR`,
USER_USERNAME_UPDATE: `${PREFIX}/USER_USERNAME_UPDATE`,
CURRENT_USER_USERNAME_UPDATE: `${PREFIX}/CURRENT_USER_USERNAME_UPDATE`,
USER_USERNAME_UPDATE_ERROR_CLEAR: `${PREFIX}/USER_USERNAME_UPDATE_ERROR_CLEAR`,
CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR`,
CURRENT_USER_AVATAR_UPDATE: `${PREFIX}/CURRENT_USER_AVATAR_UPDATE`,
USER_DELETE: `${PREFIX}/USER_DELETE`,
@@ -132,6 +138,7 @@ export default {
TASK_CREATE_HANDLE: `${PREFIX}/TASK_CREATE_HANDLE`,
TASK_UPDATE: `${PREFIX}/TASK_UPDATE`,
TASK_UPDATE_HANDLE: `${PREFIX}/TASK_UPDATE_HANDLE`,
TASK_MOVE: `${PREFIX}/TASK_MOVE`,
TASK_DELETE: `${PREFIX}/TASK_DELETE`,
TASK_DELETE_HANDLE: `${PREFIX}/TASK_DELETE_HANDLE`,
@@ -32,6 +32,7 @@ import {
fetchActionsInCurrentCard,
fetchBoard,
moveCurrentCard,
moveTask,
removeLabelFromCurrentCard,
removeUserFromCurrentCard,
transferCurrentCard,
@@ -111,6 +112,7 @@ const mapDispatchToProps = (dispatch) =>
onLabelDelete: deleteLabel,
onTaskCreate: createTaskInCurrentCard,
onTaskUpdate: updateTask,
onTaskMove: moveTask,
onTaskDelete: deleteTask,
onAttachmentCreate: createAttachmentInCurrentCard,
onAttachmentUpdate: updateAttachment,
+17 -1
View File
@@ -2,7 +2,17 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { usersExceptCurrentSelector } from '../selectors';
import { closeModal, deleteUser, updateUser } from '../actions/entry';
import {
clearUserEmailUpdateError,
clearUserPasswordUpdateError,
clearUserUsernameUpdateError,
closeModal,
deleteUser,
updateUser,
updateUserEmail,
updateUserPassword,
updateUserUsername,
} from '../actions/entry';
import UsersModal from '../components/UsersModal';
const mapStateToProps = (state) => {
@@ -17,6 +27,12 @@ const mapDispatchToProps = (dispatch) =>
bindActionCreators(
{
onUpdate: updateUser,
onUsernameUpdate: updateUserUsername,
onUsernameUpdateMessageDismiss: clearUserUsernameUpdateError,
onEmailUpdate: updateUserEmail,
onEmailUpdateMessageDismiss: clearUserEmailUpdateError,
onPasswordUpdate: updateUserPassword,
onPasswordUpdateMessageDismiss: clearUserPasswordUpdateError,
onDelete: deleteUser,
onClose: closeModal,
},
+8 -10
View File
@@ -79,21 +79,19 @@ i18n
debug: process.env.NODE_ENV !== 'production',
});
i18n.loadCoreLocale = (language = i18n.resolvedLanguage) => {
i18n.loadCoreLocale = async (language = i18n.resolvedLanguage) => {
if (language === 'en') {
return;
}
import(`./locales/${language}/core`).then((module) => {
const locale = module.default;
const { default: locale } = await import(`./locales/${language}/core`);
Object.keys(locale).forEach((namespace) => {
if (namespace === 'dateFns') {
i18n.dateFns.addLocale(language, locale[namespace]);
} else {
i18n.addResourceBundle(language, namespace, locale[namespace], true, true);
}
});
Object.keys(locale).forEach((namespace) => {
if (namespace === 'dateFns') {
i18n.dateFns.addLocale(language, locale[namespace]);
} else {
i18n.addResourceBundle(language, namespace, locale[namespace], true, true);
}
});
};
@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import './Markdown.module.scss'; // FIXME: import as styles?
@@ -31,14 +32,12 @@ const Markdown = React.memo(({ linkStopPropagation, ...props }) => {
}
return (
/* eslint-disable react/jsx-props-no-spreading */
<ReactMarkdown
{...props}
{...props} // eslint-disable-line react/jsx-props-no-spreading
components={components}
remarkPlugins={[remarkGfm]}
remarkPlugins={[remarkGfm, remarkBreaks]}
className="markdown-body"
/>
/* eslint-enable react/jsx-props-no-spreading */
);
});
+1
View File
@@ -134,6 +134,7 @@ export default {
subscribeToMyOwnCardsByDefault: 'Standardmäßig meine eigenen Karten abonnieren',
taskActions_title: 'Aufgabenaktionen',
tasks: 'Aufgaben',
thereIsNoPreviewAvailableForThisAttachment: 'Für diesen Anhang ist keine Vorschau verfügbar',
time: 'Zeit',
timer: 'Timer',
title: 'Titel',
+4
View File
@@ -70,6 +70,7 @@ export default {
editBoard_title: 'Edit Board',
editDueDate_title: 'Edit Due Date',
editEmail_title: 'Edit E-mail',
editInformation_title: 'Edit Information',
editLabel_title: 'Edit Label',
editPassword_title: 'Edit Password',
editTimer_title: 'Edit Timer',
@@ -126,6 +127,8 @@ export default {
subscribeToMyOwnCardsByDefault: 'Subscribe to my own cards by default',
taskActions_title: 'Task Actions',
tasks: 'Tasks',
thereIsNoPreviewAvailableForThisAttachment:
'There is no preview available for this attachment',
time: 'Time',
timer: 'Timer',
title: 'Title',
@@ -178,6 +181,7 @@ export default {
editDueDate_title: 'Edit Due Date',
editDescription_title: 'Edit Description',
editEmail_title: 'Edit E-mail',
editInformation_title: 'Edit Information',
editPassword_title: 'Edit Password',
editTimer_title: 'Edit Timer',
editTitle_title: 'Edit Title',
+4
View File
@@ -11,6 +11,8 @@ import frLogin from './fr/login';
import jaLogin from './ja/login';
import plLogin from './pl/login';
import ruLogin from './ru/login';
import skLogin from './sk/login';
import svLogin from './sv/login';
import uzLogin from './uz/login';
import zhLogin from './zh/login';
@@ -24,6 +26,8 @@ const localePairs = [
['ja', jaLogin],
['pl', plLogin],
['ru', ruLogin],
['sk', skLogin],
['sv', svLogin],
['uz', uzLogin],
['zh', zhLogin],
];
+1
View File
@@ -122,6 +122,7 @@ export default {
subscribeToMyOwnCardsByDefault: 'По умолчанию подписаться на мои собственные карточки',
taskActions: 'Действия с задачей',
tasks: 'Задачи',
thereIsNoPreviewAvailableForThisAttachment: 'Предпросмотр для этого вложения недоступен',
time: 'Время',
timer: 'Таймер',
title: 'Название',
+215
View File
@@ -0,0 +1,215 @@
import dateFns from 'date-fns/locale/sk';
export default {
dateFns,
format: {
date: 'd.M.yyyy',
time: 'p',
dateTime: '$t(format:date) $t(format:time)',
longDate: 'd MMM',
longDateTime: "d MMMM 'v' p",
},
translation: {
common: {
account: 'Účet',
actions: 'Akcia',
addAttachment_title: 'Pridať prílohu',
addComment: 'Pridať komentár',
addManager_title: 'Pridať správcu',
addMember_title: 'Pridať člena',
addUser_title: 'Pridať používateľa',
administrator: 'Administrátor',
all: 'Všetko',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Všetky zmeny budú automaticky uložené<br />po obnovení spojenia',
areYouSureYouWantToDeleteThisAttachment: 'Naozaj chcete zmazať túto prílohu?',
areYouSureYouWantToDeleteThisBoard: 'Naozaj chcete zmazať túto tabuľu?',
areYouSureYouWantToDeleteThisCard: 'Naozaj chcete zmazať túto kartu?',
areYouSureYouWantToDeleteThisComment: 'Naozaj chcete zmazať tento komentár?',
areYouSureYouWantToDeleteThisLabel: 'Naozaj chcete zmazať tento štítok?',
areYouSureYouWantToDeleteThisList: 'Naozaj chcete zmazať tento zoznam?',
areYouSureYouWantToDeleteThisProject: 'Naozaj chcete zmazať tento projekt?',
areYouSureYouWantToDeleteThisTask: 'Naozaj chcete zmazať túto úlohu?',
areYouSureYouWantToDeleteThisUser: 'Naozaj chcete zmazať tohoto používateľa?',
areYouSureYouWantToLeaveBoard: 'Naozaj chcete opustiť túto tabuľu?',
areYouSureYouWantToLeaveProject: 'Naozaj chcete opustiť tento projekt?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Naozaj chcete zmazať daného správcu tohto projektu?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Naozaj chcete odstrániť tohoto člena z tabule?',
attachment: 'Príloha',
attachments: 'Prílohy',
authentication: 'Overenie',
background: 'Pozadie',
board: 'Tabuľa',
boardNotFound_title: 'Tabuľa neexistuje',
cardActions_title: 'Akcie na karte',
cardNotFound_title: 'Karta neexistuje',
cardOrActionAreDeleted: 'Karta alebo akcia sú zmazané',
color: 'Farba',
createBoard_title: 'Vytvoriť tabuľu',
createLabel_title: 'Vytvoriť štítok',
createNewOneOrSelectExistingOne: 'Vytvoriť nový alebo vyberte<br />už existujúci',
createProject_title: 'Vytvoriť projekt',
createTextFile_title: 'Vytvoriť textový súbor',
currentPassword: 'Aktuálne heslo',
dangerZone_title: 'Nebezpečná zóna',
date: 'Dátum',
dueDate_title: 'Termín do',
deleteAttachment_title: 'Zmazať prílohu',
deleteBoard_title: 'Zmazať tabuľu',
deleteCard_title: 'Zmazať kartu',
deleteComment_title: 'Zmazať komentár',
deleteLabel_title: 'Zmazať štítok',
deleteList_title: 'Zmazať zoznam',
deleteProject_title: 'Zmazať projekt',
deleteTask_title: 'Zmazať úlohu',
deleteUser_title: 'Zmazať používateľa',
description: 'Popis',
dropFileToUpload: 'Potiahnutím nahraj súbor',
editAttachment_title: 'Upraviť prílohu',
editAvatar_title: 'Upraviť avatar',
editBoard_title: 'Upraviť tabuľu',
editDueDate_title: 'Upraviť Termín do',
editEmail_title: 'Upraviť e-mail',
editInformation_title: 'Upraviť informácie',
editLabel_title: 'Upraviť štítok',
editPassword_title: 'Upraviť heslo',
editTimer_title: 'Upraviť časovač',
editUsername_title: 'Upraviť používateľské meno',
email: 'E-mail',
emailAlreadyInUse: 'E-mail je už použitý',
enterCardTitle: 'Vlož názov karty...',
enterDescription: 'Vlož popis...',
enterFilename: 'Vlož názov súboru',
enterListTitle: 'Vlož názov zoznamu...',
enterProjectTitle: 'Vlož názov projektu',
enterTaskDescription: 'Vlož popis úlohy...',
filterByLabels_title: 'Filtruj podľa štítku',
filterByMembers_title: 'Filtruj podľa člena',
fromComputer_title: 'Z počítača',
general: 'Všeobecné',
hours: 'Hodiny',
invalidCurrentPassword: 'Neplatné aktuálne heslo',
labels: 'Štítky',
leaveBoard_title: 'Opustiť tabuľu',
leaveProject_title: 'Opustiť projekt',
list: 'Zoznam',
listActions_title: 'Zoznam akcií',
managers: 'Správcovia',
members: 'Členovia',
minutes: 'Minúty',
moveCard_title: 'Presunúť kartu',
name: 'Meno',
newEmail: 'Nový e-mail',
newPassword: 'Nové heslo',
newUsername: 'Nové používateľské meno',
noConnectionToServer: 'Nie je spojenie k serveru',
noBoards: 'Žiadne tabule',
noLists: 'Žiadne zoznamy',
noProjects: 'Žiadne projekty',
notifications: 'Oznámenia',
noUnreadNotifications: 'Žiadne neprečítané oznámenia',
openBoard_title: 'Otvoriť tabuľu',
optional_inline: 'voliteľné',
organization: 'Spoločnosť',
phone: 'Telefón',
preferences: 'Voľby',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: stlačte Ctrl-V (Cmd-V na Mac) pre vloženie prílohy zo schránky.',
project: 'Projekt',
projectNotFound_title: 'Projekt neexistuje',
removeManager_title: 'Odstrániť správcu',
removeMember_title: 'Odstrániť člena',
seconds: 'Sekúnd',
selectBoard: 'Vybrať tabuľu',
selectList: 'Vybrať zoznam',
selectProject: 'Vybrať projekt',
settings: 'Nastavenia',
subscribeToMyOwnCardsByDefault: 'Predvolene odoberať vlastné karty',
taskActions_title: 'Akcie na úlohe',
tasks: 'Úlohy',
time: 'Čas',
timer: 'Časovač',
title: 'Názov',
userActions_title: 'Akcie na používateľovi',
userAddedThisCardToList: '<0>{{user}}</0><1> pridal kartu do {{list}}</1>',
userLeftNewCommentToCard: '{{user}} zanechal nový komentár «{{comment}}» k <2>{{card}}</2>',
userMovedCardFromListToList: '{{user}} presunul <2>{{card}}</2> z {{fromList}} do {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> presunul túto kartu z {{fromList}} do {{toList}}</1>',
username: 'Používateľské meno',
usernameAlreadyInUse: 'Používateľské meno je zabrané',
users: 'Používatelia',
writeComment: 'Napísať komentár...',
},
action: {
addAnotherCard: 'Pridať ďalšiu kartu',
addAnotherList: 'Pridať ďalší zoznam',
addAnotherTask: 'Pridať ďalšiu úlohu',
addCard: 'Pridať kartu',
addCard_title: 'Pridať kartu',
addComment: 'Pridať komentár',
addList: 'Pridať zoznam',
addMoreDetailedDescription: 'Pridať ďalší detailný popis',
addTask: 'Pridať úlohu',
addToCard: 'Pridať na kartu',
addUser: 'Pridať používateľa',
createBoard: 'Vytvoriť tabuľu',
createFile: 'Vytvoriť súbor',
createLabel: 'Vytvoriť štítok',
createNewLabel: 'Vytvoriť nový štítok',
createProject: 'Vytvoriť projekt',
delete: 'Zmazať',
deleteAttachment: 'Zmazať prílohu',
deleteAvatar: 'Zmazať avatar',
deleteBoard: 'Zmazať tabuľu',
deleteCard: 'Zmazať kartu',
deleteCard_title: 'Zmazať kartu',
deleteComment: 'Zmazať komentár',
deleteImage: 'Zmazať obrázok',
deleteLabel: 'Zmazať štítok',
deleteList: 'Zmazať zoznam',
deleteList_title: 'Zmazať zoznam',
deleteProject: 'Zmazať projekt',
deleteProject_title: 'Zmazať projekt',
deleteTask: 'Zmazať úlohu',
deleteTask_title: 'Zmazať úlohu',
deleteUser: 'Zmazať používateľa',
edit: 'Upraviť',
editDueDate_title: 'Upraviť termín do',
editDescription_title: 'Upraviť popis',
editEmail_title: 'Upraviť e-mail',
editInformation_title: 'Upraviť informácie',
editPassword_title: 'Upraviť heslo',
editTimer_title: 'Upraviť časovač',
editTitle_title: 'Upraviť názov',
editUsername_title: 'Upraviť používateľské meno',
leaveBoard: 'Opustiť tabuľu',
leaveProject: 'Opustiť projekt',
logOut_title: 'Odhlásiť sa',
makeCover_title: 'Vytvoriť obal',
move: 'Presunúť',
moveCard_title: 'Presunúť kartu',
remove: 'Odstrániť',
removeBackground: 'Odstrániť pozadie',
removeCover_title: 'Odstrániť obal',
removeFromBoard: 'Odstrániť z tabule',
removeFromProject: 'Odstrániť z projektu',
removeManager: 'Odstrániť správcu',
removeMember: 'Odstrániť člena',
save: 'Uložiť',
showAllAttachments: 'Zozbraziť všetky prílohy ({{hidden}} skryté)',
showFewerAttachments: 'Zobraziť menej príloh',
start: 'Start',
stop: 'Stop',
subscribe: 'Odoberať',
unsubscribe: 'Neodoberať',
uploadNewAvatar: 'Nahrať nový avatar',
uploadNewImage: 'Nahrať nový obrázok',
},
},
};
+20
View File
@@ -0,0 +1,20 @@
export default {
translation: {
common: {
emailOrUsername: 'E-mail alebo používateľské meno',
invalidEmailOrUsername: 'Nesprávny e-mail alebo používateľské meno',
invalidPassword: 'Nesprávne heslo',
logInToPlanka: 'Prihlásiť sa do Planka',
noInternetConnection: 'Bez pripojenia k internetu',
pageNotFound_title: 'Stránka neexistuje',
password: 'Heslo',
projectManagement: 'Správa projektu',
serverConnectionFailed: 'Pripojenie k serveru zlyhalo',
unknownError: 'Neznáma chyba, skúste to neskôr',
},
action: {
logIn: 'Prihlásiť sa',
},
},
};
+216
View File
@@ -0,0 +1,216 @@
import dateFns from 'date-fns/locale/sv';
export default {
dateFns,
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: 'Konto',
actions: 'Åtgärder',
addAttachment_title: 'Bifoga',
addComment: 'Kommentera',
addManager_title: 'Lägg till Projektledare',
addMember_title: 'Lägg till Medlem',
addUser_title: 'Lägg till Användare',
administrator: 'Administratör',
all: 'Alla',
allChangesWillBeAutomaticallySavedAfterConnectionRestored:
'Alla ändringar kommer att sparas automatiskt<br />så fort anslutningen är återställd',
areYouSureYouWantToDeleteThisAttachment:
'Är du säker på att du vill ta bort den här bilagan?',
areYouSureYouWantToDeleteThisBoard: 'Är du säker på att du vill ta bort den här tavlan?',
areYouSureYouWantToDeleteThisCard: 'Är du säker på att du vill ta bort det här kortet?',
areYouSureYouWantToDeleteThisComment:
'Är du säker på att du vill ta bort den här kommentaren?',
areYouSureYouWantToDeleteThisLabel: 'Är du säker på att du vill ta bort den här etiketten?',
areYouSureYouWantToDeleteThisList: 'Är du säker på att du vill ta bort den här listan?',
areYouSureYouWantToDeleteThisProject: 'Är du säker på att du vill ta bort det här projektet?',
areYouSureYouWantToDeleteThisTask: 'Är du säker på att du vill ta bort den här uppgiften?',
areYouSureYouWantToDeleteThisUser: 'Är du säker på att du vill ta bort den här användaren?',
areYouSureYouWantToLeaveBoard: 'Är du säker på att du vill lämna tavlan?',
areYouSureYouWantToLeaveProject: 'Är du säker på att du vill lämna projektet?',
areYouSureYouWantToRemoveThisManagerFromProject:
'Är du säker på att du vill ta bort den här projektledaren från projektet?',
areYouSureYouWantToRemoveThisMemberFromBoard:
'Är du säker på att du vill ta bort den här medlemmen från tavlan?',
attachment: 'Bilaga',
attachments: 'Bilagor',
authentication: 'Autentisering',
background: 'Bakgrund',
board: 'Tavla',
boardNotFound_title: 'Tavla Ej Funnen',
cardActions_title: 'Kortåtgärder',
cardNotFound_title: 'Kort Ej Funnet',
cardOrActionAreDeleted: 'Kort eller åtgärd är borttagen',
color: 'Färg',
createBoard_title: 'Skapa Tavla',
createLabel_title: 'Skapa Etikett',
createNewOneOrSelectExistingOne: 'Skapa en ny eller välj<br />en redan existerande',
createProject_title: 'Skapa Projekt',
createTextFile_title: 'Skapa Textfil',
currentPassword: 'Nuvarande lösenord',
dangerZone_title: 'Farozon',
date: 'Datum',
dueDate_title: 'Förfallodatum',
deleteAttachment_title: 'Ta Bort Bilaga',
deleteBoard_title: 'Ta Bort Tavla',
deleteCard_title: 'Ta Bort Kort',
deleteComment_title: 'Ta Bort Kommentar',
deleteLabel_title: 'Ta Bort Etikett',
deleteList_title: 'Ta Bort Lista',
deleteProject_title: 'Ta Bort Projekt',
deleteTask_title: 'Ta Bort Uppgift',
deleteUser_title: 'Ta Bort Användare',
description: 'Beskrivning',
dropFileToUpload: 'Släpp en fil för att ladda upp',
editAttachment_title: 'Redigera Bilaga',
editAvatar_title: 'Redigera Avatar',
editBoard_title: 'Redigera Tavla',
editDueDate_title: 'Redigera Förfallodatum',
editEmail_title: 'Redigera E-mail',
editLabel_title: 'Redigera Etikett',
editPassword_title: 'Redigera Lösenord',
editTimer_title: 'Redigera Timer',
editUsername_title: 'Redigera Användarnamn',
email: 'E-mail',
emailAlreadyInUse: 'E-mail används redan',
enterCardTitle: 'Ange kortets titel...',
enterDescription: 'Ange beskrivning...',
enterFilename: 'Ange filnamn',
enterListTitle: 'Ange listans titel...',
enterProjectTitle: 'Ange projektets titel',
enterTaskDescription: 'Ange uppgiftsbeskrivning...',
filterByLabels_title: 'Filtrera Efter Etiketter',
filterByMembers_title: 'Filtrera Efter Medlemmar',
fromComputer_title: 'Från dator',
general: 'Allmänt',
hours: 'Timmar',
invalidCurrentPassword: 'Ogiltigt nuvarande lösenord',
labels: 'Etiketter',
leaveBoard_title: 'Lämna Tavla',
leaveProject_title: 'Lämna Projekt',
list: 'Lista',
listActions_title: 'Liståtgärder',
managers: 'Projektledare',
members: 'Medlemmar',
minutes: 'Minuter',
moveCard_title: 'Flytta Kort',
name: 'Namn',
newEmail: 'Ny e-mail',
newPassword: 'Nytt lösenord',
newUsername: 'Nytt användarnamn',
noConnectionToServer: 'Ingen anslutning till servern',
noBoards: 'Inga tavlor',
noLists: 'Inga listor',
noProjects: 'Inga projekt',
notifications: 'Notifikationer',
noUnreadNotifications: 'Inga olästa notifikationer',
openBoard_title: 'Öppna Tavla',
optional_inline: 'valfri',
organization: 'Organisation',
phone: 'Telefon',
preferences: 'Preferenser',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tips: tryck på Ctrl-V (Cmd-V på Mac) för att lägga till en bilaga från urklipp.',
project: 'Projekt',
projectNotFound_title: 'Projekt Ej Funnet',
removeManager_title: 'Ta Bort Projektledare',
removeMember_title: 'Ta Bort Medlem',
seconds: 'Sekunder',
selectBoard: 'Välj tavla',
selectList: 'Välj lista',
selectProject: 'Välj projekt',
settings: 'Inställningar',
subscribeToMyOwnCardsByDefault: 'Prenumerera på mina egna kort som standard',
taskActions_title: 'Uppgiftsåtgärder',
tasks: 'Uppgifter',
time: 'Tid',
timer: 'Timer',
title: 'Titel',
userActions_title: 'Användaråtgärder',
userAddedThisCardToList: '<0>{{user}}</0><1> lade till detta kort i {{list}}</1>',
userLeftNewCommentToCard: '{{user}} lämnade en ny kommentar «{{comment}}» på <2>{{card}}</2>',
userMovedCardFromListToList:
'{{user}} flyttade <2>{{card}}</2> från {{fromList}} till {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0><1> flyttade detta kort från {{fromList}} till {{toList}}</1>',
username: 'Användarnamn',
usernameAlreadyInUse: 'Användarnamnet används redan',
users: 'Användare',
writeComment: 'Skriv en kommentar...',
},
action: {
addAnotherCard: 'Lägg till ytterligare ett kort',
addAnotherList: 'Lägg till ytterligare en lista',
addAnotherTask: 'Lägg till ytterligare en uppgift',
addCard: 'Lägg till kort',
addCard_title: 'Lägg Till Kort',
addComment: 'Lägg till kommentar',
addList: 'Lägg till lista',
addMoreDetailedDescription: 'Lägg till en mer detaljerad beskrivning',
addTask: 'Lägg till uppgift',
addToCard: 'Lägg till i kort',
addUser: 'Lägg till användare',
createBoard: 'Skapa tavla',
createFile: 'Skapa fil',
createLabel: 'Skapa etikett',
createNewLabel: 'Skapa ny etikett',
createProject: 'Skapa projekt',
delete: 'Ta Bort',
deleteAttachment: 'Ta bort bilaga',
deleteAvatar: 'Ta bort avatar',
deleteBoard: 'Ta bort tavla',
deleteCard: 'Ta bort kort',
deleteCard_title: 'Ta Bort Kort',
deleteComment: 'Ta bort kommentar',
deleteImage: 'Ta bort bild',
deleteLabel: 'Ta bort etikett',
deleteList: 'Ta bort lista',
deleteList_title: 'Ta Bort Lista',
deleteProject: 'Ta bort projekt',
deleteProject_title: 'Ta Bort Projekt',
deleteTask: 'Ta bort uppgift',
deleteTask_title: 'Ta Bort Uppgift',
deleteUser: 'Ta bort användare',
edit: 'Redigera',
editDueDate_title: 'Redigera Förfallodatum',
editDescription_title: 'Redigera Beskrivning',
editEmail_title: 'Redigera E-mail',
editPassword_title: 'Redigera Lösenord',
editTimer_title: 'Redigera Timer',
editTitle_title: 'Redigera Titel',
editUsername_title: 'Redigera Användarnamn',
leaveBoard: 'Lämna tavla',
leaveProject: 'Lämna projekt',
logOut_title: 'Logga ut',
makeCover_title: 'Skapa Omslagsbild',
move: 'Flytta',
moveCard_title: 'Flytta Kort',
remove: 'Ta Bort',
removeBackground: 'Ta bort bakgrund',
removeCover_title: 'Ta Bort Omslagsbild',
removeFromBoard: 'Ta bort from tavla',
removeFromProject: 'Ta bort from projekt',
removeManager: 'Ta bort projektledare',
removeMember: 'Ta bort medlem',
save: 'Spara',
showAllAttachments: 'Visa alla bilagor ({{hidden}} dolda)',
showFewerAttachments: 'Visa färre bilagor',
start: 'Starta',
stop: 'Stoppa',
subscribe: 'Prenumerera',
unsubscribe: 'Avprenumerera',
uploadNewAvatar: 'Ladda upp ny avatar',
uploadNewImage: 'Ladda upp ny bild',
},
},
};
+20
View File
@@ -0,0 +1,20 @@
export default {
translation: {
common: {
emailOrUsername: 'E-mail eller användarnamn',
invalidEmailOrUsername: 'Ogiltig e-mail eller användarnamn',
invalidPassword: 'Ogiltigt lösenord',
logInToPlanka: 'Logga in på Planka',
noInternetConnection: 'Ingen internetanslutning',
pageNotFound_title: 'Sidan Kunde Inte Hittas',
password: 'Lösenord',
projectManagement: 'Projektledning',
serverConnectionFailed: 'Server connection failed',
unknownError: 'Okänt fel, försök igen senare',
},
action: {
logIn: 'Logga in',
},
},
};
+1
View File
@@ -9,6 +9,7 @@ export default class extends Model {
id: attr(),
url: attr(),
coverUrl: attr(),
image: attr(),
name: attr(),
cardId: fk({
to: 'Card',
+1 -1
View File
@@ -206,7 +206,7 @@ export default class extends Model {
}
getOrderedTasksQuerySet() {
return this.tasks.orderBy('id');
return this.tasks.orderBy('position');
}
getOrderedAttachmentsQuerySet() {
+1
View File
@@ -7,6 +7,7 @@ export default class extends Model {
static fields = {
id: attr(),
position: attr(),
name: attr(),
isCompleted: attr({
getDefault: () => false,
+37
View File
@@ -35,6 +35,7 @@ export default class extends Model {
static fields = {
id: attr(),
email: attr(),
username: attr(),
name: attr(),
avatarUrl: attr(),
phone: attr(),
@@ -140,6 +141,18 @@ export default class extends Model {
break;
}
case ActionTypes.USER_EMAIL_UPDATE_ERROR_CLEAR: {
const userModel = User.withId(payload.id);
userModel.update({
emailUpdateForm: {
...userModel.emailUpdateForm,
error: null,
},
});
break;
}
case ActionTypes.USER_PASSWORD_UPDATE: {
const userModel = User.withId(payload.id);
@@ -174,6 +187,18 @@ export default class extends Model {
break;
}
case ActionTypes.USER_PASSWORD_UPDATE_ERROR_CLEAR: {
const userModel = User.withId(payload.id);
userModel.update({
passwordUpdateForm: {
...userModel.passwordUpdateForm,
error: null,
},
});
break;
}
case ActionTypes.USER_USERNAME_UPDATE: {
const userModel = User.withId(payload.id);
@@ -208,6 +233,18 @@ export default class extends Model {
break;
}
case ActionTypes.USER_USERNAME_UPDATE_ERROR_CLEAR: {
const userModel = User.withId(payload.id);
userModel.update({
usernameUpdateForm: {
...userModel.usernameUpdateForm,
error: null,
},
});
break;
}
case ActionTypes.USER_AVATAR_UPDATE:
User.withId(payload.id).update({
isAvatarUpdating: true,
+17 -3
View File
@@ -1,7 +1,7 @@
import { call, put, select } from 'redux-saga/effects';
import request from '../request';
import { pathSelector } from '../../../selectors';
import { nextTaskPositionSelector, pathSelector, taskByIdSelector } from '../../../selectors';
import {
createTask,
deleteTask,
@@ -14,11 +14,16 @@ import api from '../../../api';
import { createLocalId } from '../../../utils/local-id';
export function* createTaskService(cardId, data) {
const nextData = {
...data,
position: yield select(nextTaskPositionSelector, cardId),
};
const localId = yield call(createLocalId);
yield put(
createTask({
...data,
...nextData,
cardId,
id: localId,
}),
@@ -26,7 +31,7 @@ export function* createTaskService(cardId, data) {
let task;
try {
({ item: task } = yield call(request, api.createTask, cardId, data));
({ item: task } = yield call(request, api.createTask, cardId, nextData));
} catch (error) {
yield put(createTask.failure(localId, error));
return;
@@ -63,6 +68,15 @@ export function* handleTaskUpdateService(task) {
yield put(handleTaskUpdate(task));
}
export function* moveTaskService(id, index) {
const { cardId } = yield select(taskByIdSelector, id);
const position = yield select(nextTaskPositionSelector, cardId, index, id);
yield call(updateTaskService, id, {
position,
});
}
export function* deleteTaskService(id) {
yield put(deleteTask(id));
+4
View File
@@ -6,6 +6,7 @@ import {
handleTaskCreateService,
handleTaskDeleteService,
handleTaskUpdateService,
moveTaskService,
updateTaskService,
} from '../services';
import EntryActionTypes from '../../../constants/EntryActionTypes';
@@ -24,6 +25,9 @@ export default function* taskWatchers() {
takeEvery(EntryActionTypes.TASK_UPDATE_HANDLE, ({ payload: { task } }) =>
handleTaskUpdateService(task),
),
takeEvery(EntryActionTypes.TASK_MOVE, ({ payload: { id, index } }) =>
moveTaskService(id, index),
),
takeEvery(EntryActionTypes.TASK_DELETE, ({ payload: { id } }) => deleteTaskService(id)),
takeEvery(EntryActionTypes.TASK_DELETE_HANDLE, ({ payload: { task } }) =>
handleTaskDeleteService(task),
+29 -5
View File
@@ -8,8 +8,16 @@ import {
clearCurrentUserPasswordUpdateErrorService,
clearCurrentUserUsernameUpdateErrorService,
clearUserCreateErrorService,
clearUserEmailUpdateErrorService,
clearUserPasswordUpdateErrorService,
clearUserUsernameUpdateErrorService,
createUserService,
deleteUserService,
handleUserCreateService,
handleUserDeleteService,
handleUserFromCardRemoveService,
handleUserToCardAddService,
handleUserUpdateService,
removeUserFromCardService,
removeUserFromCurrentCardService,
removeUserFromFilterInCurrentBoardService,
@@ -19,11 +27,9 @@ import {
updateCurrentUserPasswordService,
updateCurrentUserService,
updateCurrentUserUsernameService,
handleUserCreateService,
handleUserUpdateService,
handleUserDeleteService,
handleUserToCardAddService,
handleUserFromCardRemoveService,
updateUserEmailService,
updateUserPasswordService,
updateUserUsernameService,
} from '../services';
import EntryActionTypes from '../../../constants/EntryActionTypes';
@@ -43,21 +49,39 @@ export default function* userWatchers() {
takeEvery(EntryActionTypes.USER_UPDATE_HANDLE, ({ payload: { user } }) =>
handleUserUpdateService(user),
),
takeEvery(EntryActionTypes.USER_EMAIL_UPDATE, ({ payload: { id, data } }) =>
updateUserEmailService(id, data),
),
takeEvery(EntryActionTypes.CURRENT_USER_EMAIL_UPDATE, ({ payload: { data } }) =>
updateCurrentUserEmailService(data),
),
takeEvery(EntryActionTypes.USER_EMAIL_UPDATE_ERROR_CLEAR, ({ payload: { id } }) =>
clearUserEmailUpdateErrorService(id),
),
takeEvery(EntryActionTypes.CURRENT_USER_EMAIL_UPDATE_ERROR_CLEAR, () =>
clearCurrentUserEmailUpdateErrorService(),
),
takeEvery(EntryActionTypes.USER_PASSWORD_UPDATE, ({ payload: { id, data } }) =>
updateUserPasswordService(id, data),
),
takeEvery(EntryActionTypes.CURRENT_USER_PASSWORD_UPDATE, ({ payload: { data } }) =>
updateCurrentUserPasswordService(data),
),
takeEvery(EntryActionTypes.USER_PASSWORD_UPDATE_ERROR_CLEAR, ({ payload: { id } }) =>
clearUserPasswordUpdateErrorService(id),
),
takeEvery(EntryActionTypes.CURRENT_USER_PASSWORD_UPDATE_ERROR_CLEAR, () =>
clearCurrentUserPasswordUpdateErrorService(),
),
takeEvery(EntryActionTypes.USER_USERNAME_UPDATE, ({ payload: { id, data } }) =>
updateUserUsernameService(id, data),
),
takeEvery(EntryActionTypes.CURRENT_USER_USERNAME_UPDATE, ({ payload: { data } }) =>
updateCurrentUserUsernameService(data),
),
takeEvery(EntryActionTypes.USER_USERNAME_UPDATE_ERROR_CLEAR, ({ payload: { id } }) =>
clearUserUsernameUpdateErrorService(id),
),
takeEvery(EntryActionTypes.CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR, () =>
clearCurrentUserUsernameUpdateErrorService(),
),
+1 -1
View File
@@ -71,7 +71,7 @@ export const makeTasksByCardIdSelector = () =>
return cardModel;
}
return cardModel.tasks.toRefArray();
return cardModel.getOrderedTasksQuerySet().toRefArray();
},
);
+16
View File
@@ -76,3 +76,19 @@ export const nextCardPositionSelector = createSelector(
return nextPosition(listModel.getOrderedFilteredCardsModelArray(), index, excludedId);
},
);
export const nextTaskPositionSelector = createSelector(
orm,
(_, cardId) => cardId,
(_, __, index) => index,
(_, __, ___, excludedId) => excludedId,
({ Card }, cardId, index, excludedId) => {
const cardModel = Card.withId(cardId);
if (!cardModel) {
return cardModel;
}
return nextPosition(cardModel.getOrderedTasksQuerySet().toRefArray(), index, excludedId);
},
);
+1
View File
@@ -10,4 +10,5 @@ export * from './board';
export * from './board-membership';
export * from './list';
export * from './card';
export * from './task';
export * from './attachment';
+20
View File
@@ -0,0 +1,20 @@
import { createSelector } from 'redux-orm';
import orm from '../orm';
export const makeTaskByIdSelector = () =>
createSelector(
orm,
(_, id) => id,
({ Task }, id) => {
const taskModel = Task.withId(id);
if (!taskModel) {
return taskModel;
}
return taskModel.ref;
},
);
export const taskByIdSelector = makeTaskByIdSelector();
+4
View File
@@ -101,6 +101,10 @@
border-radius: 0;
}
}
.pswp .pswp__img--placeholder {
display: none !important;
}
}
#root {
+1 -1
View File
@@ -2,7 +2,7 @@ version: '3'
services:
planka:
image: meltyshev/planka:latest
image: ghcr.io/plankanban/planka:latest
command: >
bash -c
"for i in `seq 1 30`; do
+276 -249
View File
@@ -1,18 +1,18 @@
{
"name": "planka",
"version": "0.1.0",
"version": "1.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "planka",
"version": "0.1.0",
"version": "1.4.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"concurrently": "^7.1.0",
"husky": "^7.0.4",
"lint-staged": "^12.3.8"
"concurrently": "^7.3.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3"
}
},
"node_modules/aggregate-error": {
@@ -217,31 +217,32 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/colorette": {
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz",
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g=="
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
"integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ=="
},
"node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz",
"integrity": "sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==",
"engines": {
"node": ">= 12"
"node": "^12.20.0 || >=14"
}
},
"node_modules/concurrently": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.1.0.tgz",
"integrity": "sha512-Bz0tMlYKZRUDqJlNiF/OImojMB9ruKUz6GCfmhFnSapXgPe+3xzY4byqoKG9tUZ7L2PGEUjfLPOLfIX3labnmw==",
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.3.0.tgz",
"integrity": "sha512-IiDwm+8DOcFEInca494A8V402tNTQlJaYq78RF2rijOrKEk/AOHTxhN4U1cp7GYKYX5Q6Ymh1dLTBlzIMN0ikA==",
"dependencies": {
"chalk": "^4.1.0",
"date-fns": "^2.16.1",
"lodash": "^4.17.21",
"rxjs": "^6.6.3",
"rxjs": "^7.0.0",
"shell-quote": "^1.7.3",
"spawn-command": "^0.0.2-1",
"supports-color": "^8.1.0",
"tree-kill": "^1.2.2",
"yargs": "^16.2.0"
"yargs": "^17.3.1"
},
"bin": {
"concurrently": "dist/bin/concurrently.js"
@@ -264,9 +265,9 @@
}
},
"node_modules/date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.1.tgz",
"integrity": "sha512-dlLD5rKaKxpFdnjrs+5azHDFOPEu4ANy/LTh04A1DTzMM7qoajmKCBc8pkKRFT41CNzw+4gQh79X5C+Jq27HAw==",
"engines": {
"node": ">=0.11"
},
@@ -310,22 +311,22 @@
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz",
"integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==",
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^6.0.0",
"human-signals": "^2.1.0",
"is-stream": "^2.0.0",
"get-stream": "^6.0.1",
"human-signals": "^3.0.1",
"is-stream": "^3.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^4.0.1",
"onetime": "^5.1.2",
"signal-exit": "^3.0.3",
"strip-final-newline": "^2.0.0"
"npm-run-path": "^5.1.0",
"onetime": "^6.0.0",
"signal-exit": "^3.0.7",
"strip-final-newline": "^3.0.0"
},
"engines": {
"node": ">=10"
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
@@ -370,22 +371,22 @@
}
},
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz",
"integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==",
"engines": {
"node": ">=10.17.0"
"node": ">=12.20.0"
}
},
"node_modules/husky": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz",
"integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==",
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz",
"integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==",
"bin": {
"husky": "lib/bin.js"
},
"engines": {
"node": ">=12"
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
@@ -419,11 +420,11 @@
}
},
"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==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
"engines": {
"node": ">=8"
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -432,57 +433,45 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/lilconfig": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz",
"integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz",
"integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==",
"engines": {
"node": ">=10"
}
},
"node_modules/lint-staged": {
"version": "12.3.8",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.3.8.tgz",
"integrity": "sha512-0+UpNaqIwKRSGAFOCcpuYNIv/j5QGVC+xUVvmSdxHO+IfIGoHbFLo3XcPmV/LLnsVj5EAncNHVtlITSoY5qWGQ==",
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz",
"integrity": "sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==",
"dependencies": {
"cli-truncate": "^3.1.0",
"colorette": "^2.0.16",
"commander": "^8.3.0",
"debug": "^4.3.3",
"execa": "^5.1.1",
"lilconfig": "2.0.4",
"listr2": "^4.0.1",
"micromatch": "^4.0.4",
"colorette": "^2.0.17",
"commander": "^9.3.0",
"debug": "^4.3.4",
"execa": "^6.1.0",
"lilconfig": "2.0.5",
"listr2": "^4.0.5",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-inspect": "^1.12.0",
"pidtree": "^0.5.0",
"object-inspect": "^1.12.2",
"pidtree": "^0.6.0",
"string-argv": "^0.3.1",
"supports-color": "^9.2.1",
"yaml": "^1.10.2"
"yaml": "^2.1.1"
},
"bin": {
"lint-staged": "bin/lint-staged.js"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
"node": "^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/lint-staged"
}
},
"node_modules/lint-staged/node_modules/supports-color": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.2.tgz",
"integrity": "sha512-XC6g/Kgux+rJXmwokjm9ECpD6k/smUoS5LKlUCcsYr4IY3rW0XyAympon2RmxGrlnZURMpg5T18gWDP9CsHXFA==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/listr2": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz",
@@ -545,14 +534,6 @@
"node": ">=8"
}
},
"node_modules/listr2/node_modules/rxjs": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
"integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/listr2/node_modules/slice-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
@@ -590,11 +571,6 @@
"node": ">=8"
}
},
"node_modules/listr2/node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -709,11 +685,14 @@
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
"engines": {
"node": ">=6"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ms": {
@@ -730,33 +709,47 @@
}
},
"node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
"integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
"dependencies": {
"path-key": "^3.0.0"
"path-key": "^4.0.0"
},
"engines": {
"node": ">=8"
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-run-path/node_modules/path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/object-inspect": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
"integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
"integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
"dependencies": {
"mimic-fn": "^2.1.0"
"mimic-fn": "^4.0.0"
},
"engines": {
"node": ">=6"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -796,9 +789,9 @@
}
},
"node_modules/pidtree": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.5.0.tgz",
"integrity": "sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
"bin": {
"pidtree": "bin/pidtree.js"
},
@@ -809,7 +802,7 @@
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"engines": {
"node": ">=0.10.0"
}
@@ -826,20 +819,39 @@
"node": ">=8"
}
},
"node_modules/restore-cursor/node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"engines": {
"node": ">=6"
}
},
"node_modules/restore-cursor/node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rfdc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
"integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA=="
},
"node_modules/rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
"integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
"dependencies": {
"tslib": "^1.9.0"
},
"engines": {
"npm": ">=2.0.0"
"tslib": "^2.1.0"
}
},
"node_modules/shebang-command": {
@@ -861,6 +873,11 @@
"node": ">=8"
}
},
"node_modules/shell-quote": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -895,7 +912,7 @@
"node_modules/spawn-command": {
"version": "0.0.2-1",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
"integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A="
"integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg=="
},
"node_modules/string-argv": {
"version": "0.3.1",
@@ -936,11 +953,14 @@
}
},
"node_modules/strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
"engines": {
"node": ">=6"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/supports-color": {
@@ -960,7 +980,7 @@
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
},
"node_modules/to-regex-range": {
"version": "5.0.1",
@@ -982,9 +1002,9 @@
}
},
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/type-fest": {
"version": "0.21.3",
@@ -1081,36 +1101,36 @@
}
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz",
"integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==",
"engines": {
"node": ">= 6"
"node": ">= 14"
}
},
"node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"version": "17.5.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
"integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
"yargs-parser": "^21.0.0"
},
"engines": {
"node": ">=10"
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"version": "21.0.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
"integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==",
"engines": {
"node": ">=10"
"node": ">=12"
}
},
"node_modules/yargs/node_modules/ansi-regex": {
@@ -1303,28 +1323,29 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"colorette": {
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz",
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g=="
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
"integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ=="
},
"commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz",
"integrity": "sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw=="
},
"concurrently": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.1.0.tgz",
"integrity": "sha512-Bz0tMlYKZRUDqJlNiF/OImojMB9ruKUz6GCfmhFnSapXgPe+3xzY4byqoKG9tUZ7L2PGEUjfLPOLfIX3labnmw==",
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.3.0.tgz",
"integrity": "sha512-IiDwm+8DOcFEInca494A8V402tNTQlJaYq78RF2rijOrKEk/AOHTxhN4U1cp7GYKYX5Q6Ymh1dLTBlzIMN0ikA==",
"requires": {
"chalk": "^4.1.0",
"date-fns": "^2.16.1",
"lodash": "^4.17.21",
"rxjs": "^6.6.3",
"rxjs": "^7.0.0",
"shell-quote": "^1.7.3",
"spawn-command": "^0.0.2-1",
"supports-color": "^8.1.0",
"tree-kill": "^1.2.2",
"yargs": "^16.2.0"
"yargs": "^17.3.1"
}
},
"cross-spawn": {
@@ -1338,9 +1359,9 @@
}
},
"date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw=="
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.1.tgz",
"integrity": "sha512-dlLD5rKaKxpFdnjrs+5azHDFOPEu4ANy/LTh04A1DTzMM7qoajmKCBc8pkKRFT41CNzw+4gQh79X5C+Jq27HAw=="
},
"debug": {
"version": "4.3.4",
@@ -1366,19 +1387,19 @@
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
},
"execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz",
"integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==",
"requires": {
"cross-spawn": "^7.0.3",
"get-stream": "^6.0.0",
"human-signals": "^2.1.0",
"is-stream": "^2.0.0",
"get-stream": "^6.0.1",
"human-signals": "^3.0.1",
"is-stream": "^3.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^4.0.1",
"onetime": "^5.1.2",
"signal-exit": "^3.0.3",
"strip-final-newline": "^2.0.0"
"npm-run-path": "^5.1.0",
"onetime": "^6.0.0",
"signal-exit": "^3.0.7",
"strip-final-newline": "^3.0.0"
}
},
"fill-range": {
@@ -1405,14 +1426,14 @@
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz",
"integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ=="
},
"husky": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz",
"integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ=="
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz",
"integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw=="
},
"indent-string": {
"version": "4.0.0",
@@ -1430,46 +1451,38 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"lilconfig": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz",
"integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA=="
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz",
"integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg=="
},
"lint-staged": {
"version": "12.3.8",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.3.8.tgz",
"integrity": "sha512-0+UpNaqIwKRSGAFOCcpuYNIv/j5QGVC+xUVvmSdxHO+IfIGoHbFLo3XcPmV/LLnsVj5EAncNHVtlITSoY5qWGQ==",
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz",
"integrity": "sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==",
"requires": {
"cli-truncate": "^3.1.0",
"colorette": "^2.0.16",
"commander": "^8.3.0",
"debug": "^4.3.3",
"execa": "^5.1.1",
"lilconfig": "2.0.4",
"listr2": "^4.0.1",
"micromatch": "^4.0.4",
"colorette": "^2.0.17",
"commander": "^9.3.0",
"debug": "^4.3.4",
"execa": "^6.1.0",
"lilconfig": "2.0.5",
"listr2": "^4.0.5",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-inspect": "^1.12.0",
"pidtree": "^0.5.0",
"object-inspect": "^1.12.2",
"pidtree": "^0.6.0",
"string-argv": "^0.3.1",
"supports-color": "^9.2.1",
"yaml": "^1.10.2"
},
"dependencies": {
"supports-color": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.2.tgz",
"integrity": "sha512-XC6g/Kgux+rJXmwokjm9ECpD6k/smUoS5LKlUCcsYr4IY3rW0XyAympon2RmxGrlnZURMpg5T18gWDP9CsHXFA=="
}
"yaml": "^2.1.1"
}
},
"listr2": {
@@ -1511,14 +1524,6 @@
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"rxjs": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz",
"integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==",
"requires": {
"tslib": "^2.1.0"
}
},
"slice-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
@@ -1546,11 +1551,6 @@
"requires": {
"ansi-regex": "^5.0.1"
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
@@ -1640,9 +1640,9 @@
}
},
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="
},
"ms": {
"version": "2.1.2",
@@ -1655,24 +1655,31 @@
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
},
"npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
"integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
"requires": {
"path-key": "^3.0.0"
"path-key": "^4.0.0"
},
"dependencies": {
"path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="
}
}
},
"object-inspect": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
"integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g=="
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
"integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ=="
},
"onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
"requires": {
"mimic-fn": "^2.1.0"
"mimic-fn": "^4.0.0"
}
},
"p-map": {
@@ -1694,14 +1701,14 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
},
"pidtree": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.5.0.tgz",
"integrity": "sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA=="
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
},
"restore-cursor": {
"version": "3.1.0",
@@ -1710,6 +1717,21 @@
"requires": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"dependencies": {
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"requires": {
"mimic-fn": "^2.1.0"
}
}
}
},
"rfdc": {
@@ -1718,11 +1740,11 @@
"integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA=="
},
"rxjs": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
"integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
"requires": {
"tslib": "^1.9.0"
"tslib": "^2.1.0"
}
},
"shebang-command": {
@@ -1738,6 +1760,11 @@
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"shell-quote": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="
},
"signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -1762,7 +1789,7 @@
"spawn-command": {
"version": "0.0.2-1",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
"integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A="
"integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg=="
},
"string-argv": {
"version": "0.3.1",
@@ -1788,9 +1815,9 @@
}
},
"strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="
},
"supports-color": {
"version": "8.1.1",
@@ -1803,7 +1830,7 @@
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
},
"to-regex-range": {
"version": "5.0.1",
@@ -1819,9 +1846,9 @@
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"type-fest": {
"version": "0.21.3",
@@ -1887,22 +1914,22 @@
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
},
"yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz",
"integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw=="
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"version": "17.5.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
"integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
"requires": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
"yargs-parser": "^21.0.0"
},
"dependencies": {
"ansi-regex": {
@@ -1941,9 +1968,9 @@
}
},
"yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="
"version": "21.0.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
"integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg=="
}
}
}
+7 -5
View File
@@ -1,6 +1,6 @@
{
"name": "planka",
"version": "1.0.0",
"version": "1.4.0",
"private": true,
"homepage": "https://plankanban.github.io/planka",
"repository": {
@@ -14,9 +14,11 @@
"client:lint": "npm run lint --prefix client",
"client:start": "npm start --prefix client",
"client:test": "npm test --prefix client",
"docker:build": "docker build -t ghcr.io/plankanban/planka:local -f Dockerfile .",
"docker:build:base": "docker build -t ghcr.io/plankanban/planka:base-local -f Dockerfile.base .",
"postinstall": "npm i --prefix server && npm i --prefix client",
"prepare": "husky install",
"lint": "npm run server:lint && npm run client:lint",
"prepare": "husky install",
"server:console": "npm run console --prefix server",
"server:db:init": "npm run db:init --prefix server",
"server:db:migrate": "npm run db:migrate --prefix server",
@@ -37,8 +39,8 @@
]
},
"dependencies": {
"concurrently": "^7.1.0",
"husky": "^7.0.4",
"lint-staged": "^12.3.8"
"concurrently": "^7.3.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3"
}
}
+1 -3
View File
@@ -54,10 +54,8 @@ module.exports = {
const attachment = await sails.helpers.attachments.createOne(
{
dirname: file.extra.dirname,
...file.extra,
filename: file.filename,
isImage: file.extra.isImage,
name: file.extra.name,
},
currentUser,
card,
@@ -46,7 +46,7 @@ module.exports = {
}
}
if (!attachment.isImage) {
if (!attachment.image) {
throw Errors.ATTACHMENT_NOT_FOUND;
}
@@ -61,7 +61,9 @@ module.exports = {
throw Errors.ATTACHMENT_NOT_FOUND;
}
this.res.setHeader('Content-Disposition', `inline; ${inputs.filename}`);
this.res.type(attachment.filename);
this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config
return exits.success(fs.createReadStream(filePath));
},
};
+4 -10
View File
@@ -14,10 +14,6 @@ module.exports = {
regex: /^[0-9]+$/,
required: true,
},
filename: {
type: 'string',
required: true,
},
},
exits: {
@@ -56,14 +52,12 @@ module.exports = {
throw Errors.ATTACHMENT_NOT_FOUND;
}
let contentDisposition;
if (attachment.isImage || path.extname(attachment.filename) === '.pdf') {
contentDisposition = 'inline';
} else {
contentDisposition = `attachment; ${inputs.filename}`;
this.res.type(attachment.filename);
if (!attachment.image && path.extname(attachment.filename) !== '.pdf') {
this.res.set('Content-Disposition', 'attachment');
}
this.res.set('Cache-Control', 'private, max-age=900'); // TODO: move to config
this.res.setHeader('Content-Disposition', contentDisposition);
return exits.success(fs.createReadStream(filePath));
},
};
+3 -6
View File
@@ -33,11 +33,6 @@ module.exports = {
},
async fn(inputs) {
// TODO: allow over HTTP without subscription
if (!this.req.isSocket) {
return this.res.badRequest();
}
const { currentUser } = this.req;
const project = await Project.findOne(inputs.projectId);
@@ -61,7 +56,9 @@ module.exports = {
this.req,
);
sails.sockets.join(this.req, `board:${board.id}`); // TODO: only when subscription needed
if (this.req.isSocket) {
sails.sockets.join(this.req, `board:${board.id}`); // TODO: only when subscription needed
}
return {
item: board,
+3 -6
View File
@@ -20,11 +20,6 @@ module.exports = {
},
async fn(inputs) {
// TODO: allow over HTTP without subscription
if (!this.req.isSocket) {
return this.res.badRequest();
}
const { currentUser } = this.req;
const { board, project } = await sails.helpers.boards
@@ -77,7 +72,9 @@ module.exports = {
card.isSubscribed = isSubscribedByCardId[card.id] || false; // eslint-disable-line no-param-reassign
});
sails.sockets.join(this.req, `board:${board.id}`); // TODO: only when subscription needed
if (this.req.isSocket) {
sails.sockets.join(this.req, `board:${board.id}`); // TODO: only when subscription needed
}
return {
item: board,
+19 -8
View File
@@ -3,25 +3,36 @@ module.exports = {
const { currentUser } = this.req;
const managerProjectIds = await sails.helpers.users.getManagerProjectIds(currentUser.id);
const managerProjects = await sails.helpers.projects.getMany(managerProjectIds);
const boardMemberships = await sails.helpers.users.getBoardMemberships(currentUser.id);
const membershipBoardIds = await sails.helpers.utils.mapRecords(boardMemberships, 'boardId');
let boardMemberships = await sails.helpers.users.getBoardMemberships(currentUser.id);
const membershipBoards = await sails.helpers.boards.getMany({
let membershipBoardIds = sails.helpers.utils.mapRecords(boardMemberships, 'boardId');
let membershipBoards = await sails.helpers.boards.getMany({
id: membershipBoardIds,
projectId: {
'!=': managerProjectIds,
},
});
const membershipProjectIds = sails.helpers.utils.mapRecords(
membershipBoards,
'projectId',
true,
let membershipProjectIds = sails.helpers.utils.mapRecords(membershipBoards, 'projectId', true);
const membershipProjects = await sails.helpers.projects.getMany(membershipProjectIds);
membershipProjectIds = sails.helpers.utils.mapRecords(membershipProjects);
membershipBoards = membershipBoards.filter((membershipBoard) =>
membershipProjectIds.includes(membershipBoard.projectId),
);
membershipBoardIds = sails.helpers.utils.mapRecords(membershipBoards);
boardMemberships = boardMemberships.filter((boardMembership) =>
membershipBoardIds.includes(boardMembership.boardId),
);
const projectIds = [...managerProjectIds, ...membershipProjectIds];
const projects = await sails.helpers.projects.getMany(projectIds);
const projects = [...managerProjects, ...membershipProjects];
const projectManagers = await sails.helpers.projects.getProjectManagers(projectIds);
+5 -1
View File
@@ -11,6 +11,10 @@ module.exports = {
regex: /^[0-9]+$/,
required: true,
},
position: {
type: 'number',
required: true,
},
name: {
type: 'string',
required: true,
@@ -39,7 +43,7 @@ module.exports = {
throw Errors.CARD_NOT_FOUND; // Forbidden
}
const values = _.pick(inputs, ['name', 'isCompleted']);
const values = _.pick(inputs, ['position', 'name', 'isCompleted']);
const task = await sails.helpers.tasks.createOne(values, card, this.req);
return {
+4 -1
View File
@@ -11,6 +11,9 @@ module.exports = {
regex: /^[0-9]+$/,
required: true,
},
position: {
type: 'number',
},
name: {
type: 'string',
isNotEmptyString: true,
@@ -42,7 +45,7 @@ module.exports = {
throw Errors.TASK_NOT_FOUND; // Forbidden
}
const values = _.pick(inputs, ['name', 'isCompleted']);
const values = _.pick(inputs, ['position', 'name', 'isCompleted']);
task = await sails.helpers.tasks.updateOne(task, values, board, this.req);
if (!task) {
+1 -1
View File
@@ -38,7 +38,7 @@ module.exports = {
inputs.request,
);
if (!inputs.card.coverAttachmentId && attachment.isImage) {
if (!inputs.card.coverAttachmentId && attachment.image) {
await sails.helpers.cards.updateOne(inputs.card, {
coverAttachmentId: attachment.id,
});
+14 -2
View File
@@ -5,11 +5,23 @@ module.exports = {
custom: (value) => _.isString(value) || _.every(value, _.isString),
required: true,
},
exceptTaskIdOrIds: {
type: 'json',
custom: (value) => _.isString(value) || _.every(value, _.isString),
},
},
async fn(inputs) {
return sails.helpers.tasks.getMany({
const criteria = {
cardId: inputs.idOrIds,
});
};
if (!_.isUndefined(inputs.exceptTaskIdOrIds)) {
criteria.id = {
'!=': inputs.exceptTaskIdOrIds,
};
}
return sails.helpers.tasks.getMany(criteria);
},
};
+4 -10
View File
@@ -10,12 +10,6 @@ module.exports = {
},
async fn(inputs) {
const boardIds = await sails.helpers.projects.getBoardIds(inputs.record.id);
await BoardMembership.destroy({
boardId: boardIds,
}).fetch();
const projectManagers = await ProjectManager.destroy({
projectId: inputs.record.id,
}).fetch();
@@ -25,14 +19,14 @@ module.exports = {
if (project) {
const managerUserIds = sails.helpers.utils.mapRecords(projectManagers, 'userId');
const boardIds = await sails.helpers.projects.getBoardIds(project.id);
const boardRooms = boardIds.map((boardId) => `board:${boardId}`);
const memberUserIds = await sails.helpers.boards.getMemberUserIds(boardIds);
const userIds = _.union(managerUserIds, memberUserIds);
userIds.forEach((userId) => {
sails.sockets.removeRoomMembersFromRooms(
`user:${userId}`,
boardIds.map((boardId) => `board:${boardId}`),
);
sails.sockets.removeRoomMembersFromRooms(`user:${userId}`, boardRooms);
sails.sockets.broadcast(
`user:${userId}`,
+25
View File
@@ -2,6 +2,7 @@ module.exports = {
inputs: {
values: {
type: 'json',
custom: (value) => _.isPlainObject(value) && _.isFinite(value.position),
required: true,
},
card: {
@@ -14,8 +15,32 @@ module.exports = {
},
async fn(inputs) {
const tasks = await sails.helpers.cards.getTasks(inputs.card.id);
const { position, repositions } = sails.helpers.utils.insertToPositionables(
inputs.values.position,
tasks,
);
repositions.forEach(async ({ id, position: nextPosition }) => {
await Task.update({
id,
cardId: inputs.card.id,
}).set({
position: nextPosition,
});
sails.sockets.broadcast(`board:${inputs.card.boardId}`, 'taskUpdate', {
item: {
id,
position: nextPosition,
},
});
});
const task = await Task.create({
...inputs.values,
position,
cardId: inputs.card.id,
}).fetch();
+1 -1
View File
@@ -7,6 +7,6 @@ module.exports = {
},
async fn(inputs) {
return Task.find(inputs.criteria).sort('id');
return Task.find(inputs.criteria).sort('position');
},
};
+38
View File
@@ -6,6 +6,17 @@ module.exports = {
},
values: {
type: 'json',
custom: (value) => {
if (!_.isPlainObject(value)) {
return false;
}
if (!_.isUndefined(value.position) && !_.isFinite(value.position)) {
return false;
}
return true;
},
required: true,
},
board: {
@@ -18,6 +29,33 @@ module.exports = {
},
async fn(inputs) {
if (!_.isUndefined(inputs.values.position)) {
const tasks = await sails.helpers.cards.getTasks(inputs.record.cardId, inputs.record.id);
const { position, repositions } = sails.helpers.utils.insertToPositionables(
inputs.values.position,
tasks,
);
inputs.values.position = position; // eslint-disable-line no-param-reassign
repositions.forEach(async ({ id, position: nextPosition }) => {
await Task.update({
id,
cardId: inputs.record.cardId,
}).set({
position: nextPosition,
});
sails.sockets.broadcast(`board:${inputs.board.id}`, 'taskUpdate', {
item: {
id,
position: nextPosition,
},
});
});
}
const task = await Task.updateOne(inputs.record.id).set(inputs.values);
if (task) {
@@ -43,15 +43,20 @@ module.exports = {
await writeFile(path.join(rootPath, filename), buffer);
const image = sharp(buffer);
let imageMetadata;
let metadata;
try {
imageMetadata = await image.metadata();
metadata = await image.metadata();
} catch (error) {} // eslint-disable-line no-empty
if (imageMetadata) {
const extra = {
dirname,
name: file.filename,
};
if (metadata && !['svg', 'pdf'].includes(metadata.format)) {
let cover256Buffer;
if (imageMetadata.height > imageMetadata.width) {
if (metadata.height > metadata.width) {
cover256Buffer = await image
.resize(256, 320)
.jpeg({
@@ -75,17 +80,16 @@ module.exports = {
fs.mkdirSync(thumbnailsPath);
await writeFile(path.join(thumbnailsPath, 'cover-256.jpg'), cover256Buffer);
extra.image = _.pick(metadata, ['width', 'height']);
} else {
extra.image = null;
}
// eslint-disable-next-line no-param-reassign
file.extra = {
dirname,
isImage: !!imageMetadata,
name: file.filename,
};
// eslint-disable-next-line no-param-reassign
file.filename = filename;
Object.assign(file, {
filename,
extra,
});
return done();
} catch (error) {
+4 -6
View File
@@ -19,10 +19,8 @@ module.exports = {
type: 'string',
required: true,
},
isImage: {
type: 'boolean',
required: true,
columnName: 'is_image',
image: {
type: 'json',
},
name: {
type: 'string',
@@ -51,9 +49,9 @@ module.exports = {
customToJSON() {
return {
..._.omit(this, ['dirname', 'filename', 'isImage']),
..._.omit(this, ['dirname', 'filename']),
url: `${sails.config.custom.attachmentsUrl}/${this.id}/download/${this.filename}`,
coverUrl: this.isImage
coverUrl: this.image
? `${sails.config.custom.attachmentsUrl}/${this.id}/download/thumbnails/cover-256.jpg`
: null,
};
+4
View File
@@ -11,6 +11,10 @@ module.exports = {
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
position: {
type: 'number',
required: true,
},
name: {
type: 'string',
required: true,
+7 -3
View File
@@ -20,20 +20,24 @@
* https://sailsjs.com/anatomy/app.js
*/
require('dotenv').config();
const dotenv = require('dotenv');
// Ensure we're in the project directory, so cwd-relative paths work as expected
// no matter where we actually lift from.
// > Note: This is not required in order to lift, but it is a convenient default.
process.chdir(__dirname);
dotenv.config();
// Attempt to import `sails` dependency, as well as `rc` (for loading `.sailsrc` files).
let sails;
let rc;
try {
sails = require('sails'); // eslint-disable-line global-require
rc = require('sails/accessible/rc'); // eslint-disable-line global-require
/* eslint-disable global-require */
sails = require('sails');
rc = require('sails/accessible/rc');
/* eslint-enable global-require */
} catch (error) {
/* eslint-disable no-console */
console.error("Encountered an error when attempting to require('sails'):");
+5 -3
View File
@@ -1,10 +1,12 @@
const config = require('./knexfile');
const initKnex = require('knex');
const knex = require('knex')(config); // eslint-disable-line import/order
const knexfile = require('./knexfile');
const knex = initKnex(knexfile);
(async () => {
try {
const isExists = await knex.schema.hasTable(config.migrations.tableName);
const isExists = await knex.schema.hasTable(knexfile.migrations.tableName);
await knex.migrate.latest();
if (!isExists) {
+2 -1
View File
@@ -1,7 +1,8 @@
const path = require('path');
const dotenv = require('dotenv');
const _ = require('lodash');
require('dotenv').config({
dotenv.config({
path: path.resolve(__dirname, '../.env'),
});
@@ -1,5 +1,5 @@
module.exports.up = (knex) =>
knex.schema.createTable('card', (table) => {
knex.schema.createTable('card', async (table) => {
/* Columns */
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
@@ -12,7 +12,7 @@ module.exports.up = (knex) =>
table.specificType('position', 'double precision');
table.text('name').notNullable();
table.text('description');
table.timestamp('dueDate', true);
table.timestamp('due_date', true);
table.jsonb('timer');
table.timestamp('created_at', true);
@@ -0,0 +1,73 @@
const path = require('path');
const sharp = require('sharp');
const _ = require('lodash');
const getConfig = require('../../get-config');
module.exports.up = async (knex) => {
await knex.schema.table('attachment', (table) => {
/* Columns */
table.jsonb('image');
});
const config = await getConfig();
const attachments = await knex('attachment');
// eslint-disable-next-line no-restricted-syntax
for (attachment of attachments) {
if (attachment.is_image) {
const image = sharp(
path.join(config.custom.attachmentsPath, attachment.dirname, attachment.filename),
);
let metadata;
try {
metadata = await image.metadata(); // eslint-disable-line no-await-in-loop
} catch (error) {
continue; // eslint-disable-line no-continue
}
if (!['svg', 'pdf'].includes(metadata.format)) {
// eslint-disable-next-line no-await-in-loop
await knex('attachment')
.update({
image: _.pick(metadata, ['width', 'height']),
})
.where('id', attachment.id);
}
}
}
return knex.schema.table('attachment', (table) => {
table.dropColumn('is_image');
});
};
module.exports.down = async (knex) => {
await knex.schema.table('attachment', (table) => {
/* Columns */
table.boolean('is_image');
});
const attachments = await knex('attachment');
// eslint-disable-next-line no-restricted-syntax
for (attachment of attachments) {
// eslint-disable-next-line no-await-in-loop
await knex('attachment')
.update({
is_image: !!attachment.image,
})
.where('id', attachment.id);
}
await knex.schema.table('attachment', (table) => {
table.dropColumn('image');
});
return knex.schema.alterTable('attachment', (table) => {
table.boolean('is_image').notNullable().alter();
});
};
@@ -0,0 +1,44 @@
const POSITION_GAP = 65535;
module.exports.up = async (knex) => {
await knex.schema.table('task', (table) => {
/* Columns */
table.specificType('position', 'double precision');
/* Indexes */
table.index('position');
});
const tasks = await knex('task').orderBy(['card_id', 'id']);
let prevCardId;
let position;
// eslint-disable-next-line no-restricted-syntax
for (task of tasks) {
if (task.card_id === prevCardId) {
position += POSITION_GAP;
} else {
prevCardId = task.card_id;
position = POSITION_GAP;
}
// eslint-disable-next-line no-await-in-loop
await knex('task')
.update({
position,
})
.where('id', task.id);
}
return knex.schema.table('task', (table) => {
table.dropNullable('position');
});
};
module.exports.down = async (knex) =>
knex.schema.table('task', (table) => {
table.dropColumn('position');
});
+38
View File
@@ -0,0 +1,38 @@
const dotenv = require('dotenv');
const sails = require('sails');
const rc = require('sails/accessible/rc');
process.chdir(__dirname);
dotenv.config();
const config = rc('sails');
const getConfigPromise = new Promise((resolve) => {
sails.load(
{
...config,
hooks: {
...config.hooks,
logger: false,
request: false,
views: false,
blueprints: false,
responses: false,
helpers: false,
pubsub: false,
policies: false,
services: false,
security: false,
i18n: false,
session: false,
http: false,
userhooks: false,
},
},
() => {
resolve(sails.config);
},
);
});
module.exports = () => getConfigPromise;
+1009 -2245
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -39,34 +39,34 @@
},
"dependencies": {
"bcrypt": "^5.0.1",
"dotenv": "^16.0.0",
"dotenv-cli": "^5.1.0",
"dotenv": "^16.0.1",
"dotenv-cli": "^6.0.0",
"filenamify": "^4.3.0",
"jsonwebtoken": "^8.5.1",
"knex": "^1.0.7",
"knex": "^2.2.0",
"lodash": "^4.17.21",
"moment": "^2.29.3",
"moment": "^2.29.4",
"rimraf": "^3.0.2",
"sails": "^1.5.2",
"sails-hook-orm": "^4.0.1",
"sails-hook-sockets": "^2.0.1",
"sails-postgresql-redacted": "^1.0.2-9",
"sharp": "^0.30.4",
"sharp": "^0.30.7",
"stream-to-array": "^2.3.0",
"uuid": "^8.3.2",
"validator": "^13.7.0"
},
"devDependencies": {
"chai": "^4.3.6",
"eslint": "^8.13.0",
"eslint": "^8.20.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-prettier": "^4.2.1",
"mocha": "^9.2.2",
"nodemon": "^2.0.15",
"prettier": "2.6.2",
"supertest": "^6.2.2"
"nodemon": "^2.0.19",
"prettier": "2.7.1",
"supertest": "^6.2.4"
},
"engines": {
"node": "^12.10"