Compare commits

..

14 Commits

Author SHA1 Message Date
Anthony
40a84d0c8a feat: Allow to add custom annotations for deployment in Helm (#1168) 2025-06-04 12:25:43 +02:00
Maksim Eltyshev
9690f7b73f fix: Prevent text overflow in activities 2025-06-03 23:15:07 +02:00
Maksim Eltyshev
04b97b66cb chore: Update dependencies 2025-06-03 13:02:16 +02:00
Maksim Eltyshev
46f4d5c1f8 fix: Canonicalize locale codes 2025-06-03 12:46:06 +02:00
Maksim Eltyshev
fc7863aaaf fix: Add mentions support when editing comments 2025-06-02 21:35:51 +02:00
Maksim Eltyshev
6c2999044b chore: Update logo assets 2025-06-02 20:13:22 +02:00
Maksim Eltyshev
a2495b664e chore: Add missing things 2025-06-02 19:44:08 +02:00
Jason Jack
e0374f30db build: Add tmpfs to compose file (#1165) 2025-06-02 19:39:10 +02:00
Maksim Eltyshev
665f9998dc fix: Prevent auto-updating translation files when key is missing 2025-06-01 21:44:14 +02:00
Maksim Eltyshev
fcf1fc4319 ref: Little refactoring 2025-06-01 20:53:35 +02:00
Maksim Eltyshev
4e05b88ecf chore: Add links to GitHub
Closes #1161
2025-05-31 00:10:00 +02:00
Roman Zavarnitsyn
c0b0436851 feat: Add ability to mention users in comments (#1162) 2025-05-30 22:01:29 +02:00
Maksim Eltyshev
eb2a3a2875 ref: Fix locale position 2025-05-30 13:44:58 +02:00
Niccolò Pedrini
74274e511f fix: Update Italian translation (#1163) 2025-05-30 13:42:57 +02:00
70 changed files with 1612 additions and 1206 deletions

View File

@@ -15,13 +15,13 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.0.4
version: 1.0.3
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "2.0.0-rc.4"
appVersion: "2.0.0-rc.3"
dependencies:
- alias: postgresql

View File

@@ -4,6 +4,10 @@ metadata:
name: {{ include "planka.fullname" . }}
labels:
{{- include "planka.labels" . | nindent 4 }}
{{- with .Values.deploymentAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}

View File

@@ -45,6 +45,9 @@ podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
# Annotations to add to the deployment
deploymentAnnotations: {}
securityContext: {}
# capabilities:
# drop:

1452
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -79,13 +79,13 @@
},
"dependencies": {
"@ballerina/highlightjs-ballerina": "^1.0.1",
"@diplodoc/cut-extension": "^0.7.3",
"@diplodoc/transform": "^4.57.2",
"@gravity-ui/markdown-editor": "^15.11.0",
"@gravity-ui/uikit": "^7.11.0",
"@diplodoc/cut-extension": "^0.7.4",
"@diplodoc/transform": "^4.57.7",
"@gravity-ui/components": "^4.4.0",
"@gravity-ui/markdown-editor": "^15.13.2",
"@gravity-ui/uikit": "^7.13.0",
"@juggle/resize-observer": "^3.4.0",
"@types/papaparse": "^5.3.16",
"@vitejs/plugin-react": "^4.4.1",
"@vitejs/plugin-react": "^4.5.1",
"browserslist-to-esbuild": "^2.1.1",
"classnames": "^2.5.1",
"date-fns": "^2.30.0",
@@ -114,7 +114,7 @@
"highlightjs-zenscript": "^2.0.0",
"hightlightjs-papyrus": "^0.0.4",
"history": "^5.3.0",
"i18next": "23.15.2",
"i18next": "^25.2.1",
"i18next-browser-languagedetector": "^8.1.0",
"initials": "^3.1.2",
"javascript-time-ago": "^2.5.11",
@@ -137,12 +137,13 @@
"react-dropzone": "^14.3.8",
"react-frame-component": "^5.2.7",
"react-hot-toast": "^2.5.2",
"react-i18next": "^15.5.1",
"react-i18next": "^15.5.2",
"react-input-mask": "^2.0.4",
"react-intersection-observer": "^9.16.0",
"react-mentions": "^4.4.10",
"react-photoswipe-gallery": "^2.2.7",
"react-redux": "^8.1.3",
"react-router-dom": "^6.30.0",
"react-router-dom": "^6.30.1",
"react-textarea-autosize": "^8.5.9",
"react-time-ago": "^7.3.3",
"redux": "^4.2.1",
@@ -151,10 +152,10 @@
"redux-saga": "^1.3.0",
"reselect": "^4.1.8",
"sails.io.js": "^1.2.1",
"sass-embedded": "^1.87.0",
"sass-embedded": "^1.89.1",
"semantic-ui-react": "^2.1.5",
"socket.io-client": "^2.5.0",
"validator": "^13.15.0",
"validator": "^13.15.15",
"vite": "^6.3.5",
"vite-plugin-commonjs": "^0.10.4",
"vite-plugin-node-polyfills": "^0.23.0",
@@ -164,7 +165,7 @@
"devDependencies": {
"@babel/eslint-parser": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@cucumber/cucumber": "^11.2.0",
"@cucumber/cucumber": "^11.3.0",
"@cucumber/pretty-formatter": "^1.0.1",
"@playwright/test": "^1.52.0",
"babel-jest": "^29.7.0",
@@ -174,7 +175,7 @@
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-prettier": "^5.4.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2",
"jest": "^29.7.0",

View File

@@ -1,13 +0,0 @@
diff --git a/node_modules/react-photoswipe-gallery/dist/gallery.js b/node_modules/react-photoswipe-gallery/dist/gallery.js
index 53cc02c..f4baccb 100644
--- a/node_modules/react-photoswipe-gallery/dist/gallery.js
+++ b/node_modules/react-photoswipe-gallery/dist/gallery.js
@@ -181,7 +181,7 @@ export const Gallery = ({
alt
} = pswpInstance.currSlide.data;
// eslint-disable-next-line no-param-reassign
- el.innerHTML = caption || alt || '';
+ el.textContent = caption || alt || '';
});
}
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -16,6 +16,7 @@
padding-bottom: 14px;
vertical-align: top;
width: calc(100% - 40px);
word-wrap: break-word;
}
.date {

View File

@@ -16,6 +16,7 @@
padding-bottom: 14px;
vertical-align: top;
width: calc(100% - 40px);
word-wrap: break-word;
}
.date {

View File

@@ -3,20 +3,19 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Pagination, Table } from 'semantic-ui-react';
import Papa from 'papaparse';
import Frame from 'react-frame-component';
import { Loader, Pagination, Table } from 'semantic-ui-react';
import styles from './CsvViewer.module.scss';
const ROWS_PER_PAGE = 50;
/* eslint-disable react/no-array-index-key */
const CsvViewer = React.memo(({ src, className }) => {
const [csvData, setCsvData] = useState(null);
const [rows, setRows] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const frameStyles = useMemo(
@@ -34,7 +33,7 @@ const CsvViewer = React.memo(({ src, className }) => {
[],
);
const handlePageChange = useCallback((e, { activePage }) => {
const handlePageChange = useCallback((_, { activePage }) => {
setCurrentPage(activePage);
}, []);
@@ -49,47 +48,46 @@ const CsvViewer = React.memo(({ src, className }) => {
Papa.parse(text, {
skipEmptyLines: true,
complete: (results) => {
const rows = results.data;
setCsvData({
rows,
totalRows: rows.length,
});
complete: ({ data }) => {
setRows(data);
},
});
} catch (err) {
setCsvData(null);
} catch {
/* empty */
}
}
fetchFile();
}, [src]);
if (!csvData) {
return null;
if (rows === null) {
return <Loader active size="big" />;
}
const startIdx = (currentPage - 1) * ROWS_PER_PAGE;
const endIdx = startIdx + ROWS_PER_PAGE;
const currentRows = csvData.rows.slice(startIdx, endIdx);
const totalPages = Math.ceil(csvData.totalRows / ROWS_PER_PAGE);
const startIndex = (currentPage - 1) * ROWS_PER_PAGE;
const endIndex = startIndex + ROWS_PER_PAGE;
const currentRows = rows.slice(startIndex, endIndex);
const totalPages = Math.ceil(rows.length / ROWS_PER_PAGE);
const content = (
<div>
return (
<Frame
head={<style>{frameStyles.join('')}</style>}
className={classNames(styles.wrapper, className)}
>
<div>
<Table celled compact>
<Table.Header>
<Table.Row>
{csvData.rows[0].map((header, index) => (
<Table.HeaderCell key={index}>{header}</Table.HeaderCell>
{rows[0].map((cell) => (
<Table.HeaderCell key={cell}>{cell}</Table.HeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{currentRows.slice(1).map((row, rowIndex) => (
<Table.Row key={rowIndex}>
{row.map((cell, cellIndex) => (
<Table.Cell key={cellIndex}>{cell}</Table.Cell>
{currentRows.slice(1).map((row) => (
<Table.Row key={row}>
{row.map((cell) => (
<Table.Cell key={cell}>{cell}</Table.Cell>
))}
</Table.Row>
))}
@@ -98,30 +96,18 @@ const CsvViewer = React.memo(({ src, className }) => {
</div>
{totalPages > 1 && (
<Pagination
activePage={currentPage}
secondary
pointing
totalPages={totalPages}
onPageChange={handlePageChange}
activePage={currentPage}
firstItem={null}
lastItem={null}
pointing
secondary
boundaryRange={1}
siblingRange={1}
onPageChange={handlePageChange}
/>
)}
</div>
);
return (
<Frame
head={<style>{frameStyles.join('')}</style>}
className={classNames(styles.wrapper, className)}
>
{content}
</Frame>
);
});
/* eslint-enable react/no-array-index-key */
CsvViewer.propTypes = {
src: PropTypes.string.isRequired,

View File

@@ -141,7 +141,7 @@ const ActionsStep = React.memo(({ onClose }) => {
</Menu.Item>
{withTrashEmptier && (
<>
{(withSubscribe || withCustomFieldGroups) && <hr className={styles.divider} />}
<hr className={styles.divider} />
<Menu.Item className={styles.menuItem} onClick={handleEmptyTrashClick}>
<Icon name="trash alternate outline" className={styles.menuItemIcon} />
{t('action.emptyTrash', {
@@ -151,7 +151,7 @@ const ActionsStep = React.memo(({ onClose }) => {
</>
)}
<>
{(withSubscribe || withTrashEmptier) && <hr className={styles.divider} />}
<hr className={styles.divider} />
{[BoardContexts.BOARD, BoardContexts.ARCHIVE, BoardContexts.TRASH].map((context) => (
<Menu.Item
key={context}

View File

@@ -3,16 +3,18 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import React, { useCallback, useState, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { Mention, MentionsInput } from 'react-mentions';
import { Button, Form } from 'semantic-ui-react';
import { useClickAwayListener, useDidUpdate, useToggle } from '../../../lib/hooks';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { useEscapeInterceptor, useForm, useNestedRef } from '../../../hooks';
import { isModifierKeyPressed } from '../../../utils/event-helpers';
import UserAvatar from '../../users/UserAvatar';
import styles from './Add.module.scss';
@@ -21,13 +23,16 @@ const DEFAULT_DATA = {
};
const Add = React.memo(() => {
const boardMemberships = useSelector(selectors.selectMembershipsForCurrentBoard);
const dispatch = useDispatch();
const [t] = useTranslation();
const [data, , setData] = useForm(DEFAULT_DATA);
const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [selectTextFieldState, selectTextField] = useToggle();
const [textFieldRef, handleTextFieldRef] = useNestedRef();
const mentionsInputRef = useRef(null);
const textFieldRef = useRef(null);
const [buttonRef, handleButtonRef] = useNestedRef();
const submit = useCallback(() => {
@@ -47,6 +52,11 @@ const Add = React.memo(() => {
}, [dispatch, data, setData, selectTextField, textFieldRef]);
const handleEscape = useCallback(() => {
if (mentionsInputRef.current.isOpened()) {
mentionsInputRef.current.clearSuggestions();
return;
}
setIsOpened(false);
textFieldRef.current.blur();
}, [textFieldRef]);
@@ -62,6 +72,15 @@ const Add = React.memo(() => {
setIsOpened(true);
}, []);
const handleFieldChange = useCallback(
(_, text) => {
setData({
text,
});
},
[setData],
);
const handleFieldKeyDown = useCallback(
(event) => {
if (isModifierKeyPressed(event) && event.key === 'Enter') {
@@ -85,6 +104,16 @@ const Add = React.memo(() => {
handleClickAwayCancel,
);
const suggestionRenderer = useCallback(
(entry, _, highlightedDisplay) => (
<div className={styles.suggestion}>
<UserAvatar id={entry.id} size="tiny" />
{highlightedDisplay}
</div>
),
[],
);
useDidUpdate(() => {
if (isOpened) {
activateEscapeInterceptor();
@@ -99,21 +128,39 @@ const Add = React.memo(() => {
return (
<Form onSubmit={handleSubmit}>
<TextArea
{...clickAwayProps} // eslint-disable-line react/jsx-props-no-spreading
ref={handleTextFieldRef}
as={TextareaAutosize}
name="text"
value={data.text}
placeholder={t('common.writeComment')}
maxLength={1048576}
minRows={isOpened ? 3 : 1}
spellCheck={false}
className={styles.field}
onFocus={handleFieldFocus}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
/>
<div className={styles.field}>
<MentionsInput
{...clickAwayProps} // eslint-disable-line react/jsx-props-no-spreading
allowSpaceInQuery
allowSuggestionsAboveCursor
ref={mentionsInputRef}
inputRef={textFieldRef}
value={data.text}
placeholder={t('common.writeComment')}
maxLength={1048576}
rows={isOpened ? 3 : 1}
className="mentions-input"
style={{
control: {
minHeight: isOpened ? '79px' : '37px',
},
}}
onFocus={handleFieldFocus}
onChange={handleFieldChange}
onKeyDown={handleFieldKeyDown}
>
<Mention
appendSpaceOnAdd
data={boardMemberships.map(({ user }) => ({
id: user.id,
display: user.username || user.name,
}))}
displayTransform={(_, display) => `@${display}`}
renderSuggestion={suggestionRenderer}
className={styles.mention}
/>
</MentionsInput>
</div>
{isOpened && (
<div className={styles.controls}>
<Button

View File

@@ -17,19 +17,34 @@
.field {
background: #fff;
border: 0;
box-sizing: border-box;
color: #333;
display: block;
line-height: 1.5;
font-size: 14px;
overflow: hidden;
padding: 8px 12px;
resize: none;
width: 100%;
margin-bottom: 8px !important;
&:focus {
outline: none;
textarea {
border: 1px solid rgba(9, 30, 66, 0.13);
border-radius: 3px;
box-sizing: border-box;
color: #333;
display: block;
line-height: 1.4;
font-size: 14px;
overflow: hidden;
padding: 8px 12px;
resize: none;
&:focus {
outline: none;
}
}
}
.mention {
background-color: #f1f8ff;
border-radius: 3px;
}
.suggestion {
align-items: center;
display: flex;
gap: 8px;
}
}

View File

@@ -4,12 +4,12 @@
*/
import { dequal } from 'dequal';
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Form, TextArea } from 'semantic-ui-react';
import { Mention, MentionsInput } from 'react-mentions';
import { Button, Form } from 'semantic-ui-react';
import { useClickAwayListener } from '../../../lib/hooks';
import selectors from '../../../selectors';
@@ -17,6 +17,7 @@ import entryActions from '../../../entry-actions';
import { useForm, useNestedRef } from '../../../hooks';
import { focusEnd } from '../../../utils/element-helpers';
import { isModifierKeyPressed } from '../../../utils/event-helpers';
import UserAvatar from '../../users/UserAvatar';
import styles from './Edit.module.scss';
@@ -24,6 +25,7 @@ const Edit = React.memo(({ commentId, onClose }) => {
const selectCommentById = useMemo(() => selectors.makeSelectCommentById(), []);
const comment = useSelector((state) => selectCommentById(state, commentId));
const boardMemberships = useSelector(selectors.selectMembershipsForCurrentBoard);
const dispatch = useDispatch();
const [t] = useTranslation();
@@ -35,12 +37,13 @@ const Edit = React.memo(({ commentId, onClose }) => {
[comment.text],
);
const [data, handleFieldChange] = useForm(() => ({
const [data, , setData] = useForm(() => ({
text: '',
...defaultData,
}));
const [textFieldRef, handleTextFieldRef] = useNestedRef();
const mentionsInputRef = useRef(null);
const textFieldRef = useRef(null);
const [buttonRef, handleButtonRef] = useNestedRef();
const submit = useCallback(() => {
@@ -60,6 +63,15 @@ const Edit = React.memo(({ commentId, onClose }) => {
submit();
}, [submit]);
const handleFieldChange = useCallback(
(_, text) => {
setData({
text,
});
},
[setData],
);
const handleFieldKeyDown = useCallback(
(event) => {
if (event.key === 'Enter') {
@@ -83,25 +95,53 @@ const Edit = React.memo(({ commentId, onClose }) => {
handleClickAwayCancel,
);
const suggestionRenderer = useCallback(
(entry, _, highlightedDisplay) => (
<div className={styles.suggestion}>
<UserAvatar id={entry.id} size="tiny" />
{highlightedDisplay}
</div>
),
[],
);
useEffect(() => {
focusEnd(textFieldRef.current);
}, [textFieldRef]);
return (
<Form onSubmit={handleSubmit}>
<TextArea
{...clickAwayProps} // eslint-disable-line react/jsx-props-no-spreading
ref={handleTextFieldRef}
as={TextareaAutosize}
name="text"
value={data.text}
maxLength={1048576}
minRows={3}
spellCheck={false}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={handleFieldChange}
/>
<div className={styles.field}>
<MentionsInput
{...clickAwayProps} // eslint-disable-line react/jsx-props-no-spreading
allowSpaceInQuery
allowSuggestionsAboveCursor
ref={mentionsInputRef}
inputRef={textFieldRef}
value={data.text}
maxLength={1048576}
rows={3}
className="mentions-input"
style={{
control: {
minHeight: '79px',
},
}}
onChange={handleFieldChange}
onKeyDown={handleFieldKeyDown}
>
<Mention
appendSpaceOnAdd
data={boardMemberships.map(({ user }) => ({
id: user.id,
display: user.username || user.name,
}))}
displayTransform={(_, display) => `@${display}`}
renderSuggestion={suggestionRenderer}
className={styles.mention}
/>
</MentionsInput>
</div>
<div className={styles.controls}>
<Button
{...clickAwayProps} // eslint-disable-line react/jsx-props-no-spreading

View File

@@ -11,21 +11,33 @@
.field {
background: #fff;
border: 1px solid rgba(9, 30, 66, 0.13);
border-radius: 3px;
box-sizing: border-box;
color: #333;
display: block;
line-height: 1.4;
font-size: 14px;
margin-bottom: 4px;
overflow: hidden;
padding: 8px 12px;
resize: none;
width: 100%;
&:focus {
outline: none;
textarea {
border: 1px solid rgba(9, 30, 66, 0.13);
border-radius: 3px;
box-sizing: border-box;
color: #333;
display: block;
line-height: 1.4;
font-size: 14px;
overflow: hidden;
padding: 8px 12px;
resize: none;
&:focus {
outline: none;
}
}
}
.mention {
background-color: #f1f8ff;
border-radius: 3px;
}
.suggestion {
align-items: center;
display: flex;
gap: 8px;
}
}

View File

@@ -7,7 +7,7 @@ import isEmail from 'validator/lib/isEmail';
import React, { useCallback, useEffect, useMemo } from 'react';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { useTranslation, Trans } from 'react-i18next';
import { Button, Divider, Form, Grid, Header, Message } from 'semantic-ui-react';
import { useDidUpdate, usePrevious, useToggle } from '../../../lib/hooks';
import { Input } from '../../../lib/custom-ui';
@@ -247,7 +247,14 @@ const Content = React.memo(() => {
/>
)}
</div>
<p className={styles.formFooter}>{t('common.poweredByPlanka')}</p>
<p className={styles.formFooter}>
<Trans i18nKey="common.poweredByPlanka">
{'Powered by '}
<a href="https://github.com/plankanban/planka" target="_blank" rel="noreferrer">
PLANKA
</a>
</Trans>
</p>
</div>
</Grid.Column>
<Grid.Column

View File

@@ -13,6 +13,7 @@ import { Button } from 'semantic-ui-react';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { formatTextWithMentions } from '../../../utils/formatters';
import Paths from '../../../constants/Paths';
import { StaticUserIds } from '../../../constants/StaticUsers';
import { NotificationTypes } from '../../../constants/Enums';
@@ -83,7 +84,7 @@ const Item = React.memo(({ id, onClose }) => {
break;
}
case NotificationTypes.COMMENT_CARD: {
const commentText = truncate(notification.data.text);
const commentText = truncate(formatTextWithMentions(notification.data.text));
contentNode = (
<Trans
@@ -122,6 +123,28 @@ const Item = React.memo(({ id, onClose }) => {
);
break;
case NotificationTypes.MENTION_IN_COMMENT: {
const commentText = truncate(formatTextWithMentions(notification.data.text));
contentNode = (
<Trans
i18nKey="common.userMentionedYouInCommentOnCard"
values={{
user: creatorUserName,
comment: commentText,
card: cardName,
}}
>
<span className={styles.author}>{creatorUserName}</span>
{` mentioned you in «${commentText}» on `}
<Link to={Paths.CARDS.replace(':id', notification.cardId)} onClick={onClose}>
{cardName}
</Link>
</Trans>
);
break;
}
default:
contentNode = null;
}

View File

@@ -14,7 +14,9 @@ import styles from './AboutPane.module.scss';
const AboutPane = React.memo(() => (
<Tab.Pane attached={false} className={styles.wrapper}>
<Image centered src={logo} size="large" />
<a href="https://github.com/plankanban/planka" target="_blank" rel="noreferrer">
<Image centered src={logo} size="large" />
</a>
<div className={styles.version}>{version}</div>
</Tab.Pane>
));

View File

@@ -23,6 +23,7 @@ import { emojiDefs } from '@gravity-ui/markdown-editor/_/bundle/emoji';
/* eslint-enable import/no-unresolved */
import link from './link';
import mention from './mention';
export default [
ins,
@@ -41,4 +42,5 @@ export default [
meta,
deflist,
link,
mention,
];

View File

@@ -0,0 +1,61 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const MENTION_REGEX = /@\[(.*?)\]\((.*?)\)/g;
export default (md) => {
md.core.ruler.push('mention', ({ tokens }) => {
tokens.forEach((token) => {
if (token.type === 'inline' && token.content) {
const matches = [...token.content.matchAll(MENTION_REGEX)];
if (matches.length > 0) {
const newChildren = [];
let lastIndex = 0;
matches.forEach((match) => {
// Add text before the mention
if (match.index > lastIndex) {
newChildren.push({
type: 'text',
content: token.content.slice(lastIndex, match.index),
level: token.level,
});
}
// Add mention token
newChildren.push({
type: 'mention',
meta: {
display: match[1],
userId: match[2],
},
level: token.level,
});
lastIndex = match.index + match[0].length;
});
// Add remaining text after last mention
if (lastIndex < token.content.length) {
newChildren.push({
type: 'text',
content: token.content.slice(lastIndex),
level: token.level,
});
}
token.children = newChildren; // eslint-disable-line no-param-reassign
}
}
});
});
// eslint-disable-next-line no-param-reassign
md.renderer.rules.mention = (tokens, index) => {
const { display, userId } = tokens[index].meta;
return `<span class="mention" data-user-id="${userId}">@${display}</span>`;
};
};

View File

@@ -100,6 +100,7 @@ export const NotificationTypes = {
MOVE_CARD: 'moveCard',
COMMENT_CARD: 'commentCard',
ADD_MEMBER_TO_CARD: 'addMemberToCard',
MENTION_IN_COMMENT: 'mentionInComment',
};
export const NotificationServiceFormats = {

View File

@@ -12,7 +12,7 @@ export default {
or: 'Nebo',
pageNotFound_title: 'Stránka nenalezena',
password: 'Heslo',
poweredByPlanka: 'Poháněno technologií PLANKA',
poweredByPlanka: 'Poháněno technologií <1>PLANKA</1>',
serverConnectionFailed: 'Připojení k serveru selhalo',
unknownError: 'Neznámá chyba, zkuste to později',
usernameAlreadyInUse: 'Uživatelské jméno se již používá',

View File

@@ -1,5 +1,6 @@
import dateFns from 'date-fns/locale/de';
import timeAgo from 'javascript-time-ago/locale/de';
import markdownEditor from './markdown-editor.json';
export default {

View File

@@ -12,7 +12,7 @@ export default {
or: 'Oder',
pageNotFound_title: 'Seite nicht gefunden',
password: 'Passwort',
poweredByPlanka: 'Powered by PLANKA',
poweredByPlanka: 'Powered by <1>PLANKA</1>',
serverConnectionFailed: 'Serververbindung fehlgeschlagen',
unknownError: 'Unbekannter Fehler, bitte später erneut versuchen',
usernameAlreadyInUse: 'Benutzername wird bereits verwendet',

View File

@@ -63,7 +63,7 @@
"numbered-list_title": "Nummerierte Liste",
"numbered-list_hint": "1. Dein Text",
"documentation": "Dokumentation",
"documentation_link": " https://diplodoc.com/docs/en/syntax/"
"documentation_link": "https://diplodoc.com/docs/en/syntax/"
},
"menubar": {
"bold": "Fett",

View File

@@ -303,6 +303,8 @@ export default {
userMarkedTaskIncompleteOnCard:
'<0>{{user}}</0> marked {{task}} incomplete on <4>{{card}}</4>',
userMarkedTaskIncompleteOnThisCard: '<0>{{user}}</0> marked {{task}} incomplete on this card',
userMentionedYouInCommentOnCard:
'<0>{{user}}</0> mentioned you in a comment «{{comment}}» on <2>{{card}}</2>',
userMovedCardFromListToList:
'<0>{{user}}</0> moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:

View File

@@ -12,7 +12,7 @@ export default {
or: 'Or',
pageNotFound_title: 'Page Not Found',
password: 'Password',
poweredByPlanka: 'Powered by PLANKA',
poweredByPlanka: 'Powered by <1>PLANKA</1>',
serverConnectionFailed: 'Server connection failed',
unknownError: 'Unknown error, try again later',
usernameAlreadyInUse: 'Username already in use',

View File

@@ -63,7 +63,7 @@
"numbered-list_title": "Numbered list",
"numbered-list_hint": "1. Your text",
"documentation": "Documentation",
"documentation_link": " https://diplodoc.com/docs/en/syntax/"
"documentation_link": "https://diplodoc.com/docs/en/syntax/"
},
"menubar": {
"bold": "Bold",

View File

@@ -298,6 +298,8 @@ export default {
userMarkedTaskIncompleteOnCard:
'<0>{{user}}</0> marked {{task}} incomplete on <4>{{card}}</4>',
userMarkedTaskIncompleteOnThisCard: '<0>{{user}}</0> marked {{task}} incomplete on this card',
userMentionedYouInCommentOnCard:
'<0>{{user}}</0> mentioned you in a comment «{{comment}}» on <2>{{card}}</2>',
userMovedCardFromListToList:
'<0>{{user}}</0> moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:

View File

@@ -12,7 +12,7 @@ export default {
or: 'Or',
pageNotFound_title: 'Page Not Found',
password: 'Password',
poweredByPlanka: 'Powered by PLANKA',
poweredByPlanka: 'Powered by <1>PLANKA</1>',
serverConnectionFailed: 'Server connection failed',
unknownError: 'Unknown error, try again later',
usernameAlreadyInUse: 'Username already in use',

View File

@@ -63,7 +63,7 @@
"numbered-list_title": "Numbered list",
"numbered-list_hint": "1. Your text",
"documentation": "Documentation",
"documentation_link": " https://diplodoc.com/docs/en/syntax/"
"documentation_link": "https://diplodoc.com/docs/en/syntax/"
},
"menubar": {
"bold": "Bold",

View File

@@ -24,8 +24,8 @@ import ptBR from './pt-BR';
import roRO from './ro-RO';
import ruRU from './ru-RU';
import skSK from './sk-SK';
import srCyrlCS from './sr-Cyrl-CS';
import srLatnCS from './sr-Latn-CS';
import srCyrlRS from './sr-Cyrl-RS';
import srLatnRS from './sr-Latn-RS';
import svSE from './sv-SE';
import trTR from './tr-TR';
import ukUA from './uk-UA';
@@ -55,8 +55,8 @@ const locales = [
roRO,
ruRU,
skSK,
srCyrlCS,
srLatnCS,
srCyrlRS,
srLatnRS,
svSE,
trTR,
ukUA,

View File

@@ -284,13 +284,29 @@ export default {
unsavedChanges: 'Modifiche non salvate',
uploadedImages: 'Immagini caricate',
userActions_title: 'Azioni utente',
userAddedThisCardToList: '<0>{{user}}</0> ha aggiunto questa scheda a {{list}}',
userAddedCardToList: '<0>{{user}}</0> ha aggiunto <2>{{card}}</2> a {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> ha aggiunto questa task a {{list}}',
userAddedUserToCard: '<0>{{actorUser}}</0> ha aggiunto {{addedUser}} a <4>{{card}}</4>',
userAddedUserToThisCard: '<0>{{actorUser}}</0> ha aggiunto {{addedUser}} a questa task',
userAddedYouToCard: '<0>{{user}}</0> ti ha aggiunto a <2>{{card}}</2>',
userCompletedTaskOnCard: '<0>{{user}}</0> ha completato {{task}} in <4>{{card}}</4>',
userCompletedTaskOnThisCard: '<0>{{user}}</0> ha completato {{task}} in questa task',
userJoinedCard: `<0>{{user}}</0> è entrato in <2>{{card}}</2>`,
userJoinedThisCard: `<0>{{user}}</0> è entrato in questa task`,
userLeftNewCommentToCard:
'<0>{{user}}</0> ha lasciato un commento «{{comment}}» a <2>{{card}}</2>',
userLeftCard: '<0>{{user}}</0> ha lasciato <2>{{card}}</2>',
userLeftThisCard: '<0>{{user}}</0> ha lasciato questa task',
userMarkedTaskIncompleteOnCard:
'<0>{{user}}</0> ha contrassegnato {{task}} come incompleta in <4>{{card}}</4>',
userMarkedTaskIncompleteOnThisCard:
'<0>{{user}}</0> ha contrassegnato {{task}} come incompleta in questa task',
userMovedCardFromListToList:
'<0>{{user}}</0> ha spostato <2>{{card}}</2> da {{fromList}} a {{toList}}',
userMovedThisCardFromListToList:
'<0>{{user}}</0> ha spostato questa scheda da {{fromList}} a {{toList}}',
userRemovedUserFromCard: '<0>{{actorUser}}</0> ha rimosso {{removedUser}} da <4>{{card}}</4>',
userRemovedUserFromThisCard: '<0>{{actorUser}}</0> ha rimosso {{removedUser}} da questa task',
username: 'Username',
users: 'Utenti',
viewer: 'Visualizzatore',
@@ -354,6 +370,7 @@ export default {
deleteProject_title: 'Elimina progetto',
deleteTask: 'Elimina task',
deleteTask_title: 'Elimina task',
deleteTaskList: 'Elimina lista di list',
deleteUser: 'Elimina utente',
deleteUser_title: 'Elimina utente',
dismissAll: 'Ignora tutto',

View File

@@ -12,7 +12,7 @@ export default {
or: 'Oppure',
pageNotFound_title: 'Pagina non trovata',
password: 'Password',
poweredByPlanka: 'Powered by Planka',
poweredByPlanka: 'Powered by <1>PLANKA</1>',
serverConnectionFailed: 'Connesione al server fallita',
unknownError: 'Errore sconosciuto, prova ancora',
usernameAlreadyInUse: 'Username già in uso',

View File

@@ -12,7 +12,7 @@ export default {
or: 'Lub',
pageNotFound_title: 'Strona nie znaleziona',
password: 'Hasło',
poweredByPlanka: 'Powered by PLANKA',
poweredByPlanka: 'Powered by <1>PLANKA</1>',
serverConnectionFailed: 'Błąd połączenia z serwerem',
unknownError: 'Nieznany błąd, spróbuj ponownie później',
usernameAlreadyInUse: 'Nazwa użytkownika nie jest dostępna',

View File

@@ -63,7 +63,7 @@
"numbered-list_title": "Lista numerowana",
"numbered-list_hint": "1. Twój tekst",
"documentation": "Dokumentacja",
"documentation_link": " https://diplodoc.com/docs/en/syntax/"
"documentation_link": "https://diplodoc.com/docs/en/syntax/"
},
"menubar": {
"bold": "Pogrubienie",

View File

@@ -1,5 +1,6 @@
import dateFns from 'date-fns/locale/ru';
import timeAgo from 'javascript-time-ago/locale/ru';
import markdownEditor from './markdown-editor.json';
export default {

View File

@@ -12,7 +12,7 @@ export default {
or: 'Или',
pageNotFound_title: 'Страница не найдена',
password: 'Пароль',
poweredByPlanka: 'Powered by PLANKA',
poweredByPlanka: 'Powered by <1>PLANKA</1>',
serverConnectionFailed: 'Не могу подключиться к серверу',
unknownError: 'Что-то пошло не так, попробуйте позже',
usernameAlreadyInUse: 'Имя пользователя уже занято',

View File

@@ -7,7 +7,7 @@
"bundle": {
"error-title": "Ошибка в редакторе markdown",
"settings_wysiwyg": "Визуальный редактор (wysiwyg)",
"settings_markup": "Разметка Markdown",
"settings_markup": "Разметка markdown",
"markup_placeholder": "Введите разметку markdown..."
},
"codeblock": {
@@ -63,7 +63,7 @@
"numbered-list_title": "Нумерованный список",
"numbered-list_hint": "1. Ваш текст",
"documentation": "Документация",
"documentation_link": " https://diplodoc.com/docs/en/syntax/"
"documentation_link": "https://diplodoc.com/docs/en/syntax/"
},
"menubar": {
"bold": "Жирный",

View File

@@ -1,7 +1,7 @@
import login from './login';
export default {
language: 'sr-Cyrl-CS',
language: 'sr-Cyrl-RS',
country: 'rs',
name: 'Српски (ћирилица)',
embeddedLocale: login,

View File

@@ -1,7 +1,7 @@
import login from './login';
export default {
language: 'sr-Latn-CS',
language: 'sr-Latn-RS',
country: 'rs',
name: 'Srpski (latinica)',
embeddedLocale: login,

View File

@@ -41,6 +41,10 @@ export const isCurrentModalAvailableForCurrentUser = createSelector(
return boardModel.project.hasManagerWithUserId(currentUserId);
}
case ModalTypes.BOARD_ACTIVITIES: {
const boardModel = Board.withId(currentModal.params.id);
return !!boardModel && boardModel.isAvailableForUser(currentUserModel);
}
default:
return true;
}

View File

@@ -12,6 +12,31 @@
height: auto;
}
.mentions-input {
&__highlighter {
line-height: 1.4;
padding: 8px 12px;
}
&__suggestions {
border: 1px solid #d4d4d5;
border-radius: 3px;
box-shadow: 0 8px 16px -4px rgba(9, 45, 66, 0.25),
0 0 0 1px rgba(9, 45, 66, 0.08);
max-height: 200px;
overflow-y: auto;
&__item {
padding: 8px 12px;
&--focused {
background-color: rgba(0, 0, 0, 0.05);
color: rgba(0, 0, 0, 0.95);
}
}
}
}
.react-datepicker {
border: 0;
color: #444444;
@@ -192,6 +217,13 @@
font-size: .85em !important;
}
.mention {
color: #0366d6;
background-color: #f1f8ff;
border-radius: 3px;
padding: 0 2px;
}
.yfm-clipboard:hover {
.yfm-clipboard-button {
min-height: auto;

View File

@@ -0,0 +1,4 @@
const MENTIONS_REGEX = /@\[(.*?)\]\(.*?\)/g;
// eslint-disable-next-line import/prefer-default-export
export const formatTextWithMentions = (text) => text.replace(MENTIONS_REGEX, '@$1');

View File

@@ -1 +1 @@
export default '2.0.0-rc.4';
export default '2.0.0-rc.3';

View File

@@ -1,12 +1,16 @@
services:
planka:
image: ghcr.io/plankanban/planka:2.0.0-rc.4
image: ghcr.io/plankanban/planka:2.0.0-rc.3
restart: on-failure
volumes:
- favicons:/app/public/favicons
- user-avatars:/app/public/user-avatars
- background-images:/app/public/background-images
- attachments:/app/private/attachments
# Optionally override this to your user/group
# user: 1000:1000
# tmpfs:
# - /app/.tmp:mode=770,uid=1000,gid=1000
ports:
- 3000:1337
environment:

30
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "planka",
"version": "2.0.0-rc.4",
"version": "2.0.0-rc.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "planka",
"version": "2.0.0-rc.4",
"version": "2.0.0-rc.3",
"hasInstallScript": true,
"dependencies": {
"concurrently": "^8.2.2",
@@ -16,9 +16,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.4.tgz",
"integrity": "sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==",
"engines": {
"node": ">=6.9.0"
}
@@ -310,9 +310,9 @@
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dependencies": {
"ms": "^2.1.3"
},
@@ -909,9 +909,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
"integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"engines": {
"node": ">= 0.4"
},
@@ -1098,14 +1098,14 @@
}
},
"node_modules/yaml": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
"integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
"node": ">= 14.6"
}
},
"node_modules/yargs": {

View File

@@ -1,6 +1,6 @@
{
"name": "planka",
"version": "2.0.0-rc.4",
"version": "2.0.0-rc.3",
"private": true,
"scripts": {
"client:build": "npm run build --prefix client",

View File

@@ -6,10 +6,18 @@
const escapeMarkdown = require('escape-markdown');
const escapeHtml = require('escape-html');
const { formatTextWithMentions } = require('../../../utils/formatters');
const extractMentionedUserIds = (text) => {
const mentionRegex = /@\[.*?\]\((.*?)\)/g;
const matches = [...text.matchAll(mentionRegex)];
return matches.map((match) => match[1]);
};
const buildAndSendNotifications = async (services, board, card, comment, actorUser, t) => {
const markdownCardLink = `[${escapeMarkdown(card.name)}](${sails.config.custom.baseUrl}/cards/${card.id})`;
const htmlCardLink = `<a href="${sails.config.custom.baseUrl}/cards/${card.id}}">${escapeHtml(card.name)}</a>`;
const commentText = _.truncate(comment.text);
const commentText = _.truncate(formatTextWithMentions(comment.text));
await sails.helpers.utils.sendNotifications(services, t('New Comment'), {
text: `${t(
@@ -91,6 +99,19 @@ module.exports = {
user: values.user,
});
let mentionedUserIds = extractMentionedUserIds(values.text);
if (mentionedUserIds.length > 0) {
const boardMemberUserIds = await sails.helpers.boards.getMemberUserIds(inputs.board.id);
mentionedUserIds = _.difference(
_.intersection(mentionedUserIds, boardMemberUserIds),
comment.userId,
);
}
const mentionedUserIdsSet = new Set(mentionedUserIds);
const cardSubscriptionUserIds = await sails.helpers.cards.getSubscriptionUserIds(
comment.cardId,
comment.userId,
@@ -101,7 +122,11 @@ module.exports = {
comment.userId,
);
const notifiableUserIds = _.union(cardSubscriptionUserIds, boardSubscriptionUserIds);
const notifiableUserIds = _.union(
mentionedUserIds,
cardSubscriptionUserIds,
boardSubscriptionUserIds,
);
await Promise.all(
notifiableUserIds.map((userId) =>
@@ -109,7 +134,9 @@ module.exports = {
values: {
userId,
comment,
type: Notification.Types.COMMENT_CARD,
type: mentionedUserIdsSet.has(userId)
? Notification.Types.MENTION_IN_COMMENT
: Notification.Types.COMMENT_CARD,
data: {
card: _.pick(values.card, ['name']),
text: comment.text,

View File

@@ -6,6 +6,8 @@
const escapeMarkdown = require('escape-markdown');
const escapeHtml = require('escape-html');
const { formatTextWithMentions } = require('../../../utils/formatters');
const buildTitle = (notification, t) => {
switch (notification.type) {
case Notification.Types.MOVE_CARD:
@@ -14,6 +16,8 @@ const buildTitle = (notification, t) => {
return t('New Comment');
case Notification.Types.ADD_MEMBER_TO_CARD:
return t('You Were Added to Card');
case Notification.Types.MENTION_IN_COMMENT:
return t('You Were Mentioned in Comment');
default:
return null;
}
@@ -56,7 +60,7 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
};
}
case Notification.Types.COMMENT_CARD: {
const commentText = _.truncate(notification.data.text);
const commentText = _.truncate(formatTextWithMentions(notification.data.text));
return {
text: `${t(
@@ -95,6 +99,30 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
escapeHtml(board.name),
),
};
case Notification.Types.MENTION_IN_COMMENT: {
const commentText = _.truncate(formatTextWithMentions(notification.data.text));
return {
text: `${t(
'%s mentioned you in %s on %s',
actorUser.name,
card.name,
board.name,
)}:\n${commentText}`,
markdown: `${t(
'%s mentioned you in %s on %s',
escapeMarkdown(actorUser.name),
markdownCardLink,
escapeMarkdown(board.name),
)}:\n\n*${escapeMarkdown(commentText)}*`,
html: `${t(
'%s mentioned you in %s on %s',
escapeHtml(actorUser.name),
htmlCardLink,
escapeHtml(board.name),
)}:\n\n<i>${escapeHtml(commentText)}</i>`,
};
}
default:
return null;
}
@@ -147,6 +175,15 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
boardLink,
)}</p>`;
break;
case Notification.Types.MENTION_IN_COMMENT:
html = `<p>${t(
'%s mentioned you in %s on %s',
escapeHtml(actorUser.name),
cardLink,
boardLink,
)}</p><p>${escapeHtml(notification.data.text)}</p>`;
break;
default:
return;
@@ -186,9 +223,11 @@ module.exports = {
values.userId = values.user.id;
}
const isCommentCard = values.type === Notification.Types.COMMENT_CARD;
const isCommentRelated =
values.type === Notification.Types.COMMENT_CARD ||
values.type === Notification.Types.MENTION_IN_COMMENT;
if (isCommentCard) {
if (isCommentRelated) {
values.commentId = values.comment.id;
} else {
values.actionId = values.action.id;
@@ -217,7 +256,7 @@ module.exports = {
boards: [inputs.board],
lists: [inputs.list],
cards: [values.card],
...(isCommentCard
...(isCommentRelated
? {
comments: [values.comment],
}

View File

@@ -14,6 +14,7 @@ const Types = {
MOVE_CARD: 'moveCard',
COMMENT_CARD: 'commentCard',
ADD_MEMBER_TO_CARD: 'addMemberToCard',
MENTION_IN_COMMENT: 'mentionInComment',
};
module.exports = {

View File

@@ -55,8 +55,8 @@ const LANGUAGES = [
'ro-RO',
'ru-RU',
'sk-SK',
'sr-Cyrl-CS',
'sr-Latn-CS',
'sr-Cyrl-RS',
'sr-Latn-RS',
'sv-SE',
'tr-TR',
'uk-UA',

View File

@@ -19,7 +19,7 @@ module.exports.i18n = {
*
*/
locales: ['en-GB', 'en-US', 'ru-RU'],
locales: ['en-GB', 'en-US', 'it-IT', 'ru-RU'],
/**
*

View File

@@ -7,8 +7,10 @@
"This is a *test* **markdown** `message`!": "This is a *test* **markdown** `message`!",
"This is a <i>test</i> <b>html</b> <code>message</code>": "This is a <i>test</i> <b>html</b> <code>message</code>",
"You Were Added to Card": "Your Were Added to Card",
"You Were Mentioned in Comment": "You Were Mentioned in Comment",
"%s added you to %s on %s": "%s added you to %s on %s",
"%s created %s in %s on %s": "%s created %s in %s on %s",
"%s left a new comment to %s on %s": "%s left a new comment to %s on %s",
"%s mentioned you in %s on %s": "%s mentioned you in %s on %s",
"%s moved %s from %s to %s on %s": "%s moved %s from %s to %s on %s"
}

View File

@@ -7,8 +7,10 @@
"This is a *test* **markdown** `message`!": "This is a *test* **markdown** `message`!",
"This is a <i>test</i> <b>html</b> <code>message</code>": "This is a <i>test</i> <b>html</b> <code>message</code>",
"You Were Added to Card": "Your Were Added to Card",
"You Were Mentioned in Comment": "You Were Mentioned in Comment",
"%s added you to %s on %s": "%s added you to %s on %s",
"%s created %s in %s on %s": "%s created %s in %s on %s",
"%s left a new comment to %s on %s": "%s left a new comment to %s on %s",
"%s mentioned you in %s on %s": "%s mentioned you in %s on %s",
"%s moved %s from %s to %s on %s": "%s moved %s from %s to %s on %s"
}

View File

@@ -0,0 +1,13 @@
{
"Card Created": "Nuova task creata",
"Card Moved": "Task spostata",
"New Comment": "Nuovo commento",
"Test Title": "Titolo di test",
"This is a test text message!": "Questo è un messaggio di testo di test!",
"This is a *test* **markdown** `message`!": "Questo è un *test* **markdown** `messaggio`!",
"This is a <i>test</i> <b>html</b> <code>message</code>": "Questo è un <i>test</i> <b>html</b> <code>messaggio</code>",
"You Were Added to Card": "Sei stato aggiunto alla task",
"%s created %s in %s on %s": "%s ha creato %s in %s in %s",
"%s left a new comment to %s on %s": "%s ha commentato %s in %s",
"%s moved %s from %s to %s on %s": "%s ha spostato %s da %s a %s in %s"
}

View File

@@ -7,8 +7,10 @@
"This is a *test* **markdown** `message`!": "Это *тестовое* **markdown** `сообщение`!",
"This is a <i>test</i> <b>html</b> <code>message</code>": "Это <i>тестовое</i> <b>html</b> <code>сообщение</code>",
"You Were Added to Card": "Вы были добавлены к карточке",
"You Were Mentioned in Comment": "Вы были упомянуты в комментарии",
"%s added you to %s on %s": "%s добавил(а) вас к %s на %s",
"%s created %s in %s on %s": "%s создал(а) %s в %s на %s",
"%s left a new comment to %s on %s": "%s оставил(а) новый комментарий к %s на %s",
"%s mentioned you in %s on %s": "%s упомянул(а) вас в %s на %s",
"%s moved %s from %s to %s on %s": "%s переместил(а) %s из %s в %s на %s"
}

View File

@@ -18,7 +18,7 @@ exports.up = async (knex) => {
FROM comment
GROUP BY card_id
) AS comments_total_by_card_id
WHERE card.id = comments_total_by_card_id.card_id
WHERE card.id = comments_total_by_card_id.card_id;
`);
return knex.schema.alterTable('card', (table) => {

View File

@@ -0,0 +1,26 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
exports.up = (knex) =>
knex.raw(`
UPDATE user_account
SET language =
CASE
WHEN language = 'sr-Cyrl-CS' THEN 'sr-Cyrl-RS'
WHEN language = 'sr-Latn-CS' THEN 'sr-Latn-RS'
END
WHERE language IN ('sr-Cyrl-CS', 'sr-Latn-CS');
`);
exports.down = (knex) =>
knex.raw(`
UPDATE user_account
SET language =
CASE
WHEN language = 'sr-Cyrl-RS' THEN 'sr-Cyrl-CS'
WHEN language = 'sr-Latn-RS' THEN 'sr-Latn-CS'
END
WHERE language IN ('sr-Cyrl-RS', 'sr-Latn-RS');
`);

573
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -69,7 +69,7 @@
"serve-static": "^1.16.2",
"sharp": "^0.33.5",
"uuid": "^9.0.1",
"validator": "^13.15.0",
"validator": "^13.15.15",
"winston": "^3.17.0",
"zxcvbn": "^4.4.2"
},
@@ -79,11 +79,11 @@
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-prettier": "^5.4.1",
"mocha": "^10.8.2",
"nodemon": "^3.1.10",
"prettier": "3.3.3",
"supertest": "^7.1.0"
"supertest": "^7.1.1"
},
"engines": {
"node": ">=18"

View File

@@ -0,0 +1,24 @@
diff --git a/node_modules/sails/lib/hooks/i18n/index.js b/node_modules/sails/lib/hooks/i18n/index.js
index a0c74ed..582ff66 100644
--- a/node_modules/sails/lib/hooks/i18n/index.js
+++ b/node_modules/sails/lib/hooks/i18n/index.js
@@ -108,7 +108,8 @@ module.exports = function(sails) {
locales: sails.config.i18n.locales,
defaultLocale: sails.config.i18n.defaultLocale,
directory: resolvedLocalesDirectory,
- extension: I18N_LOCALES_FILE_EXTENSION
+ extension: I18N_LOCALES_FILE_EXTENSION,
+ devMode: false
});
// Add all of the i18n prototype methods into this hook.
@@ -176,7 +177,8 @@ module.exports = function(sails) {
defaultLocale: sails.config.i18n.defaultLocale,
directory: resolvedLocalesDirectory,
extension: I18N_LOCALES_FILE_EXTENSION,
- request: req
+ request: req,
+ devMode: false
});
// Mix translation capabilities into res.locals.

View File

@@ -0,0 +1,7 @@
const MENTIONS_REGEX = /@\[(.*?)\]\(.*?\)/g;
const formatTextWithMentions = (text) => text.replace(MENTIONS_REGEX, '@$1');
module.exports = {
formatTextWithMentions,
};

View File

@@ -1 +1 @@
module.exports = '2.0.0-rc.4';
module.exports = '2.0.0-rc.3';