Compare commits

...
12 Commits
Author SHA1 Message Date
Maksim Eltyshev 3ad383fca6 chore: Update version 2023-01-09 12:35:08 +01:00
Maksim Eltyshev 38eab8b166 ref: Prettify eslint-disables 2023-01-09 12:33:49 +01:00
Maksim Eltyshev aa69bb8d1e feat: Labels reordering
Closes #289
2023-01-09 12:17:06 +01:00
dependabot[bot] d627a19935 chore(deps): Bump json5 from 1.0.1 to 1.0.2 in /client (#370)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 02:26:00 +05:00
dependabot[bot] b7cf1853f5 chore(deps): Bump json5 from 1.0.1 to 1.0.2 in /server (#369)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 02:23:01 +05:00
dependabot[bot] 4850fc5fc8 chore(deps): Bump jsonwebtoken from 8.5.1 to 9.0.0 in /server (#357)
Bumps [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) from 8.5.1 to 9.0.0.
- [Release notes](https://github.com/auth0/node-jsonwebtoken/releases)
- [Changelog](https://github.com/auth0/node-jsonwebtoken/blob/master/CHANGELOG.md)
- [Commits](https://github.com/auth0/node-jsonwebtoken/compare/v8.5.1...v9.0.0)

---
updated-dependencies:
- dependency-name: jsonwebtoken
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 02:20:25 +05:00
Maksim Eltyshev 1dfd6972bf Merge branch 'master' of https://github.com/plankanban/planka 2023-01-08 22:11:20 +01:00
Maksim Eltyshev b58b91baa1 ref: Little refactoring 2023-01-08 22:10:41 +01:00
RAR ed89fe5351 feat: Open card once created with Ctrl+Enter (#373) 2023-01-09 01:59:24 +05:00
Maksim Eltyshev 6021d67a00 fix: Subscribe only when needed 2023-01-05 15:03:06 +01:00
RAR 58afeb1e6b fix: Fix npm install errors on Windows (#368) 2023-01-05 18:16:55 +05:00
albanobattistella 7182a09931 fix: Update Italian translation (#363) 2023-01-03 03:16:35 +05:00
69 changed files with 506 additions and 335 deletions
+12 -12
View File
@@ -14958,9 +14958,9 @@
"peer": true "peer": true
}, },
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.1", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": { "bin": {
"json5": "lib/cli.js" "json5": "lib/cli.js"
}, },
@@ -21971,9 +21971,9 @@
} }
}, },
"node_modules/tsconfig-paths/node_modules/json5": { "node_modules/tsconfig-paths/node_modules/json5": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dependencies": { "dependencies": {
"minimist": "^1.2.0" "minimist": "^1.2.0"
}, },
@@ -34502,9 +34502,9 @@
"peer": true "peer": true
}, },
"json5": { "json5": {
"version": "2.2.1", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
}, },
"jsonfile": { "jsonfile": {
"version": "6.1.0", "version": "6.1.0",
@@ -39416,9 +39416,9 @@
}, },
"dependencies": { "dependencies": {
"json5": { "json5": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"requires": { "requires": {
"minimist": "^1.2.0" "minimist": "^1.2.0"
} }
+11 -9
View File
@@ -11,15 +11,17 @@ const createBoard = (projectId, data, headers) =>
const createBoardWithImport = (projectId, data, requestId, headers) => const createBoardWithImport = (projectId, data, requestId, headers) =>
http.post(`/projects/${projectId}/boards?requestId=${requestId}`, data, headers); http.post(`/projects/${projectId}/boards?requestId=${requestId}`, data, headers);
const getBoard = (id, headers) => const getBoard = (id, subscribe, headers) =>
socket.get(`/boards/${id}`, undefined, headers).then((body) => ({ socket
...body, .get(`/boards/${id}${subscribe ? '?subscribe=true' : ''}`, undefined, headers)
included: { .then((body) => ({
...body.included, ...body,
cards: body.included.cards.map(transformCard), included: {
attachments: body.included.attachments.map(transformAttachment), ...body.included,
}, cards: body.included.cards.map(transformCard),
})); attachments: body.included.attachments.map(transformAttachment),
},
}));
const updateBoard = (id, data, headers) => socket.patch(`/boards/${id}`, data, headers); const updateBoard = (id, data, headers) => socket.patch(`/boards/${id}`, data, headers);
+2 -1
View File
@@ -9,7 +9,8 @@ const createUser = (data, headers) => socket.post('/users', data, headers);
const getUser = (id, headers) => socket.get(`/users/${id}`, undefined, headers); const getUser = (id, headers) => socket.get(`/users/${id}`, undefined, headers);
const getCurrentUser = (headers) => socket.get('/users/me', undefined, headers); const getCurrentUser = (subscribe, headers) =>
socket.get(`/users/me${subscribe ? '?subscribe=true' : ''}`, undefined, headers);
const updateUser = (id, data, headers) => socket.patch(`/users/${id}`, data, headers); const updateUser = (id, data, headers) => socket.patch(`/users/${id}`, data, headers);
@@ -25,6 +25,7 @@ const BoardActions = React.memo(
onLabelFromFilterRemove, onLabelFromFilterRemove,
onLabelCreate, onLabelCreate,
onLabelUpdate, onLabelUpdate,
onLabelMove,
onLabelDelete, onLabelDelete,
}) => { }) => {
return ( return (
@@ -54,6 +55,7 @@ const BoardActions = React.memo(
onLabelRemove={onLabelFromFilterRemove} onLabelRemove={onLabelFromFilterRemove}
onLabelCreate={onLabelCreate} onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate} onLabelUpdate={onLabelUpdate}
onLabelMove={onLabelMove}
onLabelDelete={onLabelDelete} onLabelDelete={onLabelDelete}
/> />
</div> </div>
@@ -82,6 +84,7 @@ BoardActions.propTypes = {
onLabelFromFilterRemove: PropTypes.func.isRequired, onLabelFromFilterRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired, onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired, onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired, onLabelDelete: PropTypes.func.isRequired,
}; };
@@ -22,6 +22,7 @@ const Filters = React.memo(
onLabelRemove, onLabelRemove,
onLabelCreate, onLabelCreate,
onLabelUpdate, onLabelUpdate,
onLabelMove,
onLabelDelete, onLabelDelete,
}) => { }) => {
const [t] = useTranslation(); const [t] = useTranslation();
@@ -80,6 +81,7 @@ const Filters = React.memo(
onDeselect={onLabelRemove} onDeselect={onLabelRemove}
onCreate={onLabelCreate} onCreate={onLabelCreate}
onUpdate={onLabelUpdate} onUpdate={onLabelUpdate}
onMove={onLabelMove}
onDelete={onLabelDelete} onDelete={onLabelDelete}
> >
<button type="button" className={styles.filterButton}> <button type="button" className={styles.filterButton}>
@@ -117,6 +119,7 @@ Filters.propTypes = {
onLabelRemove: PropTypes.func.isRequired, onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired, onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired, onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired, onLabelDelete: PropTypes.func.isRequired,
}; };
@@ -45,6 +45,7 @@ const ActionsStep = React.memo(
onLabelRemove, onLabelRemove,
onLabelCreate, onLabelCreate,
onLabelUpdate, onLabelUpdate,
onLabelMove,
onLabelDelete, onLabelDelete,
onClose, onClose,
}) => { }) => {
@@ -119,6 +120,7 @@ const ActionsStep = React.memo(
onDeselect={onLabelRemove} onDeselect={onLabelRemove}
onCreate={onLabelCreate} onCreate={onLabelCreate}
onUpdate={onLabelUpdate} onUpdate={onLabelUpdate}
onMove={onLabelMove}
onDelete={onLabelDelete} onDelete={onLabelDelete}
onBack={handleBack} onBack={handleBack}
/> />
@@ -241,6 +243,7 @@ ActionsStep.propTypes = {
onLabelRemove: PropTypes.func.isRequired, onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired, onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired, onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired, onLabelDelete: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
+3
View File
@@ -48,6 +48,7 @@ const Card = React.memo(
onLabelRemove, onLabelRemove,
onLabelCreate, onLabelCreate,
onLabelUpdate, onLabelUpdate,
onLabelMove,
onLabelDelete, onLabelDelete,
}) => { }) => {
const nameEdit = useRef(null); const nameEdit = useRef(null);
@@ -189,6 +190,7 @@ const Card = React.memo(
onLabelRemove={onLabelRemove} onLabelRemove={onLabelRemove}
onLabelCreate={onLabelCreate} onLabelCreate={onLabelCreate}
onLabelUpdate={onLabelUpdate} onLabelUpdate={onLabelUpdate}
onLabelMove={onLabelMove}
onLabelDelete={onLabelDelete} onLabelDelete={onLabelDelete}
> >
<Button className={classNames(styles.actionsButton, styles.target)}> <Button className={classNames(styles.actionsButton, styles.target)}>
@@ -241,6 +243,7 @@ Card.propTypes = {
onLabelRemove: PropTypes.func.isRequired, onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired, onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired, onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired, onLabelDelete: PropTypes.func.isRequired,
}; };
+2 -4
View File
@@ -22,11 +22,9 @@ const Tasks = React.memo(({ items }) => {
return ( return (
<> <>
{/* eslint-disable jsx-a11y/click-events-have-key-events, {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */} jsx-a11y/no-static-element-interactions */}
<div className={styles.button} onClick={handleToggleClick}> <div className={styles.button} onClick={handleToggleClick}>
{/* eslint-enable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<span className={styles.progressWrapper}> <span className={styles.progressWrapper}>
<Progress <Progress
autoSuccess autoSuccess
@@ -61,11 +61,9 @@ const Item = React.forwardRef(
const extension = filename.slice((Math.max(0, filename.lastIndexOf('.')) || Infinity) + 1); const extension = filename.slice((Math.max(0, filename.lastIndexOf('.')) || Infinity) + 1);
return ( return (
/* eslint-disable jsx-a11y/click-events-have-key-events, /* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */ jsx-a11y/no-static-element-interactions */
<div ref={ref} 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 <div
className={styles.thumbnail} className={styles.thumbnail}
style={{ style={{
@@ -62,6 +62,7 @@ const CardModal = React.memo(
onLabelRemove, onLabelRemove,
onLabelCreate, onLabelCreate,
onLabelUpdate, onLabelUpdate,
onLabelMove,
onLabelDelete, onLabelDelete,
onTaskCreate, onTaskCreate,
onTaskUpdate, onTaskUpdate,
@@ -235,6 +236,7 @@ const CardModal = React.memo(
onDeselect={onLabelRemove} onDeselect={onLabelRemove}
onCreate={onLabelCreate} onCreate={onLabelCreate}
onUpdate={onLabelUpdate} onUpdate={onLabelUpdate}
onMove={onLabelMove}
onDelete={onLabelDelete} onDelete={onLabelDelete}
> >
<Label name={label.name} color={label.color} /> <Label name={label.name} color={label.color} />
@@ -252,6 +254,7 @@ const CardModal = React.memo(
onDeselect={onLabelRemove} onDeselect={onLabelRemove}
onCreate={onLabelCreate} onCreate={onLabelCreate}
onUpdate={onLabelUpdate} onUpdate={onLabelUpdate}
onMove={onLabelMove}
onDelete={onLabelDelete} onDelete={onLabelDelete}
> >
<button <button
@@ -419,6 +422,7 @@ const CardModal = React.memo(
onDeselect={onLabelRemove} onDeselect={onLabelRemove}
onCreate={onLabelCreate} onCreate={onLabelCreate}
onUpdate={onLabelUpdate} onUpdate={onLabelUpdate}
onMove={onLabelMove}
onDelete={onLabelDelete} onDelete={onLabelDelete}
> >
<Button fluid className={styles.actionButton}> <Button fluid className={styles.actionButton}>
@@ -546,6 +550,7 @@ CardModal.propTypes = {
onLabelRemove: PropTypes.func.isRequired, onLabelRemove: PropTypes.func.isRequired,
onLabelCreate: PropTypes.func.isRequired, onLabelCreate: PropTypes.func.isRequired,
onLabelUpdate: PropTypes.func.isRequired, onLabelUpdate: PropTypes.func.isRequired,
onLabelMove: PropTypes.func.isRequired,
onLabelDelete: PropTypes.func.isRequired, onLabelDelete: PropTypes.func.isRequired,
onTaskCreate: PropTypes.func.isRequired, onTaskCreate: PropTypes.func.isRequired,
onTaskUpdate: PropTypes.func.isRequired, onTaskUpdate: PropTypes.func.isRequired,
@@ -55,14 +55,12 @@ const Item = React.memo(
</span> </span>
<NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}> <NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}>
<div className={classNames(canEdit && styles.contentHoverable)}> <div className={classNames(canEdit && styles.contentHoverable)}>
{/* eslint-disable jsx-a11y/click-events-have-key-events, {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */} jsx-a11y/no-static-element-interactions */}
<span <span
className={classNames(styles.text, canEdit && styles.textEditable)} className={classNames(styles.text, canEdit && styles.textEditable)}
onClick={handleClick} 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)}> <span className={classNames(styles.task, isCompleted && styles.taskCompleted)}>
{name} {name}
</span> </span>
+46 -30
View File
@@ -1,53 +1,69 @@
import upperFirst from 'lodash/upperFirst'; import upperFirst from 'lodash/upperFirst';
import camelCase from 'lodash/camelCase'; import camelCase from 'lodash/camelCase';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { Draggable } from 'react-beautiful-dnd';
import { Button } from 'semantic-ui-react'; import { Button } from 'semantic-ui-react';
import styles from './Item.module.scss'; import styles from './Item.module.scss';
import globalStyles from '../../styles.module.scss'; import globalStyles from '../../styles.module.scss';
const Item = React.memo( const Item = React.memo(
({ name, color, isPersisted, isActive, canEdit, onSelect, onDeselect, onEdit }) => { ({ id, index, name, color, isPersisted, isActive, canEdit, onSelect, onDeselect, onEdit }) => {
const handleToggleClick = useCallback(() => { const handleToggleClick = useCallback(() => {
if (isActive) { if (isPersisted) {
onDeselect(); if (isActive) {
} else { onDeselect();
onSelect(); } else {
onSelect();
}
} }
}, [isActive, onSelect, onDeselect]); }, [isPersisted, isActive, onSelect, onDeselect]);
return ( return (
<div className={styles.wrapper}> <Draggable draggableId={id} index={index} isDragDisabled={!isPersisted || !canEdit}>
<Button {({ innerRef, draggableProps, dragHandleProps }, { isDragging }) => {
fluid const contentNode = (
content={name} // eslint-disable-next-line react/jsx-props-no-spreading
active={isActive} <div {...draggableProps} ref={innerRef} className={styles.wrapper}>
disabled={!isPersisted} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
className={classNames( jsx-a11y/no-static-element-interactions */}
styles.labelButton, <span
isActive && styles.labelButtonActive, {...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
globalStyles[`background${upperFirst(camelCase(color))}`], className={classNames(
)} styles.name,
onClick={handleToggleClick} isActive && styles.nameActive,
/> globalStyles[`background${upperFirst(camelCase(color))}`],
{canEdit && ( )}
<Button onClick={handleToggleClick}
icon="pencil" >
size="small" {name}
floated="right" </span>
disabled={!isPersisted} {canEdit && (
className={styles.editButton} <Button
onClick={onEdit} icon="pencil"
/> size="small"
)} floated="right"
</div> disabled={!isPersisted}
className={styles.editButton}
onClick={onEdit}
/>
)}
</div>
);
return isDragging ? ReactDOM.createPortal(contentNode, document.body) : contentNode;
}}
</Draggable>
); );
}, },
); );
Item.propTypes = { Item.propTypes = {
id: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
name: PropTypes.string, name: PropTypes.string,
color: PropTypes.string.isRequired, color: PropTypes.string.isRequired,
isPersisted: PropTypes.bool.isRequired, isPersisted: PropTypes.bool.isRequired,
@@ -12,16 +12,15 @@
} }
} }
.labelButton { .name {
border-radius: 3px;
color: #fff; color: #fff;
cursor: pointer;
flex: 1 1 auto; flex: 1 1 auto;
font-size: 14px;
font-weight: bold; font-weight: bold;
margin-right: 0;
overflow: hidden; overflow: hidden;
padding: 8px 32px 8px 10px; padding: 8px 32px 8px 10px;
position: relative; position: relative;
text-align: left;
text-overflow: ellipsis; text-overflow: ellipsis;
text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2); text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2);
@@ -30,7 +29,7 @@
} }
} }
.labelButtonActive:before { .nameActive:before {
bottom: 1px; bottom: 1px;
content: "Г"; content: "Г";
font-size: 18px; font-size: 18px;
+54 -15
View File
@@ -2,10 +2,12 @@ import pick from 'lodash/pick';
import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { Button } from 'semantic-ui-react'; import { Button } from 'semantic-ui-react';
import { Input, Popup } from '../../lib/custom-ui'; import { Input, Popup } from '../../lib/custom-ui';
import { useField, useSteps } from '../../hooks'; import { useField, useSteps } from '../../hooks';
import DroppableTypes from '../../constants/DroppableTypes';
import AddStep from './AddStep'; import AddStep from './AddStep';
import EditStep from './EditStep'; import EditStep from './EditStep';
import Item from './Item'; import Item from './Item';
@@ -27,6 +29,7 @@ const LabelsStep = React.memo(
onDeselect, onDeselect,
onCreate, onCreate,
onUpdate, onUpdate,
onMove,
onDelete, onDelete,
onBack, onBack,
}) => { }) => {
@@ -74,6 +77,17 @@ const LabelsStep = React.memo(
[onDeselect], [onDeselect],
); );
const handleDragEnd = useCallback(
({ draggableId, source, destination }) => {
if (!destination || source.index === destination.index) {
return;
}
onMove(draggableId, destination.index);
},
[onMove],
);
const handleUpdate = useCallback( const handleUpdate = useCallback(
(id, data) => { (id, data) => {
onUpdate(id, data); onUpdate(id, data);
@@ -145,21 +159,45 @@ const LabelsStep = React.memo(
onChange={handleSearchChange} onChange={handleSearchChange}
/> />
{filteredItems.length > 0 && ( {filteredItems.length > 0 && (
<div className={styles.items}> <DragDropContext onDragEnd={handleDragEnd}>
{filteredItems.map((item) => ( <Droppable droppableId="labels" type={DroppableTypes.LABEL}>
<Item {({ innerRef, droppableProps, placeholder }) => (
key={item.id} <div
name={item.name} {...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
color={item.color} ref={innerRef}
isPersisted={item.isPersisted} className={styles.items}
isActive={currentIds.includes(item.id)} >
canEdit={canEdit} {filteredItems.map((item, index) => (
onSelect={() => handleSelect(item.id)} <Item
onDeselect={() => handleDeselect(item.id)} key={item.id}
onEdit={() => handleEdit(item.id)} id={item.id}
/> index={index}
))} name={item.name}
</div> color={item.color}
isPersisted={item.isPersisted}
isActive={currentIds.includes(item.id)}
canEdit={canEdit}
onSelect={() => handleSelect(item.id)}
onDeselect={() => handleDeselect(item.id)}
onEdit={() => handleEdit(item.id)}
/>
))}
{placeholder}
</div>
)}
</Droppable>
<Droppable droppableId="labels:hack" type={DroppableTypes.LABEL}>
{({ innerRef, droppableProps, placeholder }) => (
<div
{...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
ref={innerRef}
className={styles.droppableHack}
>
{placeholder}
</div>
)}
</Droppable>
</DragDropContext>
)} )}
{canEdit && ( {canEdit && (
<Button <Button
@@ -186,6 +224,7 @@ LabelsStep.propTypes = {
onDeselect: PropTypes.func.isRequired, onDeselect: PropTypes.func.isRequired,
onCreate: PropTypes.func.isRequired, onCreate: PropTypes.func.isRequired,
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onMove: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onBack: PropTypes.func, onBack: PropTypes.func,
}; };
@@ -15,6 +15,11 @@
} }
} }
.droppableHack {
display: none;
position: fixed;
}
.items { .items {
margin-top: 8px; margin-top: 8px;
max-height: 60vh; max-height: 60vh;
+26 -16
View File
@@ -21,36 +21,46 @@ const CardAdd = React.memo(({ isOpened, onCreate, onClose }) => {
const nameField = useRef(null); const nameField = useRef(null);
const submit = useCallback(() => { const submit = useCallback(
const cleanData = { (autoOpen) => {
...data, const cleanData = {
name: data.name.trim(), ...data,
}; name: data.name.trim(),
};
if (!cleanData.name) { if (!cleanData.name) {
nameField.current.ref.current.select(); nameField.current.ref.current.select();
return; return;
} }
onCreate(cleanData); onCreate(cleanData, autoOpen);
setData(DEFAULT_DATA);
setData(DEFAULT_DATA); if (autoOpen) {
focusNameField(); onClose();
}, [onCreate, data, setData, focusNameField]); } else {
focusNameField();
}
},
[onCreate, onClose, data, setData, focusNameField],
);
const handleFieldKeyDown = useCallback( const handleFieldKeyDown = useCallback(
(event) => { (event) => {
switch (event.key) { switch (event.key) {
case 'Enter': case 'Enter': {
event.preventDefault(); event.preventDefault();
submit(); const autoOpen = event.ctrlKey;
submit(autoOpen);
break; break;
case 'Escape': }
case 'Escape': {
onClose(); onClose();
break; break;
}
default: default:
} }
}, },
+3 -7
View File
@@ -95,18 +95,14 @@ const List = React.memo(
ref={innerRef} ref={innerRef}
className={styles.innerWrapper} className={styles.innerWrapper}
> >
{/* eslint-disable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions,
react/jsx-props-no-spreading */}
<div className={styles.outerWrapper}> <div className={styles.outerWrapper}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */}
<div <div
{...dragHandleProps} {...dragHandleProps} // eslint-disable-line react/jsx-props-no-spreading
className={classNames(styles.header, canEdit && styles.headerEditable)} className={classNames(styles.header, canEdit && styles.headerEditable)}
onClick={handleHeaderClick} onClick={handleHeaderClick}
> >
{/* eslint-enable jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions,
react/jsx-props-no-spreading */}
<NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}> <NameEdit ref={nameEdit} defaultValue={name} onUpdate={handleNameUpdate}>
<div className={styles.headerName}>{name}</div> <div className={styles.headerName}>{name}</div>
</NameEdit> </NameEdit>
+2
View File
@@ -1,10 +1,12 @@
const BOARD = 'BOARD'; const BOARD = 'BOARD';
const LABEL = 'LABEL';
const LIST = 'LIST'; const LIST = 'LIST';
const CARD = 'CARD'; const CARD = 'CARD';
const TASK = 'TASK'; const TASK = 'TASK';
export default { export default {
BOARD, BOARD,
LABEL,
LIST, LIST,
CARD, CARD,
TASK, TASK,
+1
View File
@@ -99,6 +99,7 @@ export default {
LABEL_CREATE_HANDLE: `${PREFIX}/LABEL_CREATE_HANDLE`, LABEL_CREATE_HANDLE: `${PREFIX}/LABEL_CREATE_HANDLE`,
LABEL_UPDATE: `${PREFIX}/LABEL_UPDATE`, LABEL_UPDATE: `${PREFIX}/LABEL_UPDATE`,
LABEL_UPDATE_HANDLE: `${PREFIX}/LABEL_UPDATE_HANDLE`, LABEL_UPDATE_HANDLE: `${PREFIX}/LABEL_UPDATE_HANDLE`,
LABEL_MOVE: `${PREFIX}/LABEL_MOVE`,
LABEL_DELETE: `${PREFIX}/LABEL_DELETE`, LABEL_DELETE: `${PREFIX}/LABEL_DELETE`,
LABEL_DELETE_HANDLE: `${PREFIX}/LABEL_DELETE_HANDLE`, LABEL_DELETE_HANDLE: `${PREFIX}/LABEL_DELETE_HANDLE`,
LABEL_TO_CARD_ADD: `${PREFIX}/LABEL_TO_CARD_ADD`, LABEL_TO_CARD_ADD: `${PREFIX}/LABEL_TO_CARD_ADD`,
@@ -41,6 +41,7 @@ const mapDispatchToProps = (dispatch) =>
onLabelFromFilterRemove: entryActions.removeLabelFromFilterInCurrentBoard, onLabelFromFilterRemove: entryActions.removeLabelFromFilterInCurrentBoard,
onLabelCreate: entryActions.createLabelInCurrentBoard, onLabelCreate: entryActions.createLabelInCurrentBoard,
onLabelUpdate: entryActions.updateLabel, onLabelUpdate: entryActions.updateLabel,
onLabelMove: entryActions.moveLabel,
onLabelDelete: entryActions.deleteLabel, onLabelDelete: entryActions.deleteLabel,
}, },
dispatch, dispatch,
+1
View File
@@ -70,6 +70,7 @@ const mapDispatchToProps = (dispatch, { id }) =>
onLabelRemove: (labelId) => entryActions.removeLabelFromCard(labelId, id), onLabelRemove: (labelId) => entryActions.removeLabelFromCard(labelId, id),
onLabelCreate: (data) => entryActions.createLabelInCurrentBoard(data), onLabelCreate: (data) => entryActions.createLabelInCurrentBoard(data),
onLabelUpdate: (labelId, data) => entryActions.updateLabel(labelId, data), onLabelUpdate: (labelId, data) => entryActions.updateLabel(labelId, data),
onLabelMove: (labelId, index) => entryActions.moveLabel(labelId, index),
onLabelDelete: (labelId) => entryActions.deleteLabel(labelId), onLabelDelete: (labelId) => entryActions.deleteLabel(labelId),
}, },
dispatch, dispatch,
@@ -86,6 +86,7 @@ const mapDispatchToProps = (dispatch) =>
onLabelRemove: entryActions.removeLabelFromCurrentCard, onLabelRemove: entryActions.removeLabelFromCurrentCard,
onLabelCreate: entryActions.createLabelInCurrentBoard, onLabelCreate: entryActions.createLabelInCurrentBoard,
onLabelUpdate: entryActions.updateLabel, onLabelUpdate: entryActions.updateLabel,
onLabelMove: entryActions.moveLabel,
onLabelDelete: entryActions.deleteLabel, onLabelDelete: entryActions.deleteLabel,
onTaskCreate: entryActions.createTaskInCurrentCard, onTaskCreate: entryActions.createTaskInCurrentCard,
onTaskUpdate: entryActions.updateTask, onTaskUpdate: entryActions.updateTask,
+1 -1
View File
@@ -34,7 +34,7 @@ const mapDispatchToProps = (dispatch, { id }) =>
{ {
onUpdate: (data) => entryActions.updateList(id, data), onUpdate: (data) => entryActions.updateList(id, data),
onDelete: () => entryActions.deleteList(id), onDelete: () => entryActions.deleteList(id),
onCardCreate: (data) => entryActions.createCard(id, data), onCardCreate: (data, autoOpen) => entryActions.createCard(id, data, autoOpen),
}, },
dispatch, dispatch,
); );
+2 -1
View File
@@ -1,10 +1,11 @@
import EntryActionTypes from '../constants/EntryActionTypes'; import EntryActionTypes from '../constants/EntryActionTypes';
const createCard = (listId, data) => ({ const createCard = (listId, data, autoOpen) => ({
type: EntryActionTypes.CARD_CREATE, type: EntryActionTypes.CARD_CREATE,
payload: { payload: {
listId, listId,
data, data,
autoOpen,
}, },
}); });
+9
View File
@@ -29,6 +29,14 @@ const handleLabelUpdate = (label) => ({
}, },
}); });
const moveLabel = (id, index) => ({
type: EntryActionTypes.LABEL_MOVE,
payload: {
id,
index,
},
});
const deleteLabel = (id) => ({ const deleteLabel = (id) => ({
type: EntryActionTypes.LABEL_DELETE, type: EntryActionTypes.LABEL_DELETE,
payload: { payload: {
@@ -106,6 +114,7 @@ export default {
handleLabelCreate, handleLabelCreate,
updateLabel, updateLabel,
handleLabelUpdate, handleLabelUpdate,
moveLabel,
deleteLabel, deleteLabel,
handleLabelDelete, handleLabelDelete,
addLabelToCard, addLabelToCard,
@@ -14,13 +14,12 @@ const Markdown = React.memo(({ linkStopPropagation, ...props }) => {
}, []); }, []);
const linkRenderer = useCallback( const linkRenderer = useCallback(
/* eslint-disable jsx-a11y/anchor-has-content,
jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions,
react/jsx-props-no-spreading */
({ node, ...linkProps }) => ( ({ node, ...linkProps }) => (
/* eslint-disable-next-line jsx-a11y/anchor-has-content,
jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */
<a <a
{...linkProps} {...linkProps} // eslint-disable-line react/jsx-props-no-spreading
rel={ rel={
ABSOLUTE_URL_REGEX.test(linkProps.href) && linkProps.target === '_blank' ABSOLUTE_URL_REGEX.test(linkProps.href) && linkProps.target === '_blank'
? 'noreferrer' ? 'noreferrer'
@@ -29,10 +28,6 @@ const Markdown = React.memo(({ linkStopPropagation, ...props }) => {
onClick={linkStopPropagation ? handleLinkClick : undefined} onClick={linkStopPropagation ? handleLinkClick : undefined}
/> />
), ),
/* eslint-enable jsx-a11y/anchor-has-content,
jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions,
react/jsx-props-no-spreading */
[linkStopPropagation, handleLinkClick], [linkStopPropagation, handleLinkClick],
); );
+1 -1
View File
@@ -84,7 +84,7 @@ export default {
editUsername_title: 'Edit Username', editUsername_title: 'Edit Username',
email: 'E-mail', email: 'E-mail',
emailAlreadyInUse: 'E-mail already in use', emailAlreadyInUse: 'E-mail already in use',
enterCardTitle: 'Enter card title...', enterCardTitle: 'Enter card title... [Ctrl+Enter] to auto-open',
enterDescription: 'Enter description...', enterDescription: 'Enter description...',
enterFilename: 'Enter filename', enterFilename: 'Enter filename',
enterListTitle: 'Enter list title...', enterListTitle: 'Enter list title...',
+5 -5
View File
@@ -54,7 +54,7 @@ export default {
createProject_title: 'Crea Progetto', createProject_title: 'Crea Progetto',
createTextFile_title: 'Create File di Testo', createTextFile_title: 'Create File di Testo',
currentPassword: 'Password corrente', currentPassword: 'Password corrente',
dangerZone_title: 'Danger Zone', dangerZone_title: 'Zona pericolosa',
date: 'Data', date: 'Data',
dueDate_title: 'Data di scadenza', dueDate_title: 'Data di scadenza',
deleteAttachment_title: 'Elimina Allegato', deleteAttachment_title: 'Elimina Allegato',
@@ -219,10 +219,10 @@ export default {
showAllAttachments: 'Mostra tutti gli allegati ({{hidden}} hidden)', showAllAttachments: 'Mostra tutti gli allegati ({{hidden}} hidden)',
showDetails: 'Mostra dettagli', showDetails: 'Mostra dettagli',
showFewerAttachments: 'Mostra meno allegati', showFewerAttachments: 'Mostra meno allegati',
start: 'Start', start: 'Inizio',
stop: 'Stop', stop: 'Ferma',
subscribe: 'Subscribe', subscribe: 'Iscriviti',
unsubscribe: 'Unsubscribe', unsubscribe: 'Annulla iscrizione',
uploadNewAvatar: 'Carica nuovo avatar', uploadNewAvatar: 'Carica nuovo avatar',
uploadNewImage: 'Carica nuova immagine', uploadNewImage: 'Carica nuova immagine',
}, },
+2 -2
View File
@@ -2,13 +2,13 @@ export default {
translation: { translation: {
common: { common: {
emailOrUsername: 'E-mail o username', emailOrUsername: 'E-mail o username',
invalidEmailOrUsername: 'E-mail or username invalido', invalidEmailOrUsername: 'E-mail o username non valido',
invalidPassword: 'Password non valida', invalidPassword: 'Password non valida',
logInToPlanka: 'Log in Planka', logInToPlanka: 'Log in Planka',
noInternetConnection: 'Nessuna connessione internet', noInternetConnection: 'Nessuna connessione internet',
pageNotFound_title: 'Pagina non trovata', pageNotFound_title: 'Pagina non trovata',
password: 'Password', password: 'Password',
projectManagement: 'Project management', projectManagement: 'Gestione del progetto',
serverConnectionFailed: 'Connesione al server fallita', serverConnectionFailed: 'Connesione al server fallita',
unknownError: 'Errore sconosciuto, prova ancora', unknownError: 'Errore sconosciuto, prova ancora',
}, },
+4
View File
@@ -175,6 +175,10 @@ export default class extends BaseModel {
return this.memberships.orderBy('id'); return this.memberships.orderBy('id');
} }
getOrderedLabelsQuerySet() {
return this.labels.orderBy('position');
}
getOrderedListsQuerySet() { getOrderedListsQuerySet() {
return this.lists.orderBy('position'); return this.lists.orderBy('position');
} }
+1
View File
@@ -8,6 +8,7 @@ export default class extends BaseModel {
static fields = { static fields = {
id: attr(), id: attr(),
position: attr(),
name: attr(), name: attr(),
color: attr(), color: attr(),
boardId: fk({ boardId: fk({
+1 -1
View File
@@ -47,7 +47,7 @@ export function* fetchBoardByCurrentPath() {
tasks, tasks,
attachments, attachments,
}, },
} = yield call(request, api.getBoard, boardId)); } = yield call(request, api.getBoard, boardId, true));
} }
} }
+1 -1
View File
@@ -6,7 +6,7 @@ import api from '../../../api';
import mergeRecords from '../../../utils/merge-records'; import mergeRecords from '../../../utils/merge-records';
export function* fetchCore() { export function* fetchCore() {
const { item: user } = yield call(request, api.getCurrentUser); const { item: user } = yield call(request, api.getCurrentUser, true);
const { items: users1 } = yield call(request, api.getUsers); const { items: users1 } = yield call(request, api.getUsers);
const { const {
@@ -62,7 +62,7 @@ export function* handleBoardMembershipCreate(boardMembership) {
if (isCurrentUser) { if (isCurrentUser) {
let board2; let board2;
try { try {
({ item: board2 } = yield call(request, api.getBoard, boardMembership.boardId)); ({ item: board2 } = yield call(request, api.getBoard, boardMembership.boardId, false));
} catch { } catch {
return; return;
} }
+1 -1
View File
@@ -96,7 +96,7 @@ export function* fetchBoard(id) {
tasks, tasks,
attachments, attachments,
}, },
} = yield call(request, api.getBoard, id)); } = yield call(request, api.getBoard, id, true));
} catch (error) { } catch (error) {
yield put(actions.fetchBoard.failure(id, error)); yield put(actions.fetchBoard.failure(id, error));
return; return;
+6 -2
View File
@@ -1,13 +1,13 @@
import { call, put, select } from 'redux-saga/effects'; import { call, put, select } from 'redux-saga/effects';
import { goToBoard } from './router'; import { goToBoard, goToCard } from './router';
import request from '../request'; import request from '../request';
import selectors from '../../../selectors'; import selectors from '../../../selectors';
import actions from '../../../actions'; import actions from '../../../actions';
import api from '../../../api'; import api from '../../../api';
import { createLocalId } from '../../../utils/local-id'; import { createLocalId } from '../../../utils/local-id';
export function* createCard(listId, data) { export function* createCard(listId, data, autoOpen) {
const { boardId } = yield select(selectors.selectListById, listId); const { boardId } = yield select(selectors.selectListById, listId);
const nextData = { const nextData = {
@@ -35,6 +35,10 @@ export function* createCard(listId, data) {
} }
yield put(actions.createCard.success(localId, card)); yield put(actions.createCard.success(localId, card));
if (autoOpen) {
yield call(goToCard, card.id);
}
} }
export function* handleCardCreate(card) { export function* handleCardCreate(card) {
+17 -2
View File
@@ -7,11 +7,16 @@ import api from '../../../api';
import { createLocalId } from '../../../utils/local-id'; import { createLocalId } from '../../../utils/local-id';
export function* createLabel(boardId, data) { export function* createLabel(boardId, data) {
const nextData = {
...data,
position: yield select(selectors.selectNextLabelPosition, boardId),
};
const localId = yield call(createLocalId); const localId = yield call(createLocalId);
yield put( yield put(
actions.createLabel({ actions.createLabel({
...data, ...nextData,
boardId, boardId,
id: localId, id: localId,
}), }),
@@ -19,7 +24,7 @@ export function* createLabel(boardId, data) {
let label; let label;
try { try {
({ item: label } = yield call(request, api.createLabel, boardId, data)); ({ item: label } = yield call(request, api.createLabel, boardId, nextData));
} catch (error) { } catch (error) {
yield put(actions.createLabel.failure(localId, error)); yield put(actions.createLabel.failure(localId, error));
return; return;
@@ -56,6 +61,15 @@ export function* handleLabelUpdate(label) {
yield put(actions.handleLabelUpdate(label)); yield put(actions.handleLabelUpdate(label));
} }
export function* moveLabel(id, index) {
const { boardId } = yield select(selectors.selectLabelById, id);
const position = yield select(selectors.selectNextLabelPosition, boardId, index, id);
yield call(updateLabel, id, {
position,
});
}
export function* deleteLabel(id) { export function* deleteLabel(id) {
yield put(actions.deleteLabel(id)); yield put(actions.deleteLabel(id));
@@ -150,6 +164,7 @@ export default {
handleLabelCreate, handleLabelCreate,
updateLabel, updateLabel,
handleLabelUpdate, handleLabelUpdate,
moveLabel,
deleteLabel, deleteLabel,
handleLabelDelete, handleLabelDelete,
addLabelToCard, addLabelToCard,
+1 -1
View File
@@ -81,7 +81,7 @@ export function* handleLocationChange() {
tasks, tasks,
attachments, attachments,
}, },
} = yield call(request, api.getBoard, currentBoard.id)); } = yield call(request, api.getBoard, currentBoard.id, true));
} catch (error) {} // eslint-disable-line no-empty } catch (error) {} // eslint-disable-line no-empty
} }
+2 -2
View File
@@ -5,8 +5,8 @@ import EntryActionTypes from '../../../constants/EntryActionTypes';
export default function* cardsWatchers() { export default function* cardsWatchers() {
yield all([ yield all([
takeEvery(EntryActionTypes.CARD_CREATE, ({ payload: { listId, data } }) => takeEvery(EntryActionTypes.CARD_CREATE, ({ payload: { listId, data, autoOpen } }) =>
services.createCard(listId, data), services.createCard(listId, data, autoOpen),
), ),
takeEvery(EntryActionTypes.CARD_CREATE_HANDLE, ({ payload: { card } }) => takeEvery(EntryActionTypes.CARD_CREATE_HANDLE, ({ payload: { card } }) =>
services.handleCardCreate(card), services.handleCardCreate(card),
+3
View File
@@ -17,6 +17,9 @@ export default function* labelsWatchers() {
takeEvery(EntryActionTypes.LABEL_UPDATE_HANDLE, ({ payload: { label } }) => takeEvery(EntryActionTypes.LABEL_UPDATE_HANDLE, ({ payload: { label } }) =>
services.handleLabelUpdate(label), services.handleLabelUpdate(label),
), ),
takeEvery(EntryActionTypes.LABEL_MOVE, ({ payload: { id, index } }) =>
services.moveLabel(id, index),
),
takeEvery(EntryActionTypes.LABEL_DELETE, ({ payload: { id } }) => services.deleteLabel(id)), takeEvery(EntryActionTypes.LABEL_DELETE, ({ payload: { id } }) => services.deleteLabel(id)),
takeEvery(EntryActionTypes.LABEL_DELETE_HANDLE, ({ payload: { label } }) => takeEvery(EntryActionTypes.LABEL_DELETE_HANDLE, ({ payload: { label } }) =>
services.handleLabelDelete(label), services.handleLabelDelete(label),
+7 -4
View File
@@ -108,10 +108,13 @@ export const selectLabelsForCurrentBoard = createSelector(
return boardModel; return boardModel;
} }
return boardModel.labels.toRefArray().map((label) => ({ return boardModel
...label, .getOrderedLabelsQuerySet()
isPersisted: !isLocalId(label.id), .toRefArray()
})); .map((label) => ({
...label,
isPersisted: !isLocalId(label.id),
}));
}, },
); );
+17
View File
@@ -49,6 +49,22 @@ export const selectNextBoardPosition = createSelector(
}, },
); );
export const selectNextLabelPosition = createSelector(
orm,
(_, boardId) => boardId,
(_, __, index) => index,
(_, __, ___, excludedId) => excludedId,
({ Board }, boardId, index, excludedId) => {
const boardModel = Board.withId(boardId);
if (!boardModel) {
return boardModel;
}
return nextPosition(boardModel.getOrderedLabelsQuerySet().toRefArray(), index, excludedId);
},
);
export const selectNextListPosition = createSelector( export const selectNextListPosition = createSelector(
orm, orm,
(_, boardId) => boardId, (_, boardId) => boardId,
@@ -102,6 +118,7 @@ export default {
selectIsCoreInitializing, selectIsCoreInitializing,
selectIsLogouting, selectIsLogouting,
selectNextBoardPosition, selectNextBoardPosition,
selectNextLabelPosition,
selectNextListPosition, selectNextListPosition,
selectNextCardPosition, selectNextCardPosition,
selectNextTaskPosition, selectNextTaskPosition,
+2
View File
@@ -7,6 +7,7 @@ import projects from './projects';
import projectManagers from './project-managers'; import projectManagers from './project-managers';
import boards from './boards'; import boards from './boards';
import boardMemberships from './board-memberships'; import boardMemberships from './board-memberships';
import labels from './labels';
import lists from './lists'; import lists from './lists';
import cards from './cards'; import cards from './cards';
import tasks from './tasks'; import tasks from './tasks';
@@ -22,6 +23,7 @@ export default {
...projectManagers, ...projectManagers,
...boards, ...boards,
...boardMemberships, ...boardMemberships,
...labels,
...lists, ...lists,
...cards, ...cards,
...tasks, ...tasks,
+25
View File
@@ -0,0 +1,25 @@
import { createSelector } from 'redux-orm';
import orm from '../orm';
export const makeSelectLabelById = () =>
createSelector(
orm,
(_, id) => id,
({ Label }, id) => {
const labelModel = Label.withId(id);
if (!labelModel) {
return labelModel;
}
return labelModel.ref;
},
);
export const selectLabelById = makeSelectLabelById();
export default {
makeSelectLabelById,
selectLabelById,
};
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "planka", "name": "planka",
"version": "1.9.2", "version": "1.10.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "planka", "name": "planka",
"version": "1.9.2", "version": "1.10.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "planka", "name": "planka",
"version": "1.9.2", "version": "1.10.0",
"private": true, "private": true,
"homepage": "https://plankanban.github.io/planka", "homepage": "https://plankanban.github.io/planka",
"repository": { "repository": {
@@ -16,7 +16,7 @@
"client:test": "npm test --prefix client", "client:test": "npm test --prefix client",
"docker:build": "docker build -t ghcr.io/plankanban/planka:local -f Dockerfile .", "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 .", "docker:build:base": "docker build -t ghcr.io/plankanban/planka:base-local -f Dockerfile.base .",
"postinstall": "npm i --prefix server && npm i --prefix client", "postinstall": "(cd server && npm i && cd ../client && npm i)",
"lint": "npm run server:lint && npm run client:lint", "lint": "npm run server:lint && npm run client:lint",
"prepare": "husky install", "prepare": "husky install",
"server:console": "npm run console --prefix server", "server:console": "npm run console --prefix server",
-4
View File
@@ -108,10 +108,6 @@ module.exports = {
request: this.req, request: this.req,
}); });
if (this.req.isSocket) {
sails.sockets.join(this.req, `board:${board.id}`); // TODO: only when subscription needed
}
return { return {
item: board, item: board,
included: { included: {
+5 -2
View File
@@ -11,6 +11,9 @@ module.exports = {
regex: /^[0-9]+$/, regex: /^[0-9]+$/,
required: true, required: true,
}, },
subscribe: {
type: 'boolean',
},
}, },
exits: { exits: {
@@ -73,8 +76,8 @@ module.exports = {
card.isSubscribed = isSubscribedByCardId[card.id] || false; card.isSubscribed = isSubscribedByCardId[card.id] || false;
}); });
if (this.req.isSocket) { if (inputs.subscribe && this.req.isSocket) {
sails.sockets.join(this.req, `board:${board.id}`); // TODO: only when subscription needed sails.sockets.join(this.req, `board:${board.id}`);
} }
return { return {
+5 -1
View File
@@ -14,6 +14,10 @@ module.exports = {
regex: /^[0-9]+$/, regex: /^[0-9]+$/,
required: true, required: true,
}, },
position: {
type: 'number',
required: true,
},
name: { name: {
type: 'string', type: 'string',
isNotEmptyString: true, isNotEmptyString: true,
@@ -55,7 +59,7 @@ module.exports = {
throw Errors.NOT_ENOUGH_RIGHTS; throw Errors.NOT_ENOUGH_RIGHTS;
} }
const values = _.pick(inputs, ['name', 'color']); const values = _.pick(inputs, ['position', 'name', 'color']);
const label = await sails.helpers.labels.createOne.with({ const label = await sails.helpers.labels.createOne.with({
values: { values: {
+4 -2
View File
@@ -14,6 +14,9 @@ module.exports = {
regex: /^[0-9]+$/, regex: /^[0-9]+$/,
required: true, required: true,
}, },
position: {
type: 'number',
},
name: { name: {
type: 'string', type: 'string',
isNotEmptyString: true, isNotEmptyString: true,
@@ -22,7 +25,6 @@ module.exports = {
color: { color: {
type: 'string', type: 'string',
isIn: Label.COLORS, isIn: Label.COLORS,
required: true,
}, },
}, },
@@ -55,7 +57,7 @@ module.exports = {
throw Errors.NOT_ENOUGH_RIGHTS; throw Errors.NOT_ENOUGH_RIGHTS;
} }
const values = _.pick(inputs, ['name', 'color']); const values = _.pick(inputs, ['position', 'name', 'color']);
label = await sails.helpers.labels.updateOne.with({ label = await sails.helpers.labels.updateOne.with({
values, values,
+5 -2
View File
@@ -13,6 +13,9 @@ module.exports = {
regex: /^[0-9]+|me$/, regex: /^[0-9]+|me$/,
required: true, required: true,
}, },
subscribe: {
type: 'boolean',
},
}, },
exits: { exits: {
@@ -26,8 +29,8 @@ module.exports = {
if (inputs.id === CURRENT_USER_ID) { if (inputs.id === CURRENT_USER_ID) {
({ currentUser: user } = this.req); ({ currentUser: user } = this.req);
if (this.req.isSocket) { if (inputs.subscribe && this.req.isSocket) {
sails.sockets.join(this.req, `user:${user.id}`); // TODO: only when subscription needed sails.sockets.join(this.req, `user:${user.id}`);
} }
} else { } else {
user = await sails.helpers.users.getOne(inputs.id); user = await sails.helpers.users.getOne(inputs.id);
+14 -2
View File
@@ -7,11 +7,23 @@ module.exports = {
custom: idOrIdsValidator, custom: idOrIdsValidator,
required: true, required: true,
}, },
exceptLabelIdOrIds: {
type: 'json',
custom: idOrIdsValidator,
},
}, },
async fn(inputs) { async fn(inputs) {
return sails.helpers.labels.getMany({ const criteria = {
boardId: inputs.idOrIds, boardId: inputs.idOrIds,
}); };
if (!_.isUndefined(inputs.exceptLabelIdOrIds)) {
criteria.id = {
'!=': inputs.exceptLabelIdOrIds,
};
}
return sails.helpers.labels.getMany(criteria);
}, },
}; };
+1 -1
View File
@@ -3,7 +3,7 @@ const valuesValidator = (value) => {
return false; return false;
} }
if (!_.isFinite(value.position)) { if (!_.isUndefined(value.position) && !_.isFinite(value.position)) {
return false; return false;
} }
+1 -1
View File
@@ -99,7 +99,7 @@ module.exports = {
throw 'userMustBePresent'; throw 'userMustBePresent';
} }
if (!_.isNil(values.position)) { if (!_.isUndefined(values.position)) {
const boardId = values.boardId || inputs.record.boardId; const boardId = values.boardId || inputs.record.boardId;
const listId = values.listId || inputs.record.listId; const listId = values.listId || inputs.record.listId;
+28
View File
@@ -3,6 +3,10 @@ const valuesValidator = (value) => {
return false; return false;
} }
if (!_.isFinite(value.position)) {
return false;
}
if (!_.isPlainObject(value.board)) { if (!_.isPlainObject(value.board)) {
return false; return false;
} }
@@ -25,8 +29,32 @@ module.exports = {
async fn(inputs) { async fn(inputs) {
const { values } = inputs; const { values } = inputs;
const labels = await sails.helpers.boards.getLabels(values.board.id);
const { position, repositions } = sails.helpers.utils.insertToPositionables(
values.position,
labels,
);
repositions.forEach(async ({ id, position: nextPosition }) => {
await Label.update({
id,
boardId: values.board.id,
}).set({
position: nextPosition,
});
sails.sockets.broadcast(`board:${values.board.id}`, 'labelUpdate', {
item: {
id,
position: nextPosition,
},
});
});
const label = await Label.create({ const label = await Label.create({
...values, ...values,
position,
boardId: values.board.id, boardId: values.board.id,
}).fetch(); }).fetch();
+1 -1
View File
@@ -9,6 +9,6 @@ module.exports = {
}, },
async fn(inputs) { async fn(inputs) {
return Label.find(inputs.criteria).sort('id'); return Label.find(inputs.criteria).sort('position');
}, },
}; };
+40
View File
@@ -1,3 +1,15 @@
const valuesValidator = (value) => {
if (!_.isPlainObject(value)) {
return false;
}
if (!_.isUndefined(value.position) && !_.isFinite(value.position)) {
return false;
}
return true;
};
module.exports = { module.exports = {
inputs: { inputs: {
record: { record: {
@@ -6,6 +18,7 @@ module.exports = {
}, },
values: { values: {
type: 'json', type: 'json',
custom: valuesValidator,
required: true, required: true,
}, },
request: { request: {
@@ -16,6 +29,33 @@ module.exports = {
async fn(inputs) { async fn(inputs) {
const { values } = inputs; const { values } = inputs;
if (!_.isUndefined(values.position)) {
const labels = await sails.helpers.boards.getLabels(inputs.record.boardId, inputs.record.id);
const { position, repositions } = sails.helpers.utils.insertToPositionables(
values.position,
labels,
);
values.position = position;
repositions.forEach(async ({ id, position: nextPosition }) => {
await Label.update({
id,
boardId: inputs.record.boardId,
}).set({
position: nextPosition,
});
sails.sockets.broadcast(`board:${inputs.record.boardId}`, 'labelUpdate', {
item: {
id,
position: nextPosition,
},
});
});
}
const label = await Label.updateOne(inputs.record.id).set({ ...values }); const label = await Label.updateOne(inputs.record.id).set({ ...values });
if (label) { if (label) {
+2 -2
View File
@@ -6,11 +6,11 @@ const valuesValidator = (value) => {
return false; return false;
} }
if (value.background && !_.isPlainObject(value.background)) { if (!_.isNil(value.background) && !_.isPlainObject(value.background)) {
return false; return false;
} }
if (value.backgroundImage && !_.isPlainObject(value.backgroundImage)) { if (!_.isNil(value.backgroundImage) && !_.isPlainObject(value.backgroundImage)) {
return false; return false;
} }
+1 -1
View File
@@ -13,7 +13,7 @@ const valuesValidator = (value) => {
return false; return false;
} }
if (value.username && !_.isString(value.username)) { if (!_.isNil(value.username) && !_.isString(value.username)) {
return false; return false;
} }
+2 -2
View File
@@ -16,11 +16,11 @@ const valuesValidator = (value) => {
return false; return false;
} }
if (value.username && !_.isString(value.username)) { if (!_.isNil(value.username) && !_.isString(value.username)) {
return false; return false;
} }
if (value.avatar && !_.isPlainObject(value.avatar)) { if (!_.isNil(value.avatar) && !_.isPlainObject(value.avatar)) {
return false; return false;
} }
+4
View File
@@ -41,6 +41,10 @@ module.exports = {
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗ // ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝ // ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
position: {
type: 'number',
required: true,
},
name: { name: {
type: 'string', type: 'string',
isNotEmptyString: true, isNotEmptyString: true,
@@ -63,11 +63,9 @@ module.exports.down = async (knex) => {
.where('id', attachment.id); .where('id', attachment.id);
} }
await knex.schema.table('attachment', (table) => { return knex.schema.table('attachment', (table) => {
table.dropColumn('image'); table.dropColumn('image');
});
return knex.schema.alterTable('attachment', (table) => { table.dropNullable('is_image');
table.boolean('is_image').notNullable().alter();
}); });
}; };
@@ -1,44 +1,5 @@
const POSITION_GAP = 65535; const { addPosition, removePosition } = require('../../utils/migrations');
module.exports.up = async (knex) => { module.exports.up = (knex) => addPosition(knex, 'task', 'card_id');
await knex.schema.table('task', (table) => {
/* Columns */
table.specificType('position', 'double precision'); module.exports.down = (knex) => removePosition(knex, 'task');
/* 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');
});
@@ -3,8 +3,8 @@ module.exports.up = async (knex) => {
table.dropColumn('type'); table.dropColumn('type');
}); });
return knex.schema.alterTable('card', (table) => { return knex.schema.table('card', (table) => {
table.bigInteger('list_id').notNullable().alter(); table.dropNullable('list_id');
}); });
}; };
@@ -12,10 +12,10 @@ module.exports.down = async (knex) => {
await knex.schema.table('board', (table) => { await knex.schema.table('board', (table) => {
/* Columns */ /* Columns */
table.text('type').notNullable().defaultTo('kanban'); table.text('type').notNullable().defaultTo('kanban'); // FIXME: drop default
}); });
return knex.schema.alterTable('card', (table) => { return knex.schema.table('card', (table) => {
table.bigInteger('list_id').alter(); table.setNullable('list_id');
}); });
}; };
@@ -100,7 +100,6 @@ const processAttachmentImage = async (attachment, attachmentsPath) => {
module.exports.up = async (knex) => { module.exports.up = async (knex) => {
const config = await getConfig(); const config = await getConfig();
const users = await knex('user_account').whereNotNull('avatar'); const users = await knex('user_account').whereNotNull('avatar');
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
@@ -0,0 +1,5 @@
const { addPosition, removePosition } = require('../../utils/migrations');
module.exports.up = (knex) => addPosition(knex, 'label', 'board_id');
module.exports.down = (knex) => removePosition(knex, 'label');
+19 -116
View File
@@ -10,7 +10,7 @@
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"dotenv-cli": "^6.0.0", "dotenv-cli": "^6.0.0",
"filenamify": "^4.3.0", "filenamify": "^4.3.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^9.0.0",
"knex": "^2.3.0", "knex": "^2.3.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
@@ -3665,9 +3665,9 @@
"dev": true "dev": true
}, },
"node_modules/json5": { "node_modules/json5": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"minimist": "^1.2.0" "minimist": "^1.2.0"
@@ -3685,32 +3685,18 @@
} }
}, },
"node_modules/jsonwebtoken": { "node_modules/jsonwebtoken": {
"version": "8.5.1", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==",
"dependencies": { "dependencies": {
"jws": "^3.2.2", "jws": "^3.2.2",
"lodash.includes": "^4.3.0", "lodash": "^4.17.21",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1", "ms": "^2.1.1",
"semver": "^5.6.0" "semver": "^7.3.8"
}, },
"engines": { "engines": {
"node": ">=4", "node": ">=12",
"npm": ">=1.4.28" "npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"bin": {
"semver": "bin/semver"
} }
}, },
"node_modules/jwa": { "node_modules/jwa": {
@@ -3881,52 +3867,17 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"node_modules/lodash.issafeinteger": { "node_modules/lodash.issafeinteger": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.issafeinteger/-/lodash.issafeinteger-4.0.4.tgz", "resolved": "https://registry.npmjs.org/lodash.issafeinteger/-/lodash.issafeinteger-4.0.4.tgz",
"integrity": "sha512-VyybxpvKqtJKs4+RibsKP1qqbFsTZ6aKDsJfzqrobfMrzMRCHhXAMlKWGKD7QHy2OwGSuKuzSAv8pDyi62huWQ==" "integrity": "sha512-VyybxpvKqtJKs4+RibsKP1qqbFsTZ6aKDsJfzqrobfMrzMRCHhXAMlKWGKD7QHy2OwGSuKuzSAv8pDyi62huWQ=="
}, },
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true "dev": true
}, },
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/log-symbols": { "node_modules/log-symbols": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -10783,9 +10734,9 @@
"dev": true "dev": true
}, },
"json5": { "json5": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"dev": true, "dev": true,
"requires": { "requires": {
"minimist": "^1.2.0" "minimist": "^1.2.0"
@@ -10800,27 +10751,14 @@
} }
}, },
"jsonwebtoken": { "jsonwebtoken": {
"version": "8.5.1", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==",
"requires": { "requires": {
"jws": "^3.2.2", "jws": "^3.2.2",
"lodash.includes": "^4.3.0", "lodash": "^4.17.21",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1", "ms": "^2.1.1",
"semver": "^5.6.0" "semver": "^7.3.8"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
} }
}, },
"jwa": { "jwa": {
@@ -10945,52 +10883,17 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"lodash.issafeinteger": { "lodash.issafeinteger": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.issafeinteger/-/lodash.issafeinteger-4.0.4.tgz", "resolved": "https://registry.npmjs.org/lodash.issafeinteger/-/lodash.issafeinteger-4.0.4.tgz",
"integrity": "sha512-VyybxpvKqtJKs4+RibsKP1qqbFsTZ6aKDsJfzqrobfMrzMRCHhXAMlKWGKD7QHy2OwGSuKuzSAv8pDyi62huWQ==" "integrity": "sha512-VyybxpvKqtJKs4+RibsKP1qqbFsTZ6aKDsJfzqrobfMrzMRCHhXAMlKWGKD7QHy2OwGSuKuzSAv8pDyi62huWQ=="
}, },
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"lodash.merge": { "lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true "dev": true
}, },
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"log-symbols": { "log-symbols": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+1 -1
View File
@@ -31,7 +31,7 @@
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"dotenv-cli": "^6.0.0", "dotenv-cli": "^6.0.0",
"filenamify": "^4.3.0", "filenamify": "^4.3.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^9.0.0",
"knex": "^2.3.0", "knex": "^2.3.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
+49
View File
@@ -0,0 +1,49 @@
const POSITION_GAP = 65535;
const addPosition = async (knex, tableName, parentFieldName) => {
await knex.schema.table(tableName, (table) => {
/* Columns */
table.specificType('position', 'double precision');
/* Indexes */
table.index('position');
});
const records = await knex(tableName).orderBy([parentFieldName, 'id']);
let prevParentId;
let position;
// eslint-disable-next-line no-restricted-syntax
for (record of records) {
if (record[parentFieldName] === prevParentId) {
position += POSITION_GAP;
} else {
prevParentId = record[parentFieldName];
position = POSITION_GAP;
}
// eslint-disable-next-line no-await-in-loop
await knex(tableName)
.update({
position,
})
.where('id', record.id);
}
return knex.schema.table(tableName, (table) => {
table.dropNullable('position');
});
};
const removePosition = (knex, tableName) =>
knex.schema.table(tableName, (table) => {
table.dropColumn('position');
});
module.exports = {
addPosition,
removePosition,
};