feat: Add ability to configure and test SMTP via UI

This commit is contained in:
Maksim Eltyshev
2025-09-22 20:35:13 +02:00
parent 3a12bb7457
commit c6f4dcdb70
114 changed files with 2161 additions and 301 deletions

View File

@@ -224,11 +224,11 @@ extraEnv: []
## value: "Your Name"
## - name: SMTP_SECURE
## value: "true"
## - name: SMTP_TLS_REJECT_UNAUTHORIZED
## value: "false"
## - name: SMTP_USER
## value: "your_email@example.com"
## - name: SMTP_PASSWORD
## value: "your_password"
## - name: SMTP_FROM
## value: "your_email@example.com"
## - name: SMTP_TLS_REJECT_UNAUTHORIZED
## value: "false"

View File

@@ -0,0 +1,59 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import ActionTypes from '../constants/ActionTypes';
const updateConfig = (data) => ({
type: ActionTypes.CONFIG_UPDATE,
payload: {
data,
},
});
updateConfig.success = (config) => ({
type: ActionTypes.CONFIG_UPDATE__SUCCESS,
payload: {
config,
},
});
updateConfig.failure = (error) => ({
type: ActionTypes.CONFIG_UPDATE__FAILURE,
payload: {
error,
},
});
const handleConfigUpdate = (config) => ({
type: ActionTypes.CONFIG_UPDATE_HANDLE,
payload: {
config,
},
});
const testSmtpConfig = () => ({
type: ActionTypes.SMTP_CONFIG_TEST,
payload: {},
});
testSmtpConfig.success = (logs) => ({
type: ActionTypes.SMTP_CONFIG_TEST__SUCCESS,
payload: {
logs,
},
});
testSmtpConfig.failure = (error) => ({
type: ActionTypes.SMTP_CONFIG_TEST__FAILURE,
payload: {
error,
},
});
export default {
updateConfig,
handleConfigUpdate,
testSmtpConfig,
};

View File

@@ -6,6 +6,7 @@
import ActionTypes from '../constants/ActionTypes';
const initializeCore = (
config,
user,
board,
webhooks,
@@ -32,6 +33,7 @@ const initializeCore = (
) => ({
type: ActionTypes.CORE_INITIALIZE,
payload: {
config,
user,
board,
webhooks,
@@ -58,10 +60,10 @@ const initializeCore = (
},
});
initializeCore.fetchConfig = (config) => ({
type: ActionTypes.CORE_INITIALIZE__CONFIG_FETCH,
initializeCore.fetchBootstrap = (bootstrap) => ({
type: ActionTypes.CORE_INITIALIZE__BOOTSTRAP_FETCH,
payload: {
config,
bootstrap,
},
});

View File

@@ -8,6 +8,7 @@ import socket from './socket';
import login from './login';
import core from './core';
import modals from './modals';
import config from './config';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
@@ -36,6 +37,7 @@ export default {
...login,
...core,
...modals,
...config,
...webhooks,
...users,
...projects,

View File

@@ -5,10 +5,10 @@
import ActionTypes from '../constants/ActionTypes';
const initializeLogin = (config) => ({
const initializeLogin = (bootstrap) => ({
type: ActionTypes.LOGIN_INITIALIZE,
payload: {
config,
bootstrap,
},
});

View File

@@ -11,6 +11,7 @@ const handleSocketDisconnect = () => ({
});
const handleSocketReconnect = (
bootstrap,
config,
user,
board,
@@ -38,6 +39,7 @@ const handleSocketReconnect = (
) => ({
type: ActionTypes.SOCKET_RECONNECT_HANDLE,
payload: {
bootstrap,
config,
user,
board,

View File

@@ -65,6 +65,7 @@ const handleUserUpdate = (
user,
projectIds,
boardIds,
bootstrap,
config,
board,
webhooks,
@@ -94,6 +95,7 @@ const handleUserUpdate = (
user,
projectIds,
boardIds,
bootstrap,
config,
board,
webhooks,

14
client/src/api/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,14 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import http from './http';
/* Actions */
const getBootstrap = (headers) => http.get('/bootstrap', undefined, headers);
export default {
getBootstrap,
};

View File

@@ -3,12 +3,18 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import http from './http';
import socket from './socket';
/* Actions */
const getConfig = (headers) => http.get('/config', undefined, headers);
const getConfig = (headers) => socket.get('/config', undefined, headers);
const updateConfig = (data, headers) => socket.patch('/config', data, headers);
const testSmtpConfig = (headers) => socket.post('/config/test-smtp', undefined, headers);
export default {
getConfig,
updateConfig,
testSmtpConfig,
};

View File

@@ -5,9 +5,10 @@
import http from './http';
import socket from './socket';
import config from './config';
import bootstrap from './bootstrap';
import terms from './terms';
import accessTokens from './access-tokens';
import config from './config';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
@@ -35,9 +36,10 @@ import notificationServices from './notification-services';
export { http, socket };
export default {
...config,
...bootstrap,
...terms,
...accessTokens,
...config,
...webhooks,
...users,
...projects,

View File

@@ -5,18 +5,22 @@
import React, { useCallback, useState } from 'react';
import classNames from 'classnames';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Modal, Tab } from 'semantic-ui-react';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { useClosableModal } from '../../../hooks';
import UsersPane from './UsersPane';
import SmtpPane from './SmtpPane';
import WebhooksPane from './WebhooksPane';
import styles from './AdministrationModal.module.scss';
const AdministrationModal = React.memo(() => {
const config = useSelector(selectors.selectConfig);
const dispatch = useDispatch();
const [t] = useTranslation();
const [activeTabIndex, setActiveTabIndex] = useState(0);
@@ -38,13 +42,21 @@ const AdministrationModal = React.memo(() => {
}),
render: () => <UsersPane />,
},
{
];
if (config.smtpHost !== undefined) {
panes.push({
menuItem: t('common.smtp', {
context: 'title',
}),
render: () => <SmtpPane />,
});
}
panes.push({
menuItem: t('common.webhooks', {
context: 'title',
}),
render: () => <WebhooksPane />,
},
];
});
const isUsersPaneActive = activeTabIndex === 0;

View File

@@ -0,0 +1,227 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { dequal } from 'dequal';
import React, { useCallback, useMemo } from 'react';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import TextareaAutosize from 'react-textarea-autosize';
import { Button, Checkbox, Divider, Form, Header, Tab, TextArea } from 'semantic-ui-react';
import { Input } from '../../../lib/custom-ui';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { useForm, useNestedRef } from '../../../hooks';
import styles from './SmtpPane.module.scss';
const SmtpPane = React.memo(() => {
const config = useSelector(selectors.selectConfig);
const smtpTest = useSelector(selectors.selectSmtpTest);
const dispatch = useDispatch();
const [t] = useTranslation();
const [passwordFieldRef, handlePasswordFieldRef] = useNestedRef('inputRef');
const defaultData = useMemo(
() => ({
smtpHost: config.smtpHost,
smtpPort: config.smtpPort,
smtpName: config.smtpName,
smtpSecure: config.smtpSecure,
smtpTlsRejectUnauthorized: config.smtpTlsRejectUnauthorized,
smtpUser: config.smtpUser,
smtpPassword: config.smtpPassword,
smtpFrom: config.smtpFrom,
}),
[config],
);
const [data, handleFieldChange] = useForm(() => ({
...defaultData,
smtpHost: defaultData.smtpHost || '',
smtpPort: defaultData.smtpPort || '',
smtpName: defaultData.smtpName || '',
smtpSecure: defaultData.smtpSecure,
smtpTlsRejectUnauthorized: defaultData.smtpTlsRejectUnauthorized,
smtpUser: defaultData.smtpUser || '',
smtpPassword: defaultData.smtpPassword || '',
smtpFrom: defaultData.smtpFrom || '',
}));
const isPasswordSet = defaultData.smtpPassword === undefined;
const cleanData = useMemo(
() => ({
...data,
smtpHost: data.smtpHost.trim() || null,
smtpPort: parseInt(data.smtpPort, 10) || null,
smtpName: data.smtpName.trim() || null,
smtpUser: data.smtpUser.trim() || null,
smtpPassword: data.smtpPassword || (isPasswordSet ? undefined : null),
smtpFrom: data.smtpFrom.trim() || null,
}),
[data, isPasswordSet],
);
const handleSubmit = useCallback(() => {
dispatch(entryActions.updateConfig(cleanData));
}, [dispatch, cleanData]);
const handlePasswordClear = useCallback(() => {
dispatch(
entryActions.updateConfig({
smtpPassword: null,
}),
);
passwordFieldRef.current.focus();
}, [dispatch, passwordFieldRef]);
const handleTestClick = useCallback(() => {
dispatch(entryActions.testSmtpConfig());
}, [dispatch]);
const isModified = !dequal(cleanData, defaultData);
return (
<Tab.Pane attached={false} className={styles.wrapper}>
<Form onSubmit={handleSubmit}>
<div className={styles.text}>{t('common.host')}</div>
<Input
fluid
name="smtpHost"
value={data.smtpHost}
maxLength={256}
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>{t('common.port')}</div>
<Input
fluid
type="number"
name="smtpPort"
value={data.smtpPort}
placeholder={data.smtpSecure ? '465' : '587'}
min={0}
max={65535}
step={1}
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>
{t('common.clientHostnameInEhlo')} (
{t('common.optional', {
context: 'inline',
})}
)
</div>
<Input
fluid
name="smtpName"
value={data.smtpName}
maxLength={256}
className={styles.field}
onChange={handleFieldChange}
/>
<Checkbox
name="smtpSecure"
checked={data.smtpSecure}
label={t('common.useSecureConnection')}
className={styles.checkbox}
onChange={handleFieldChange}
/>
<Checkbox
name="smtpTlsRejectUnauthorized"
checked={data.smtpTlsRejectUnauthorized}
label={t('common.rejectUnauthorizedTlsCertificates')}
className={classNames(styles.field, styles.checkbox)}
onChange={handleFieldChange}
/>
<div className={styles.text}>
{t('common.username')} (
{t('common.optional', {
context: 'inline',
})}
)
</div>
<Input
fluid
name="smtpUser"
value={data.smtpUser}
maxLength={256}
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.text}>
{t('common.password')} (
{t('common.optional', {
context: 'inline',
})}
)
</div>
<Input.Password
fluid
ref={handlePasswordFieldRef}
name="smtpPassword"
value={data.smtpPassword}
placeholder={isPasswordSet ? t('common.passwordIsSet') : undefined}
maxLength={256}
className={styles.field}
onClear={!data.smtpPassword && isPasswordSet ? handlePasswordClear : undefined}
onChange={handleFieldChange}
/>
<div className={styles.text}>
{t('common.defaultFrom')} (
{t('common.optional', {
context: 'inline',
})}
)
</div>
<Input
fluid
name="smtpFrom"
value={data.smtpFrom}
maxLength={256}
className={styles.field}
onChange={handleFieldChange}
/>
<div className={styles.controls}>
<Button positive disabled={!isModified} content={t('action.save')} />
{config.smtpHost && !isModified && (
<Button
type="button"
content={t('action.sendTestEmail')}
loading={smtpTest.isLoading}
disabled={smtpTest.isLoading}
onClick={handleTestClick}
/>
)}
</div>
</Form>
{smtpTest.logs && (
<>
<Divider horizontal>
<Header as="h4">
{t('common.testLog', {
context: 'title',
})}
</Header>
</Divider>
<TextArea
readOnly
as={TextareaAutosize}
value={smtpTest.logs.join('\n')}
className={styles.testLog}
/>
</>
)}
</Tab.Pane>
);
});
export default SmtpPane;

View File

@@ -0,0 +1,41 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.checkbox {
display: block;
padding: 8px 0;
}
.controls {
display: flex;
justify-content: space-between;
}
.field {
margin-bottom: 8px;
}
.testLog {
border: 1px solid rgba(9, 30, 66, 0.13);
border-radius: 3px;
color: #333;
line-height: 1.4;
padding: 8px 12px;
width: 100%;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}
.wrapper {
border: none;
box-shadow: none;
}
}

View File

@@ -22,8 +22,8 @@ const UsersPane = React.memo(() => {
const activeUsersTotal = useSelector(selectors.selectActiveUsersTotal);
const canAdd = useSelector((state) => {
const oidcConfig = selectors.selectOidcConfig(state);
return !oidcConfig || !oidcConfig.isEnforced;
const oidcBootstrap = selectors.selectOidcBootstrap(state);
return !oidcBootstrap || !oidcBootstrap.isEnforced;
});
const [t] = useTranslation();

View File

@@ -30,8 +30,8 @@ const Core = React.memo(() => {
// TODO: move to selector?
const isNewVersionAvailable = useSelector((state) => {
const config = selectors.selectConfig(state);
return !!config && config.version !== version;
const bootstrap = selectors.selectBootstrap(state);
return !!bootstrap && bootstrap.version !== version;
});
const [t] = useTranslation();

View File

@@ -86,7 +86,7 @@ const createMessage = (error) => {
};
const Content = React.memo(() => {
const config = useSelector(selectors.selectConfig);
const bootstrap = useSelector(selectors.selectBootstrap);
const {
data: defaultData,
@@ -139,8 +139,8 @@ const Content = React.memo(() => {
dispatch(entryActions.clearAuthenticateError());
}, [dispatch]);
const withOidc = !!config.oidc;
const isOidcEnforced = withOidc && config.oidc.isEnforced;
const withOidc = !!bootstrap.oidc;
const isOidcEnforced = withOidc && bootstrap.oidc.isEnforced;
useEffect(() => {
if (!isOidcEnforced) {

View File

@@ -39,7 +39,7 @@ export default {
/* Core */
CORE_INITIALIZE: 'CORE_INITIALIZE',
CORE_INITIALIZE__CONFIG_FETCH: 'CORE_INITIALIZE__CONFIG_FETCH',
CORE_INITIALIZE__BOOTSTRAP_FETCH: 'CORE_INITIALIZE__BOOTSTRAP_FETCH',
FAVORITES_TOGGLE: 'FAVORITES_TOGGLE',
EDIT_MODE_TOGGLE: 'EDIT_MODE_TOGGLE',
HOME_VIEW_UPDATE: 'HOME_VIEW_UPDATE',
@@ -51,6 +51,16 @@ export default {
MODAL_OPEN: 'MODAL_OPEN',
MODAL_CLOSE: 'MODAL_CLOSE',
/* Config */
CONFIG_UPDATE: 'CONFIG_UPDATE',
CONFIG_UPDATE__SUCCESS: 'CONFIG_UPDATE__SUCCESS',
CONFIG_UPDATE__FAILURE: 'CONFIG_UPDATE__FAILURE',
CONFIG_UPDATE_HANDLE: 'CONFIG_UPDATE_HANDLE',
SMTP_CONFIG_TEST: 'SMTP_CONFIG_TEST',
SMTP_CONFIG_TEST__SUCCESS: 'SMTP_CONFIG_TEST__SUCCESS',
SMTP_CONFIG_TEST__FAILURE: 'SMTP_CONFIG_TEST__FAILURE',
/* Webhooks */
WEBHOOK_CREATE: 'WEBHOOK_CREATE',

View File

@@ -34,6 +34,12 @@ export default {
MODAL_OPEN: `${PREFIX}/MODAL_OPEN`,
MODAL_CLOSE: `${PREFIX}/MODAL_CLOSE`,
/* Config */
CONFIG_UPDATE: `${PREFIX}/CONFIG_UPDATE`,
CONFIG_UPDATE_HANDLE: `${PREFIX}/CONFIG_UPDATE_HANDLE`,
SMTP_CONFIG_TEST: `${PREFIX}/SMTP_CONFIG_TEST`,
/* Webhooks */
WEBHOOK_CREATE: `${PREFIX}/WEBHOOK_CREATE`,

View File

@@ -39,6 +39,8 @@ export default [
'commentUpdate',
'commentDelete',
'configUpdate',
'customFieldCreate',
'customFieldUpdate',
'customFieldDelete',

View File

@@ -0,0 +1,31 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import EntryActionTypes from '../constants/EntryActionTypes';
const updateConfig = (data) => ({
type: EntryActionTypes.CONFIG_UPDATE,
payload: {
data,
},
});
const handleConfigUpdate = (config) => ({
type: EntryActionTypes.CONFIG_UPDATE_HANDLE,
payload: {
config,
},
});
const testSmtpConfig = () => ({
type: EntryActionTypes.SMTP_CONFIG_TEST,
payload: {},
});
export default {
updateConfig,
handleConfigUpdate,
testSmtpConfig,
};

View File

@@ -7,6 +7,7 @@ import socket from './socket';
import login from './login';
import core from './core';
import modals from './modals';
import config from './config';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
@@ -34,6 +35,7 @@ export default {
...login,
...core,
...modals,
...config,
...webhooks,
...users,
...projects,

View File

@@ -5,13 +5,15 @@
import { useCallback, useState } from 'react';
const CHECKED_TYPES_SET = new Set(['checkbox', 'radio']);
export default (initialData) => {
const [data, setData] = useState(initialData);
const handleFieldChange = useCallback((_, { type, name: fieldName, value, checked }) => {
setData((prevData) => ({
...prevData,
[fieldName]: type === 'radio' ? checked : value,
[fieldName]: CHECKED_TYPES_SET.has(type) ? checked : value,
}));
}, []);

View File

@@ -14,7 +14,7 @@ import styles from './InputPassword.module.css';
const STRENGTH_SCORE_COLORS = ['red', 'orange', 'yellow', 'olive', 'green'];
const InputPassword = React.forwardRef(
({ value, withStrengthBar, minStrengthScore, className, ...props }, ref) => {
({ value, withStrengthBar, minStrengthScore, className, onClear, ...props }, ref) => {
const [isVisible, toggleVisible] = useToggle();
const strengthScore = useMemo(() => {
@@ -32,8 +32,13 @@ const InputPassword = React.forwardRef(
const inputProps = {
...props,
ref,
value,
type: isVisible ? 'text' : 'password',
icon: <Icon link name={isVisible ? 'eye' : 'eye slash'} onClick={handleToggleClick} />,
icon: onClear ? (
<Icon link name="cancel" onClick={onClear} />
) : (
<Icon link name={isVisible ? 'eye' : 'eye slash'} onClick={handleToggleClick} />
),
};
if (!withStrengthBar) {
@@ -68,12 +73,14 @@ InputPassword.propTypes = {
withStrengthBar: PropTypes.bool,
minStrengthScore: PropTypes.number,
className: PropTypes.string,
onClear: PropTypes.func,
};
InputPassword.defaultProps = {
withStrengthBar: false,
minStrengthScore: 2,
className: undefined,
onClear: undefined,
};
export default React.memo(InputPassword);

View File

@@ -98,6 +98,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'اللون',
comments: null,
@@ -120,6 +121,7 @@ export default {
date: 'تاريخ',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'حذف المرفق',
@@ -183,6 +185,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'ساعات',
importBoard_title: 'استيراد اللوحة',
invalidCurrentPassword: 'كلمة المرور الحالية غير صالحة',
@@ -226,8 +229,10 @@ export default {
optional_inline: 'اختياري',
organization: 'المنظمة',
others: null,
passwordIsSet: null,
phone: 'الهاتف',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'التفضيلات',
pressPasteShortcutToAddAttachmentFromClipboard:
'نصيحة: اضغط على Ctrl-V (Cmd-V على Mac) لإضافة مرفق من الحافظة.',
@@ -236,6 +241,7 @@ export default {
projectNotFound_title: 'المشروع غير موجود',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'إزالة المدير',
removeMember_title: 'إزالة العضو',
role: null,
@@ -262,6 +268,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: 'فرز القائمة',
stopwatch: 'المؤقت',
story: null,
@@ -273,6 +280,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'لا يوجد معاينة متاحة لهذا المرفق.',
time: 'الوقت',
title: 'العنوان',
@@ -286,6 +294,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'إجراءات المستخدم',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> تمت إضافة هذه البطاقة إلى {{list}}',
@@ -422,6 +431,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'حفظ',
sendTestEmail: null,
showActive: null,
showAllAttachments: 'إظهار جميع المرفقات ({{hidden}} hidden)',
showCardsWithThisUser: null,

View File

@@ -102,6 +102,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'Цвят',
comments: null,
@@ -124,6 +125,7 @@ export default {
date: 'Дата',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'Изтриване на прикачен файл',
@@ -187,6 +189,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'Часове',
importBoard_title: 'Импортиране на табло',
invalidCurrentPassword: 'Невалидна текуща парола',
@@ -230,8 +233,10 @@ export default {
optional_inline: 'по желание',
organization: 'Организация',
others: null,
passwordIsSet: null,
phone: 'Телефон',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'Предпочитания',
pressPasteShortcutToAddAttachmentFromClipboard:
'Съвет: натиснете Ctrl-V (Cmd-V на Mac), за да добавите прикачен файл от клипборда',
@@ -240,6 +245,7 @@ export default {
projectNotFound_title: 'Проектът не е намерен',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Премахване на мениджър',
removeMember_title: 'Премахване на член',
role: null,
@@ -266,6 +272,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: 'Сортиране на списък',
stopwatch: 'Хронометър',
story: null,
@@ -277,6 +284,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'Няма наличен преглед за този прикачен файл.',
time: 'Време',
title: 'Заглавие',
@@ -290,6 +298,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'Потребителски действия',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> добави тази карта в {{list}}',
@@ -427,6 +436,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'Запазване',
sendTestEmail: null,
showActive: null,
showAllAttachments: 'Показване на всички прикачени файлове ({{hidden}} скрити)',
showCardsWithThisUser: null,

View File

@@ -111,6 +111,7 @@ export default {
'Karty na tomto seznamu jsou kompletní a připravené k archivaci.',
cardsOnThisListAreReadyToBeWorkedOn: 'Karty na tomto seznamu jsou připraveny k práci.',
clickHereOrRefreshPageToUpdate: '<0>Klikněte sem</0> nebo aktualizujte stránku.',
clientHostnameInEhlo: null,
closed: 'Uzavřeno',
color: 'Barva',
comments: 'Komentáře',
@@ -133,6 +134,7 @@ export default {
date: 'Datum',
deactivateUser_title: 'Deaktivace uživatele',
defaultCardType_title: 'Výchozí typ karty',
defaultFrom: null,
defaultView_title: 'Výchozí zobrazení',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Pro smazání tohoto projektu je třeba nejprve smazat všechny nástěnky',
@@ -197,6 +199,7 @@ export default {
grid: 'Mřížka',
hideCompletedTasks: 'Skrýt dokončené úkoly',
hideFromProjectListAndFavorites: 'Skrýt ze seznamu projektů a oblíbených položek',
host: null,
hours: 'Hodiny',
importBoard_title: 'Importovat nástěnku',
invalidCurrentPassword: 'Neplatné aktuální heslo',
@@ -241,9 +244,11 @@ export default {
optional_inline: 'volitelné',
organization: 'Společnost',
others: 'Jiné',
passwordIsSet: null,
phone: 'Telefon',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA používá <1><0>Apprise</0></1> k zasílání oznámení do více než 100 oblíbených služeb.',
port: null,
preferences: 'Volby',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: stisknutím Ctrl-V (Cmd-V na Macu) přidáte přílohu ze schránky.',
@@ -252,6 +257,7 @@ export default {
projectNotFound_title: 'Projekt nenalezen',
projectOwner: 'Vlastník projektu',
referenceDataAndKnowledgeStorage: 'Uchovávání referenčních údajů a znalostí.',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Odstranit správce',
removeMember_title: 'Odstranit člena',
role: 'Role',
@@ -278,6 +284,7 @@ export default {
shared: 'Sdílené',
sharedWithMe_title: 'Sdíleno se mnou',
showOnFrontOfCard: 'Zobrazit na přední straně karty',
smtp: null,
sortList_title: 'Řadit podle',
stopwatch: 'Časovač',
story: 'Příběh',
@@ -289,6 +296,7 @@ export default {
taskList_title: 'Seznam úkolů',
team: 'Tým',
terms: 'Podmínky',
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'Pro tuto přílohu není k dispozici žádný náhled.',
time: 'Čas',
title: 'Titulek',
@@ -302,6 +310,7 @@ export default {
uploadFailedNotEnoughStorageSpace: 'Nahrávání se nezdařilo: Nedostatek úložného prostoru.',
uploadedImages: 'Nahrané obrázky',
url: 'URL',
useSecureConnection: null,
userActions_title: 'Akce uživatele',
userAddedCardToList: '<0>{{user}}</0> přidal <2>{{card}}</2> do {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> přidal kartu do {{list}}',
@@ -442,6 +451,7 @@ export default {
restoreToList: 'Obnovit do {{list}}',
returnToBoard: 'Návrat na nástěnku',
save: 'Uložit',
sendTestEmail: null,
showActive: 'Zobrazit aktivní',
showAllAttachments: 'Zozbrazit všechny přílohy ({{hidden}} skryté)',
showCardsWithThisUser: 'Zobrazit karty tohoto uživatele',

View File

@@ -115,6 +115,7 @@ export default {
'Kort på denne liste er afsluttede og klar til at blive arkiveret.',
cardsOnThisListAreReadyToBeWorkedOn: 'Kort på denne liste er klar til at blive arbejdet på.',
clickHereOrRefreshPageToUpdate: '<0>Klik her</0> eller opdater siden for at opdatere.',
clientHostnameInEhlo: null,
closed: 'Lukket',
color: 'Farve',
comments: 'Kommentarer',
@@ -138,6 +139,7 @@ export default {
date: 'Dato',
deactivateUser_title: 'Deaktiver bruger',
defaultCardType_title: 'Standard korttype',
defaultFrom: null,
defaultView_title: 'Standard visning',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Slet alle tavler for at kunne slette dette projekt.',
@@ -202,6 +204,7 @@ export default {
grid: 'Gitter',
hideCompletedTasks: null,
hideFromProjectListAndFavorites: 'Skjul fra projektliste og favoritter',
host: null,
hours: 'Timer',
importBoard_title: 'Importer tavle',
invalidCurrentPassword: 'Nuværende adgangskode er ugyldig',
@@ -245,9 +248,11 @@ export default {
optional_inline: 'valgfri',
organization: 'Organisation',
others: 'Andre',
passwordIsSet: null,
phone: 'Telefon',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA bruger <1><0>Apprise</0></1> til at sende notifikationer til over 100 populære tjenester.',
port: null,
preferences: 'Præferencer',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: Tryk Ctrl-V (Cmd-V på Mac) for at vedhæfte direkte fra udklipsholder.',
@@ -256,6 +261,7 @@ export default {
projectNotFound_title: 'Projekt ikke fundet',
projectOwner: 'Projektejer',
referenceDataAndKnowledgeStorage: 'Reference data og vidensopbevaring',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Fjern projektleder',
removeMember_title: 'Fjern medlem',
role: 'Rolle',
@@ -282,6 +288,7 @@ export default {
shared: 'Delt',
sharedWithMe_title: 'Delt med mig',
showOnFrontOfCard: 'Vis på forsiden af kortet',
smtp: null,
sortList_title: 'Sortér liste',
stopwatch: 'Stopur',
story: 'Story',
@@ -293,6 +300,7 @@ export default {
taskList_title: 'Opgaveliste',
team: 'Team',
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment:
'Der er ingen forhåndsvisning tilgængelig for denne vedhæftning.',
time: 'Tid',
@@ -307,6 +315,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Uploadede billeder',
url: null,
useSecureConnection: null,
userActions_title: 'Brugerhandlinger',
userAddedCardToList: '<0>{{user}}</0> tilføjede <2>{{card}}</2> til {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> tilføjede kortet til {{list}}',
@@ -447,6 +456,7 @@ export default {
restoreToList: 'Gendan til {{list}}',
returnToBoard: 'Tilbage til tavle',
save: 'Gem ændringer',
sendTestEmail: null,
showActive: 'Vis aktive',
showAllAttachments: 'Vis alle vedhæftninger ({{hidden}} skjulte)',
showCardsWithThisUser: 'Vis kort med denne bruger',

View File

@@ -125,6 +125,7 @@ export default {
cardsOnThisListAreReadyToBeWorkedOn: 'Karten in dieser Liste sind bereit zur Bearbeitung.',
clickHereOrRefreshPageToUpdate:
'<0>Hier klicken</0> oder Seite aktualisieren, um zu aktualisieren.',
clientHostnameInEhlo: null,
closed: 'Geschlossen',
color: 'Farbe',
comments: 'Kommentare',
@@ -147,6 +148,7 @@ export default {
date: 'Datum',
deactivateUser_title: 'Benutzer deaktivieren',
defaultCardType_title: 'Standard-Kartentyp',
defaultFrom: null,
defaultView_title: 'Standardansicht',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Löschen Sie alle Arbeitsbereiche, um dieses Projekt löschen zu können',
@@ -211,6 +213,7 @@ export default {
grid: 'Raster',
hideCompletedTasks: null,
hideFromProjectListAndFavorites: 'Aus Projektliste und Favoriten ausblenden',
host: null,
hours: 'Stunden',
importBoard_title: 'Board importieren',
invalidCurrentPassword: 'Das aktuelle Passwort ist falsch',
@@ -254,9 +257,11 @@ export default {
optional_inline: 'Optional',
organization: 'Organisation',
others: 'Andere',
passwordIsSet: null,
phone: 'Telefon',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA verwendet <1><0>Apprise</0></1>, um Benachrichtigungen an über 100 beliebte Dienste zu senden.',
port: null,
preferences: 'Voreinstellungen',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tipp: Drücken Sie STRG-V (Cmd-V auf Mac), um einen Anhang aus der Zwischenablage hinzuzufügen.',
@@ -265,6 +270,7 @@ export default {
projectNotFound_title: 'Projekt nicht gefunden',
projectOwner: 'Projektleitung',
referenceDataAndKnowledgeStorage: 'Speichern von Wissen und Referenzen.',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Projektleiter entfernen',
removeMember_title: 'Mitglied entfernen',
role: 'Rolle',
@@ -291,6 +297,7 @@ export default {
shared: 'Geteilt',
sharedWithMe_title: 'Mit mir geteilt',
showOnFrontOfCard: 'Auf der Vorderseite der Karte anzeigen',
smtp: null,
sortList_title: 'Liste sortieren',
stopwatch: 'Stoppuhr',
story: 'Wissen',
@@ -302,6 +309,7 @@ export default {
taskList_title: 'Aufgaben',
team: 'Team',
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'Für diesen Anhang ist keine Vorschau verfügbar.',
time: 'Zeit',
title: 'Titel',
@@ -315,6 +323,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Hochgeladene Bilder',
url: null,
useSecureConnection: null,
userActions_title: 'Benutzeraktionen',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> hat diese Karte hinzugefügt zu {{list}}',
@@ -452,6 +461,7 @@ export default {
restoreToList: 'Wiederherstellen in {{list}}',
returnToBoard: 'Zurück zum Arbeitsbereich',
save: 'Speichern',
sendTestEmail: null,
showActive: 'Aktive anzeigen',
showAllAttachments: 'Alle Anhänge anzeigen ({{hidden}} versteckt)',
showCardsWithThisUser: 'Karten mit diesem Benutzer zeigen',

View File

@@ -125,6 +125,7 @@ export default {
cardsOnThisListAreReadyToBeWorkedOn:
'Οι κάρτες σε αυτήν τη λίστα είναι έτοιμες για επεξεργασία.',
clickHereOrRefreshPageToUpdate: '<0>Κάντε κλικ εδώ</0> ή ανανεώστε τη σελίδα για ενημέρωση.',
clientHostnameInEhlo: null,
closed: 'Κλειστό',
color: 'Χρώμα',
comments: 'Σχόλια',
@@ -148,6 +149,7 @@ export default {
date: 'Ημερομηνία',
deactivateUser_title: 'Απενεργοποίηση χρήστη',
defaultCardType_title: 'Προεπιλεγμένος τύπος κάρτας',
defaultFrom: null,
defaultView_title: 'Προεπιλεγμένη προβολή',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Διαγράψτε όλους τους πίνακες για να μπορέσετε να διαγράψετε αυτό το έργο',
@@ -212,6 +214,7 @@ export default {
grid: 'Πλέγμα',
hideCompletedTasks: null,
hideFromProjectListAndFavorites: 'Απόκρυψη από τη λίστα έργων και τα αγαπημένα',
host: null,
hours: 'Ώρες',
importBoard_title: 'Εισαγωγή πίνακα',
invalidCurrentPassword: 'Μη έγκυρος τρέχων κωδικός',
@@ -255,9 +258,11 @@ export default {
optional_inline: 'προαιρετικό',
organization: 'Οργάνωση',
others: 'Άλλοι',
passwordIsSet: null,
phone: 'Τηλέφωνο',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'Το PLANKA χρησιμοποιεί το <1><0>Apprise</0></1> για να στέλνει ειδοποιήσεις σε πάνω από 100 δημοφιλείς υπηρεσίες.',
port: null,
preferences: 'Προτιμήσεις',
pressPasteShortcutToAddAttachmentFromClipboard:
'Συμβουλή: πατήστε Ctrl-V (Cmd-V σε Mac) για να προσθέσετε συνημμένο από το πρόχειρο.',
@@ -266,6 +271,7 @@ export default {
projectNotFound_title: 'Το έργο δεν βρέθηκε',
projectOwner: 'Ιδιοκτήτης έργου',
referenceDataAndKnowledgeStorage: 'Αποθήκευση δεδομένων και γνώσης αναφοράς.',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Αφαίρεση διαχειριστή',
removeMember_title: 'Αφαίρεση μέλους',
role: 'Ρόλος',
@@ -292,6 +298,7 @@ export default {
shared: 'Κοινόχρηστο',
sharedWithMe_title: 'Κοινόχρηστο με εμένα',
showOnFrontOfCard: 'Εμφάνιση στο μπροστινό μέρος της κάρτας',
smtp: null,
sortList_title: 'Ταξινόμηση λίστας',
stopwatch: 'Χρονόμετρο',
story: 'Ιστορία',
@@ -303,6 +310,7 @@ export default {
taskList_title: 'Λίστα εργασιών',
team: 'Ομάδα',
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment:
'Δεν υπάρχει διαθέσιμη προεπισκόπηση για αυτό το συνημμένο.',
time: 'Ώρα',
@@ -317,6 +325,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Μεταφορτωμένες εικόνες',
url: null,
useSecureConnection: null,
userActions_title: 'Ενέργειες χρήστη',
userAddedCardToList: '<0>{{user}}</0> πρόσθεσε <2>{{card}}</2> στη λίστα {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> πρόσθεσε αυτήν την κάρτα στη λίστα {{list}}',
@@ -463,6 +472,7 @@ export default {
restoreToList: 'Επαναφορά στη {{list}}',
returnToBoard: 'Επιστροφή στον πίνακα',
save: 'Αποθήκευση',
sendTestEmail: null,
showActive: 'Εμφάνιση ενεργών',
showAllAttachments: 'Εμφάνιση όλων των συνημμένων ({{hidden}} κρυφά)',
showCardsWithThisUser: 'Εμφάνιση καρτών με αυτόν τον χρήστη',

View File

@@ -115,6 +115,7 @@ export default {
'Cards on this list are complete and ready to be archived.',
cardsOnThisListAreReadyToBeWorkedOn: 'Cards on this list are ready to be worked on.',
clickHereOrRefreshPageToUpdate: '<0>Click here</0> or refresh the page to update.',
clientHostnameInEhlo: 'Client hostname in EHLO',
closed: 'Closed',
color: 'Color',
comments: 'Comments',
@@ -137,6 +138,7 @@ export default {
date: 'Date',
deactivateUser_title: 'Deactivate User',
defaultCardType_title: 'Default Card Type',
defaultFrom: 'Default "from"',
defaultView_title: 'Default View',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Delete all boards to be able to delete this project',
@@ -201,6 +203,7 @@ export default {
grid: 'Grid',
hideCompletedTasks: 'Hide completed tasks',
hideFromProjectListAndFavorites: 'Hide from project list and favorites',
host: 'Host',
hours: 'Hours',
importBoard_title: 'Import Board',
invalidCurrentPassword: 'Invalid current password',
@@ -245,9 +248,11 @@ export default {
optional_inline: 'optional',
organization: 'Organization',
others: 'Others',
passwordIsSet: 'Password is set',
phone: 'Phone',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA uses <1><0>Apprise</0></1> to send notifications to over 100 popular services.',
port: 'Port',
preferences: 'Preferences',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard.',
@@ -256,6 +261,7 @@ export default {
projectNotFound_title: 'Project Not Found',
projectOwner: 'Project owner',
referenceDataAndKnowledgeStorage: 'Reference data and knowledge storage.',
rejectUnauthorizedTlsCertificates: 'Reject unauthorized TLS certificates',
removeManager_title: 'Remove Manager',
removeMember_title: 'Remove Member',
role: 'Role',
@@ -282,6 +288,7 @@ export default {
shared: 'Shared',
sharedWithMe_title: 'Shared With Me',
showOnFrontOfCard: 'Show on front of card',
smtp: 'SMTP',
sortList_title: 'Sort List',
stopwatch: 'Stopwatch',
story: 'Story',
@@ -293,6 +300,7 @@ export default {
taskList_title: 'Task List',
team: 'Team',
terms: 'Terms',
testLog_title: 'Test Log',
thereIsNoPreviewAvailableForThisAttachment:
'There is no preview available for this attachment.',
time: 'Time',
@@ -307,6 +315,7 @@ export default {
uploadFailedNotEnoughStorageSpace: 'Upload failed: Not enough storage space.',
uploadedImages: 'Uploaded images',
url: 'URL',
useSecureConnection: 'Use secure connection',
userActions_title: 'User Actions',
userAddedCardToList: '<0>{{user}}</0> added <2>{{card}}</2> to {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> added this card to {{list}}',
@@ -446,6 +455,7 @@ export default {
restoreToList: 'Restore to {{list}}',
returnToBoard: 'Return to board',
save: 'Save',
sendTestEmail: 'Send test email',
showActive: 'Show active',
showAllAttachments: 'Show all attachments ({{hidden}} hidden)',
showCardsWithThisUser: 'Show cards with this user',

View File

@@ -110,6 +110,7 @@ export default {
'Cards on this list are complete and ready to be archived.',
cardsOnThisListAreReadyToBeWorkedOn: 'Cards on this list are ready to be worked on.',
clickHereOrRefreshPageToUpdate: '<0>Click here</0> or refresh the page to update.',
clientHostnameInEhlo: 'Client hostname in EHLO',
closed: 'Closed',
color: 'Color',
comments: 'Comments',
@@ -132,6 +133,7 @@ export default {
date: 'Date',
deactivateUser_title: 'Deactivate User',
defaultCardType_title: 'Default Card Type',
defaultFrom: 'Default "from"',
defaultView_title: 'Default View',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Delete all boards to be able to delete this project',
@@ -196,6 +198,7 @@ export default {
grid: 'Grid',
hideCompletedTasks: 'Hide completed tasks',
hideFromProjectListAndFavorites: 'Hide from project list and favorites',
host: 'Host',
hours: 'Hours',
importBoard_title: 'Import Board',
invalidCurrentPassword: 'Invalid current password',
@@ -240,9 +243,11 @@ export default {
optional_inline: 'optional',
organization: 'Organization',
others: 'Others',
passwordIsSet: 'Password is set',
phone: 'Phone',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA uses <1><0>Apprise</0></1> to send notifications to over 100 popular services.',
port: 'Port',
preferences: 'Preferences',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: press Ctrl-V (Cmd-V on Mac) to add an attachment from the clipboard.',
@@ -251,6 +256,7 @@ export default {
projectNotFound_title: 'Project Not Found',
projectOwner: 'Project owner',
referenceDataAndKnowledgeStorage: 'Reference data and knowledge storage.',
rejectUnauthorizedTlsCertificates: 'Reject unauthorized TLS certificates',
removeManager_title: 'Remove Manager',
removeMember_title: 'Remove Member',
role: 'Role',
@@ -277,6 +283,7 @@ export default {
shared: 'Shared',
sharedWithMe_title: 'Shared With Me',
showOnFrontOfCard: 'Show on front of card',
smtp: 'SMTP',
sortList_title: 'Sort List',
stopwatch: 'Stopwatch',
story: 'Story',
@@ -288,6 +295,7 @@ export default {
taskList_title: 'Task List',
team: 'Team',
terms: 'Terms',
testLog_title: 'Test Log',
thereIsNoPreviewAvailableForThisAttachment:
'There is no preview available for this attachment.',
time: 'Time',
@@ -302,6 +310,7 @@ export default {
uploadFailedNotEnoughStorageSpace: 'Upload failed: Not enough storage space.',
uploadedImages: 'Uploaded images',
url: 'URL',
useSecureConnection: 'Use secure connection',
userActions_title: 'User Actions',
userAddedCardToList: '<0>{{user}}</0> added <2>{{card}}</2> to {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> added this card to {{list}}',
@@ -441,6 +450,7 @@ export default {
restoreToList: 'Restore to {{list}}',
returnToBoard: 'Return to board',
save: 'Save',
sendTestEmail: 'Send test email',
showActive: 'Show active',
showAllAttachments: 'Show all attachments ({{hidden}} hidden)',
showCardsWithThisUser: 'Show cards with this user',

View File

@@ -116,6 +116,7 @@ export default {
cardsOnThisListAreReadyToBeWorkedOn:
'Las tarjetas en esta lista están listas para ser trabajadas.',
clickHereOrRefreshPageToUpdate: '<0>Haz clic aquí</0> o actualiza la página para actualizar.',
clientHostnameInEhlo: null,
closed: 'Cerrado',
color: 'Color',
comments: 'Comentarios',
@@ -138,6 +139,7 @@ export default {
date: 'Fecha',
deactivateUser_title: 'Desactivar usuario',
defaultCardType_title: 'Tipo de tarjeta por defecto',
defaultFrom: null,
defaultView_title: 'Vista por defecto',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Elimina todos los tableros para poder eliminar este proyecto.',
@@ -202,6 +204,7 @@ export default {
grid: 'Cuadrícula',
hideCompletedTasks: null,
hideFromProjectListAndFavorites: 'Ocultar de la lista de proyectos y favoritos',
host: null,
hours: 'Horas',
importBoard_title: 'Importar tablero',
invalidCurrentPassword: 'Contraseña actual inválida',
@@ -245,9 +248,11 @@ export default {
optional_inline: 'opcional',
organization: 'Organización',
others: 'Otros',
passwordIsSet: null,
phone: 'Teléfono',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA utiliza <1><0>Apprise</0></1> para enviar notificaciones a más de 100 servicios populares.',
port: null,
preferences: 'Preferencias',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: presiona Ctrl-V (Cmd-V en Mac) para añadir adjuntos desde el portapapeles.',
@@ -256,6 +261,7 @@ export default {
projectNotFound_title: 'Proyecto no encontrado',
projectOwner: 'Propietario del proyecto',
referenceDataAndKnowledgeStorage: 'Datos de referencia y almacenamiento de conocimiento',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: null,
removeMember_title: 'Remover Miembro',
role: 'Rol',
@@ -282,6 +288,7 @@ export default {
shared: 'Compartido',
sharedWithMe_title: 'Compartido conmigo',
showOnFrontOfCard: 'Mostrar en el frente de la tarjeta',
smtp: null,
sortList_title: 'Ordenar',
stopwatch: 'Temporizador',
story: 'Historia',
@@ -293,6 +300,7 @@ export default {
taskList_title: 'Lista de tareas',
team: 'Equipo',
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment:
'No hay vista previa disponible para este adjunto.',
time: 'Tiempo',
@@ -307,6 +315,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Imágenes subidas',
url: null,
useSecureConnection: null,
userActions_title: 'Acciones de Usuario',
userAddedCardToList: '<0>{{user}}</0> añadió <2>{{card}}</2> a {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> añadido a esta tarjeta en {{list}}',
@@ -446,6 +455,7 @@ export default {
restoreToList: 'Restaurar a {{list}}',
returnToBoard: 'Volver al tablero',
save: 'Guardar',
sendTestEmail: null,
showActive: 'Mostrar activos',
showAllAttachments: 'Mostrar todos los adjuntos ({{hidden}} ocultos)',
showCardsWithThisUser: 'Mostrar tarjetas con este usuario',

View File

@@ -115,6 +115,7 @@ export default {
'Kaardid sellel nimekirjal on täidetud ja valmis arhiveerimiseks.',
cardsOnThisListAreReadyToBeWorkedOn: 'Kaardid sellel nimekirjal on valmis tööle.',
clickHereOrRefreshPageToUpdate: '<0>Klõpsa siia</0> või uuendage lehte.',
clientHostnameInEhlo: null,
closed: 'Suletud',
color: 'Värv',
comments: 'Kommentaarid',
@@ -137,6 +138,7 @@ export default {
date: 'Kuupäev',
deactivateUser_title: 'Deaktiveeri kasutaja',
defaultCardType_title: 'Vaikimisi kaardi tüüp',
defaultFrom: null,
defaultView_title: 'Vaikimisi vaade',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Kustuta kõik tahvlid, et seda projekti kustutada',
@@ -201,6 +203,7 @@ export default {
grid: 'Grill',
hideCompletedTasks: null,
hideFromProjectListAndFavorites: 'Peida projektiloendist ja lemmikutest',
host: null,
hours: 'Tunnid',
importBoard_title: 'Impordi tahvel',
invalidCurrentPassword: 'Vale praegune parool',
@@ -244,9 +247,11 @@ export default {
optional_inline: 'valikuline',
organization: 'Organisatsioon',
others: 'Teised',
passwordIsSet: null,
phone: 'Telefon',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA kasutab <1><0>Apprise</0></1> teavitusteenuse, et teavitada üle 100 populaarset teenust.',
port: null,
preferences: 'Eelistused',
pressPasteShortcutToAddAttachmentFromClipboard:
"Näpunäide: vajutage Ctrl-V (Cmd-V Mac'il) manuse lisamiseks kleebist.",
@@ -255,6 +260,7 @@ export default {
projectNotFound_title: 'Projekt ei leitud',
projectOwner: 'Projekti omanik',
referenceDataAndKnowledgeStorage: 'Viideandmete ja teadmise salvestamiseks.',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Eemalda haldur',
removeMember_title: 'Eemalda liige',
role: 'Roll',
@@ -281,6 +287,7 @@ export default {
shared: 'Jagatud',
sharedWithMe_title: 'Jagatud minuga',
showOnFrontOfCard: 'Kuva kaardi ees',
smtp: null,
sortList_title: 'Nimekiri sorteerimine',
stopwatch: 'Stopper',
story: 'Kirjeldus',
@@ -292,6 +299,7 @@ export default {
taskList_title: 'Ülesanne nimekiri',
team: 'Töögrupp',
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'Selle manusi eelvaadet pole saadaval.',
time: 'Aeg',
title: 'Pealkiri',
@@ -305,6 +313,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Laaditud pildid',
url: null,
useSecureConnection: null,
userActions_title: 'Kasutaja tegevused',
userAddedCardToList: '<0>{{user}}</0> lisas <2>{{card}}</2> nimekirjaan {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> lisas selle kaardi nimekirjaan {{list}}',
@@ -445,6 +454,7 @@ export default {
restoreToList: 'Taasta nimekirja {{list}}',
returnToBoard: 'Tagasi tahvlile',
save: 'Salvesta',
sendTestEmail: null,
showActive: 'Näita aktiivseid',
showAllAttachments: 'Näita kõiki manuseid ({{hidden}} peidetud)',
showCardsWithThisUser: 'Näita selle kasutajaga kaarte',

View File

@@ -99,6 +99,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'رنگ',
comments: null,
@@ -121,6 +122,7 @@ export default {
date: 'تاریخ',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'حذف پیوست',
@@ -184,6 +186,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'ساعت‌ها',
importBoard_title: 'وارد کردن برد',
invalidCurrentPassword: 'رمز عبور فعلی نامعتبر است',
@@ -227,8 +230,10 @@ export default {
optional_inline: 'اختیاری',
organization: 'سازمان',
others: null,
passwordIsSet: null,
phone: 'تلفن',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'ترجیحات',
pressPasteShortcutToAddAttachmentFromClipboard:
'نکته: با فشردن Ctrl-V (Cmd-V در مک) می‌توانید پیوست را از کلیپ بورد اضافه کنید.',
@@ -237,6 +242,7 @@ export default {
projectNotFound_title: 'پروژه یافت نشد',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'حذف مدیر',
removeMember_title: 'حذف عضو',
role: null,
@@ -263,6 +269,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: 'مرتب‌سازی لیست',
stopwatch: 'کرنومتر',
story: null,
@@ -274,6 +281,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'پیش نمایشی برای این پیوست موجود نیست.',
time: 'زمان',
title: 'عنوان',
@@ -287,6 +295,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'اقدامات کاربر',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> این کارت را به {{list}} اضافه کرد',
@@ -424,6 +433,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'ذخیره',
sendTestEmail: null,
showActive: null,
showAllAttachments: 'نمایش همه پیوست‌ها ({{hidden}} مخفی)',
showCardsWithThisUser: null,

View File

@@ -111,6 +111,7 @@ export default {
'Tämän listan kortit ovat valmiita ja voidaan arkistoida.',
cardsOnThisListAreReadyToBeWorkedOn: 'Tämän listan kortit ovat valmiita työstettäväksi.',
clickHereOrRefreshPageToUpdate: '<0>Päivitä tästä</0> tai lataa sivu uudelleen.',
clientHostnameInEhlo: null,
closed: 'Suljettu',
color: 'Väri',
comments: 'Kommentit',
@@ -133,6 +134,7 @@ export default {
date: 'Päivämäärä',
deactivateUser_title: 'Poista käyttäjä käytöstä',
defaultCardType_title: 'Oletuskorttityyppi',
defaultFrom: null,
defaultView_title: 'Oletusnäkymä',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Poista kaikki taulut, jotta voit poistaa tämän projektin',
@@ -197,6 +199,7 @@ export default {
grid: 'Ruudukko',
hideCompletedTasks: null,
hideFromProjectListAndFavorites: 'Piilota projektilistasta ja suosikeista',
host: null,
hours: 'Tunnit',
importBoard_title: 'Tuo taulu',
invalidCurrentPassword: 'Virheellinen nykyinen salasana',
@@ -240,9 +243,11 @@ export default {
optional_inline: 'valinnainen',
organization: 'Organisaatio',
others: 'Muut',
passwordIsSet: null,
phone: 'Puhelin',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA käyttää <1><0>Apprise</0></1> lähettääkseen ilmoituksia yli 100 suosittuun palveluun.',
port: null,
preferences: 'Asetukset',
pressPasteShortcutToAddAttachmentFromClipboard:
'Vinkki: paina Ctrl-V (tai Cmd-V Macilla) lisätäksesi liitteen leikepöydältä.',
@@ -251,6 +256,7 @@ export default {
projectNotFound_title: 'Projektia ei löytynyt',
projectOwner: 'Projektin omistaja',
referenceDataAndKnowledgeStorage: 'Viitetiedot ja tietovarasto.',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Poista ylläpitäjä',
removeMember_title: 'Poista jäsen',
role: 'Rooli',
@@ -277,6 +283,7 @@ export default {
shared: 'Jaettu',
sharedWithMe_title: 'Jaettu kanssani',
showOnFrontOfCard: 'Näytä kortin etupuolella',
smtp: null,
sortList_title: 'Lajittele lista',
stopwatch: 'Ajastin',
story: 'Tarina',
@@ -288,6 +295,7 @@ export default {
taskList_title: 'Tehtävälista',
team: 'Tiimi',
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'Tälle liitteelle ei ole esikatselua saatavilla.',
time: 'Aika',
title: 'Otsikko',
@@ -301,6 +309,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Ladatut kuvat',
url: null,
useSecureConnection: null,
userActions_title: 'Käyttäjän toiminnot',
userAddedCardToList: '<0>{{user}}</0> lisäsi <2>{{card}}</2> listaan {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> lisäsi tämän kortin listaan {{list}}',
@@ -445,6 +454,7 @@ export default {
restoreToList: 'Palauta listaan {{list}}',
returnToBoard: 'Palaa tauluun',
save: 'Tallenna',
sendTestEmail: null,
showActive: 'Näytä aktiiviset',
showAllAttachments: 'Näytä kaikki liitteet ({{hidden}} piilotettu)',
showCardsWithThisUser: 'Näytä kortit, joissa tämä käyttäjä',

View File

@@ -118,6 +118,7 @@ export default {
cardsOnThisListAreReadyToBeWorkedOn: 'Les cartes de cette liste sont prêtes à être traitées.',
clickHereOrRefreshPageToUpdate:
'<0>Cliquez ici</0> ou rafraîchissez la page pour mettre à jour.',
clientHostnameInEhlo: null,
closed: 'Fermé',
color: 'Couleur',
comments: 'Commentaires',
@@ -141,6 +142,7 @@ export default {
date: 'Date',
deactivateUser_title: 'Désactiver lutilisateur',
defaultCardType_title: 'Type de carte par défaut',
defaultFrom: null,
defaultView_title: 'Vue par défaut',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Supprimer tous les tableaux pour pouvoir supprimer ce projet.',
@@ -205,6 +207,7 @@ export default {
grid: 'Grille',
hideCompletedTasks: null,
hideFromProjectListAndFavorites: 'Masquer de la liste des projets et des favoris',
host: null,
hours: 'Heures',
importBoard_title: 'Importer un tableau',
invalidCurrentPassword: 'Mot de passe actuel invalide',
@@ -248,9 +251,11 @@ export default {
optional_inline: 'optionnel',
organization: 'Organisation',
others: 'Autres',
passwordIsSet: null,
phone: 'Téléphone',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA utilise <1><0>Apprise</0></1> pour envoyer des notifications vers plus de 100 services populaires.',
port: null,
preferences: 'Préférences',
pressPasteShortcutToAddAttachmentFromClipboard:
'Conseil: appuyer sur Ctrl-V (Cmd-V sur Mac) pour ajouter une pièce jointe depuis le presse-papiers',
@@ -259,6 +264,7 @@ export default {
projectNotFound_title: 'Projet introuvable',
projectOwner: 'Propriétaire de projet',
referenceDataAndKnowledgeStorage: 'Stockage de données de référence et de connaissances.',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Supprimer le responsable',
removeMember_title: 'Supprimer le membre',
role: 'Rôle',
@@ -285,6 +291,7 @@ export default {
shared: 'Partagé',
sharedWithMe_title: 'Partagé avec moi',
showOnFrontOfCard: 'Afficher sur le devant de la carte',
smtp: null,
sortList_title: 'Trier la liste',
stopwatch: 'Minuteur',
story: 'Story',
@@ -296,6 +303,7 @@ export default {
taskList_title: 'Liste de tâches',
team: "Mes projets d'équipe",
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment:
"Il n'y a pas d'aperçu disponible pour cette pièce jointe.",
time: 'Temps',
@@ -310,6 +318,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Images téléchargées',
url: 'URL',
useSecureConnection: null,
userActions_title: "Actions de l'utilisateur",
userAddedCardToList: '<0>{{user}}</0> a ajouté <2>{{card}}</2> à {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> a ajouté cette carte à {{list}}',
@@ -449,6 +458,7 @@ export default {
restoreToList: 'Restauré dans {{list}}',
returnToBoard: 'Retourner au tableau',
save: 'Sauvegarder',
sendTestEmail: null,
showActive: 'Voir les actifs',
showAllAttachments: 'Afficher toutes les pièces jointes ({{hidden}} masquées)',
showCardsWithThisUser: 'Voir les cartes avec cet utilisateur',

View File

@@ -108,6 +108,7 @@ export default {
cardsOnThisListAreReadyToBeWorkedOn: 'A listán lévő kártyák készen állnak a munkára.',
clickHereOrRefreshPageToUpdate:
'<0>Kattintson ide</0> vagy frissítse az oldalt a frissítéshez.',
clientHostnameInEhlo: null,
closed: 'Lezárt',
color: 'Szín',
comments: 'Megjegyzések',
@@ -130,6 +131,7 @@ export default {
date: 'Dátum',
deactivateUser_title: 'Felhasználó inaktiválása',
defaultCardType_title: 'Alapértelmezett kártyatípus',
defaultFrom: null,
defaultView_title: 'Alapértelmezett nézet',
deleteAllBoardsToBeAbleToDeleteThisProject:
'A projekt törléséhez törölni kell az összes táblát.',
@@ -194,6 +196,7 @@ export default {
grid: 'Rács',
hideCompletedTasks: 'Befejezett feladatok elrejtése',
hideFromProjectListAndFavorites: 'Elrejtés a projektlistából és a kedvencekből',
host: null,
hours: 'Órák',
importBoard_title: 'Tábla importálása',
invalidCurrentPassword: 'Érvénytelen jelenlegi jelszó',
@@ -238,9 +241,11 @@ export default {
optional_inline: 'opcionális',
organization: 'Szervezet',
others: 'Egyebek',
passwordIsSet: null,
phone: 'Telefon',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'A PLANKA az Apprise szolgáltatást használja több mint 100 népszerű szolgáltatás értesítéseinek küldésére.',
port: null,
preferences: 'Beállítások',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tipp: nyomja meg a Ctrl-V (Cmd-V a Mac-en) billentyűkombinációt a vágólapról történő melléklet hozzáadásához.',
@@ -249,6 +254,7 @@ export default {
projectNotFound_title: 'Projekt nem található',
projectOwner: 'Projekt tulajdonos',
referenceDataAndKnowledgeStorage: 'Referenciaadatok és tudástár.',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Menedzser eltávolítása',
removeMember_title: 'Tag eltávolítása',
role: 'Szerepkör',
@@ -275,6 +281,7 @@ export default {
shared: 'Megosztott',
sharedWithMe_title: 'Velem megosztva',
showOnFrontOfCard: 'Megjelenítés a kártya borítóján',
smtp: null,
sortList_title: 'Rendezés listában',
stopwatch: 'Stopper',
story: 'Story',
@@ -286,6 +293,7 @@ export default {
taskList_title: 'Feladatlista',
team: 'Csapat',
terms: 'Felhasználási feltételek',
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'Nincs elérhető előnézet ehhez a melléklethez.',
time: 'Idő',
title: 'Cím',
@@ -299,6 +307,7 @@ export default {
uploadFailedNotEnoughStorageSpace: 'Feltöltési hiba: nincs elég szabad tárhely',
uploadedImages: 'Feltöltött képek',
url: 'URL',
useSecureConnection: null,
userActions_title: 'Felhasználói műveletek',
userAddedCardToList:
'<0>{{user}}</0> hozzáadta a(z) <2>{{card}}</2> kártyát ehhez a listához: {{list}}',
@@ -446,6 +455,7 @@ export default {
restoreToList: 'Visszaállítás ide: {{list}}',
returnToBoard: 'Vissza a táblához',
save: 'Mentés',
sendTestEmail: null,
showActive: 'Aktívak megjelenítése',
showAllAttachments: 'Összes melléklet megjelenítése ({{hidden}} rejtve)',
showCardsWithThisUser: 'Kártyák megjelenítése ezzel a felhasználóval',

View File

@@ -101,6 +101,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'Warna',
comments: null,
@@ -123,6 +124,7 @@ export default {
date: 'Tanggal',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'Hapus Lampiran',
@@ -186,6 +188,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'Jam',
importBoard_title: 'Impor Papan',
invalidCurrentPassword: 'Kata sandi saat ini tidak valid',
@@ -229,8 +232,10 @@ export default {
optional_inline: 'opsional',
organization: 'Organisasi',
others: null,
passwordIsSet: null,
phone: 'Ponsel',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'Preferensi',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: tekan Ctrl-V (Cmd-V di Mac) untuk menambahkan lampiran dari papan klip.',
@@ -239,6 +244,7 @@ export default {
projectNotFound_title: 'Proyek Tidak Ditemukan',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Hapus Manager',
removeMember_title: 'Hapus Anggota',
role: null,
@@ -265,6 +271,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: null,
stopwatch: 'Stopwatch',
story: null,
@@ -276,6 +283,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment:
'Tidak ada pratinjau yang tersedia untuk lampiran ini.',
time: 'Waktu',
@@ -290,6 +298,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'Aksi Pengguna',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> menambahkan kartu ini ke {{list}}',
@@ -426,6 +435,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'Simpan',
sendTestEmail: null,
showActive: null,
showAllAttachments: 'Tampilkan semua lampiran ({{hidden}} tersembunyi)',
showCardsWithThisUser: null,

View File

@@ -116,6 +116,7 @@ export default {
cardsOnThisListAreReadyToBeWorkedOn:
'Le schede in questa lista sono pronte per essere lavorate.',
clickHereOrRefreshPageToUpdate: '<0>Clicca qui</0> o ricarica la pagina per aggiornare.',
clientHostnameInEhlo: null,
closed: 'Chiuso',
color: 'Colore',
comments: 'Commenti',
@@ -139,6 +140,7 @@ export default {
date: 'Data',
deactivateUser_title: 'Disattiva utente',
defaultCardType_title: 'Tipo di scheda predefinito',
defaultFrom: null,
defaultView_title: 'Vista predefinita',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Elimina tutte le bacheche per poter eliminare questo progetto.',
@@ -203,6 +205,7 @@ export default {
grid: 'Griglia',
hideCompletedTasks: 'Nascondi task completate',
hideFromProjectListAndFavorites: 'Nascondi dalla lista dei progetti e dai preferiti',
host: null,
hours: 'Ore',
importBoard_title: 'Importa board',
invalidCurrentPassword: 'Password corrente non valida',
@@ -247,9 +250,11 @@ export default {
optional_inline: 'opzionale',
organization: 'Organizazzione',
others: 'Altri',
passwordIsSet: null,
phone: 'Telefono',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA utilizza <1><0>Apprise</0></1> per inviare notifiche a oltre 100 servizi popolari.',
port: null,
preferences: 'Preferenze',
pressPasteShortcutToAddAttachmentFromClipboard:
'Consiglio: premi Ctrl-V (Cmd-V on Mac) per aggiungere un allegato dalla clipboard.',
@@ -258,6 +263,7 @@ export default {
projectNotFound_title: 'Progetto non trovato',
projectOwner: 'Proprietario del progetto',
referenceDataAndKnowledgeStorage: 'Dati di riferimento e di archiviazione',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Rimuovi manager',
removeMember_title: 'Rimuovi membro',
role: 'Ruolo',
@@ -284,6 +290,7 @@ export default {
shared: 'Condiviso',
sharedWithMe_title: 'Condiviso con me',
showOnFrontOfCard: 'Mostra davanti alla scheda',
smtp: null,
sortList_title: 'Ordina',
stopwatch: 'Timer',
story: 'Storia',
@@ -295,6 +302,7 @@ export default {
taskList_title: 'Lista di task',
team: 'Team',
terms: 'Ho letto e accetto i termini e condizioni.',
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment:
'Non è disponibile alcuna anteprima per questo allegato.',
time: 'Tempo',
@@ -310,6 +318,7 @@ export default {
'Caricamento fallito: spazio di archiviazione insufficiente.',
uploadedImages: 'Immagini caricate',
url: 'URL',
useSecureConnection: null,
userActions_title: 'Azioni utente',
userAddedCardToList: '<0>{{user}}</0> ha aggiunto <2>{{card}}</2> a {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> ha aggiunto questa task a {{list}}',
@@ -450,6 +459,7 @@ export default {
restoreToList: 'Ripristina a {{list}}',
returnToBoard: 'Torna alla bacheca',
save: 'Salva',
sendTestEmail: null,
showActive: 'Mostra attivi',
showAllAttachments: 'Mostra tutti gli allegati ({{hidden}} nascosti)',
showCardsWithThisUser: 'Mostra schede con questo utente',

View File

@@ -101,6 +101,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: '色',
comments: null,
@@ -123,6 +124,7 @@ export default {
date: '日付',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: '添付ファイルを削除',
@@ -186,6 +188,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: '時間',
importBoard_title: 'インポートボード',
invalidCurrentPassword: '現在のパスワードが無効',
@@ -229,8 +232,10 @@ export default {
optional_inline: '任意',
organization: '組織',
others: null,
passwordIsSet: null,
phone: '電話番号',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: '環境設定',
pressPasteShortcutToAddAttachmentFromClipboard:
'ヒント: Ctrl-V(MacではCmd-V)を押して、クリップボードから添付ファイルを追加します。',
@@ -239,6 +244,7 @@ export default {
projectNotFound_title: 'プロジェクトがありません',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'マネージャーを削除',
removeMember_title: 'メンバーを削除',
role: null,
@@ -265,6 +271,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: null,
stopwatch: 'タイマー',
story: null,
@@ -276,6 +283,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'この添付ファイルにはプレビューがありません。',
time: '時間',
title: 'タイトル',
@@ -289,6 +297,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'ユーザーのアクション',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> 様が {{list}} をこのカードに追加しました',
@@ -426,6 +435,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: '保存',
sendTestEmail: null,
showActive: null,
showAllAttachments: '全ての添付ファイルを表示する({{hidden}} 非表示)',
showCardsWithThisUser: null,

View File

@@ -99,6 +99,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: '색상',
comments: null,
@@ -121,6 +122,7 @@ export default {
date: '날짜',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: '첨부 파일 삭제',
@@ -184,6 +186,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: '시간',
importBoard_title: '보드 가져오기',
invalidCurrentPassword: '잘못된 현재 비밀번호',
@@ -227,8 +230,10 @@ export default {
optional_inline: '선택 사항',
organization: '조직',
others: null,
passwordIsSet: null,
phone: '전화',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: '환경 설정',
pressPasteShortcutToAddAttachmentFromClipboard:
'팁: Ctrl-V (Mac에서는 Cmd-V)를 눌러 클립보드에서 첨부 파일을 추가하세요.',
@@ -237,6 +242,7 @@ export default {
projectNotFound_title: '프로젝트를 찾을 수 없음',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: '관리자 제거',
removeMember_title: '멤버 제거',
role: null,
@@ -263,6 +269,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: '목록 정렬',
stopwatch: '스톱워치',
story: null,
@@ -274,6 +281,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment:
'이 첨부 파일에 대한 미리보기를 사용할 수 없습니다.',
time: '시간',
@@ -288,6 +296,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: '사용자 작업',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0>님이 이 카드를 {{list}}에 추가했습니다',
@@ -425,6 +434,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: '저장',
sendTestEmail: null,
showActive: null,
showAllAttachments: '모든 첨부 파일 보기 ({{hidden}} 숨김)',
showCardsWithThisUser: null,

View File

@@ -101,6 +101,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'Kleur',
comments: null,
@@ -123,6 +124,7 @@ export default {
date: 'Datum',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'Bijlage verwijderen',
@@ -186,6 +188,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'Uren',
importBoard_title: 'Bord importeren',
invalidCurrentPassword: 'Ongeldig huidig wachtwoord',
@@ -229,8 +232,10 @@ export default {
optional_inline: 'optioneel',
organization: 'Organisatie',
others: null,
passwordIsSet: null,
phone: 'Telefoon',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'Voorkeuren',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: druk op Ctrl-V (Cmd-V op Mac) om een bijlage van het klembord toe te voegen.',
@@ -239,6 +244,7 @@ export default {
projectNotFound_title: 'Project niet gevonden',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Manager verwijderen',
removeMember_title: 'Lid verwijderen',
role: null,
@@ -265,6 +271,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: null,
stopwatch: 'Stopwatch',
story: null,
@@ -276,6 +283,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment:
'Er is geen voorbeeld beschikbaar voor deze bijlage.',
time: 'Tijd',
@@ -290,6 +298,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'Gebruikersacties',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> heeft deze kaart toegevoegd aan {{list}}',
@@ -427,6 +436,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'Opslaan',
sendTestEmail: null,
showActive: null,
showAllAttachments: 'Alle bijlagen weergeven ({{hidden}} verbergen)',
showCardsWithThisUser: null,

View File

@@ -108,6 +108,7 @@ export default {
'Karty na tej liście są ukończone i gotowe do zarchiwizowania.',
cardsOnThisListAreReadyToBeWorkedOn: 'Karty na tej liście są gotowe do pracy nad nimi.',
clickHereOrRefreshPageToUpdate: '<0>Naciśnij tutaj</0> lub odśwież stronę, by zaktualizować.',
clientHostnameInEhlo: null,
closed: 'Zamknięte',
color: 'Kolor',
comments: 'Komentarze',
@@ -130,6 +131,7 @@ export default {
date: 'Data',
deactivateUser_title: 'Dezaktywuj Użytkownika',
defaultCardType_title: 'Domyślny Typ Karty',
defaultFrom: null,
defaultView_title: 'Domyślny Widok',
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'Usuń Załącznik',
@@ -193,6 +195,7 @@ export default {
grid: 'Siatka',
hideCompletedTasks: null,
hideFromProjectListAndFavorites: 'Ukryj z listy projektów i ulubionych',
host: null,
hours: 'Godzin',
importBoard_title: 'Importuj Tablicę',
invalidCurrentPassword: 'Błędne obecne hasło',
@@ -236,9 +239,11 @@ export default {
optional_inline: 'opcjonalny',
organization: 'Organizacja',
others: 'Inne',
passwordIsSet: null,
phone: 'Telefon',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA używa <1><0>Apprise</0></1> do wysyłania powiadomień do ponad 100 popularnych serwisów.',
port: null,
preferences: 'Preferencje',
pressPasteShortcutToAddAttachmentFromClipboard:
'Podpowiedź: naciśnij Ctrl-V (Cmd-V na Macu) aby dodać załącznik ze schowka.',
@@ -247,6 +252,7 @@ export default {
projectNotFound_title: 'Projektu Nie Znaleziono',
projectOwner: 'Właściciel projektu',
referenceDataAndKnowledgeStorage: 'Odnoś się do danych i przechowuj wiedzę',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Usuń Zarządcę',
removeMember_title: 'Usuń Członka',
role: 'Rola',
@@ -273,6 +279,7 @@ export default {
shared: 'Udostępniane',
sharedWithMe_title: 'Udostępniane Dla Mnie',
showOnFrontOfCard: 'Pokazuj na przodzie karty',
smtp: null,
sortList_title: 'Sortowanie Listy',
stopwatch: 'Stoper',
story: 'Scenorys',
@@ -284,6 +291,7 @@ export default {
taskList_title: 'Lista Zadań',
team: 'Zespół',
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'Brak podglądu dostępnego dla tego załącznika.',
time: 'Czas',
title: 'Tytuł',
@@ -297,6 +305,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Wgrane obrazy',
url: null,
useSecureConnection: null,
userActions_title: 'Akcje Użytkownika',
userAddedCardToList: '<0>{{user}}</0> dodał <2>{{card}}</2> do {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> dodał tę kartę do {{list}}',
@@ -437,6 +446,7 @@ export default {
restoreToList: 'Przywróć na {{list}}',
returnToBoard: 'Przywróć do tablicy',
save: 'Zapisz',
sendTestEmail: null,
showActive: 'Pokaż aktywne',
showAllAttachments: 'Pokaż wszystkie załączniki ({{hidden}} są ukryte)',
showCardsWithThisUser: 'Pokaż karty z tym użytkownikiem',

View File

@@ -116,6 +116,7 @@ export default {
cardsOnThisListAreReadyToBeWorkedOn:
'Os cartões nesta lista estão prontos para serem trabalhados.',
clickHereOrRefreshPageToUpdate: '<0>Clique aqui</0> ou atualize a página para atualizar.',
clientHostnameInEhlo: null,
closed: 'Fechado',
color: 'Cor',
comments: 'Comentários',
@@ -139,6 +140,7 @@ export default {
date: 'Data',
deactivateUser_title: 'Desativar Usuário',
defaultCardType_title: 'Tipo de Cartão Padrão',
defaultFrom: null,
defaultView_title: 'Visualização Padrão',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Excluir todos os quadros para poder excluir este projeto',
@@ -203,6 +205,7 @@ export default {
grid: 'Grade',
hideCompletedTasks: 'Ocultar tarefas concluídas',
hideFromProjectListAndFavorites: 'Ocultar da lista de projetos e favoritos',
host: null,
hours: 'Horas',
importBoard_title: 'Importar Quadro',
invalidCurrentPassword: 'Senha atual inválida',
@@ -247,9 +250,11 @@ export default {
optional_inline: 'opcional',
organization: 'Organização',
others: 'Outros',
passwordIsSet: null,
phone: 'Telefone',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA usa <1><0>Apprise</0></1> para enviar notificações para mais de 100 serviços populares.',
port: null,
preferences: 'Preferências',
pressPasteShortcutToAddAttachmentFromClipboard:
'Dica: pressione Ctrl-V (Cmd-V no Mac) para adicionar um anexo da área de transferência.',
@@ -258,6 +263,7 @@ export default {
projectNotFound_title: 'Projeto não encontrado',
projectOwner: 'Proprietário do projeto',
referenceDataAndKnowledgeStorage: 'Armazenamento de dados de referência e conhecimento.',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Remover Gerente',
removeMember_title: 'Remover Membro',
role: 'Função',
@@ -284,6 +290,7 @@ export default {
shared: 'Compartilhado',
sharedWithMe_title: 'Compartilhado Comigo',
showOnFrontOfCard: 'Mostrar na frente do cartão',
smtp: null,
sortList_title: 'Ordenar Lista',
stopwatch: 'Cronômetro',
story: 'História',
@@ -295,6 +302,7 @@ export default {
taskList_title: 'Lista de Tarefas',
team: 'Equipe',
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment:
'Não há pré-visualização disponível para este anexo.',
time: 'Tempo',
@@ -309,6 +317,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Imagens enviadas',
url: 'URL',
useSecureConnection: null,
userActions_title: 'Ações do Usuário',
userAddedCardToList: '<0>{{user}}</0> adicionou <2>{{card}}</2> à {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> adicionou este cartão a {{list}}',
@@ -449,6 +458,7 @@ export default {
restoreToList: 'Restaurar para {{list}}',
returnToBoard: 'Voltar ao quadro',
save: 'Salvar',
sendTestEmail: null,
showActive: 'Mostrar ativos',
showAllAttachments: 'Mostrar todos os anexos ({{hidden}} ocultos)',
showCardsWithThisUser: 'Mostrar cartões com este usuário',

View File

@@ -102,6 +102,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'Cor',
comments: null,
@@ -124,6 +125,7 @@ export default {
date: 'Data',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'Eliminar Anexo',
@@ -187,6 +189,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'Horas',
importBoard_title: 'Importar Quadro',
invalidCurrentPassword: 'Palavra-passe atual inválida',
@@ -230,8 +233,10 @@ export default {
optional_inline: 'opcional',
organization: 'Organização',
others: null,
passwordIsSet: null,
phone: 'Telefone',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'Preferências',
pressPasteShortcutToAddAttachmentFromClipboard:
'Dica: prima Ctrl-V (Cmd-V no Mac) para adicionar um anexo da área de transferência.',
@@ -240,6 +245,7 @@ export default {
projectNotFound_title: 'Projeto não encontrado',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Remover Gestor',
removeMember_title: 'Remover Membro',
role: null,
@@ -266,6 +272,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: null,
stopwatch: 'Cronómetro',
story: null,
@@ -277,6 +284,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment:
'Não há pré-visualização disponível para este anexo.',
time: 'Tempo',
@@ -291,6 +299,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'Ações do Utilizador',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> adicionou este cartão à {{list}}',
@@ -428,6 +437,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'Guardar',
sendTestEmail: null,
showActive: null,
showAllAttachments: 'Mostrar todos os anexos ({{hidden}} ocultos)',
showCardsWithThisUser: null,

View File

@@ -101,6 +101,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'Culoarea',
comments: null,
@@ -123,6 +124,7 @@ export default {
date: 'Data',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'Ștergeți atașamentul',
@@ -186,6 +188,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'Ore',
importBoard_title: 'Import Tabla',
invalidCurrentPassword: 'Parolă actuală nevalidă',
@@ -229,8 +232,10 @@ export default {
optional_inline: 'optional',
organization: 'Organizatia',
others: null,
passwordIsSet: null,
phone: 'Telefon',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'Preferințe',
pressPasteShortcutToAddAttachmentFromClipboard:
'Sfat: apăsați Ctrl-V (Cmd-V pe Mac) pentru a adăuga un atașament din clipboard.',
@@ -239,6 +244,7 @@ export default {
projectNotFound_title: 'Proiectul nu a fost găsit',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Eliminați Manager',
removeMember_title: 'Eliminați membru',
role: null,
@@ -265,6 +271,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: null,
stopwatch: 'Cronometru',
story: null,
@@ -276,6 +283,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment:
'Nu există nicio previzualizare disponibilă pentru acest atașament.',
time: 'Timp',
@@ -290,6 +298,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'Acțiunile utilizatorului',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> a adăugat acest card în {{list}}',
@@ -427,6 +436,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'Salveaza',
sendTestEmail: null,
showActive: null,
showAllAttachments: 'Afișați toate atașamentele ({{hidden}} ascunse)',
showCardsWithThisUser: null,

View File

@@ -114,6 +114,7 @@ export default {
'Карточки в этом списке завершены и готовы к архивированию.',
cardsOnThisListAreReadyToBeWorkedOn: 'Карточки в этом списке готовы к работе.',
clickHereOrRefreshPageToUpdate: '<0>Нажмите здесь</0> или обновите страницу для обновления.',
clientHostnameInEhlo: null,
closed: 'Закрыто',
color: 'Цвет',
comments: 'Комментарии',
@@ -136,6 +137,7 @@ export default {
date: 'Дата',
deactivateUser_title: 'Деактивировать пользователя',
defaultCardType_title: 'Тип карточки по умолчанию',
defaultFrom: null,
defaultView_title: 'Вид по умолчанию',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Удалите все доски, чтобы иметь возможность удалить этот проект',
@@ -200,6 +202,7 @@ export default {
grid: 'Сетка',
hideCompletedTasks: null,
hideFromProjectListAndFavorites: 'Скрыть из списка проектов и избранного',
host: null,
hours: 'Часы',
importBoard_title: 'Импорт доски',
invalidCurrentPassword: 'Неверный текущий пароль',
@@ -243,9 +246,11 @@ export default {
optional_inline: 'необязательно',
organization: 'Организация',
others: 'Другие',
passwordIsSet: null,
phone: 'Телефон',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA использует <1><0>Apprise</0></1> для отправки уведомлений в более чем 100 популярных сервисов.',
port: null,
preferences: 'Предпочтения',
pressPasteShortcutToAddAttachmentFromClipboard:
'Совет: нажмите Ctrl-V (Cmd-V на Mac), чтобы добавить вложение из буфера обмена.',
@@ -254,6 +259,7 @@ export default {
projectNotFound_title: 'Проект не найден',
projectOwner: 'Владелец проекта',
referenceDataAndKnowledgeStorage: 'Хранение справочных данных и знаний',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Удалить менеджера',
removeMember_title: 'Удаление участника',
role: 'Роль',
@@ -280,6 +286,7 @@ export default {
shared: 'Общий',
sharedWithMe_title: 'Общий со мной',
showOnFrontOfCard: 'Показать на лицевой стороне карточки',
smtp: null,
sortList_title: 'Сортировка списка',
stopwatch: 'Секундомер',
story: 'История',
@@ -291,6 +298,7 @@ export default {
taskList_title: 'Список задач',
team: 'Команда',
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'Предпросмотр для этого вложения недоступен.',
time: 'Время',
title: 'Название',
@@ -304,6 +312,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Загруженные изображения',
url: null,
useSecureConnection: null,
userActions_title: 'Действия с пользователем',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> добавил(а) эту карточку в {{list}}',
@@ -441,6 +450,7 @@ export default {
restoreToList: 'Восстановить в {{list}}',
returnToBoard: 'Вернуться на доску',
save: 'Сохранить',
sendTestEmail: null,
showActive: 'Показать активные',
showAllAttachments: 'Показать все вложения ({{hidden}} скрыто)',
showCardsWithThisUser: 'Показать карточки с этим пользователем',

View File

@@ -101,6 +101,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'Farba',
comments: null,
@@ -123,6 +124,7 @@ export default {
date: 'Dátum',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'Zmazať prílohu',
@@ -186,6 +188,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'Hodiny',
importBoard_title: null,
invalidCurrentPassword: 'Neplatné aktuálne heslo',
@@ -229,8 +232,10 @@ export default {
optional_inline: 'voliteľné',
organization: 'Spoločnosť',
others: null,
passwordIsSet: null,
phone: 'Telefón',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'Voľby',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tip: stlačte Ctrl-V (Cmd-V na Mac) pre vloženie prílohy zo schránky.',
@@ -239,6 +244,7 @@ export default {
projectNotFound_title: 'Projekt neexistuje',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Odstrániť správcu',
removeMember_title: 'Odstrániť člena',
role: null,
@@ -265,6 +271,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: null,
stopwatch: 'Časovač',
story: null,
@@ -276,6 +283,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: null,
time: 'Čas',
title: 'Názov',
@@ -289,6 +297,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'Akcie na používateľovi',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> pridal kartu do {{list}}',
@@ -426,6 +435,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'Uložiť',
sendTestEmail: null,
showActive: null,
showAllAttachments: 'Zozbraziť všetky prílohy ({{hidden}} skryté)',
showCardsWithThisUser: null,

View File

@@ -101,6 +101,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'Боја',
comments: null,
@@ -123,6 +124,7 @@ export default {
date: 'Датум',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'Обриши прилог',
@@ -186,6 +188,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'Сати',
importBoard_title: 'Увези таблу',
invalidCurrentPassword: 'Неисправна тренутна лозинка',
@@ -229,8 +232,10 @@ export default {
optional_inline: 'опционо',
organization: 'Организација',
others: null,
passwordIsSet: null,
phone: 'Телефон',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'Својства',
pressPasteShortcutToAddAttachmentFromClipboard:
'Савет: притисни Ctrl-V (Cmd-V на Меку) да би додао прилог са бележнице.',
@@ -239,6 +244,7 @@ export default {
projectNotFound_title: 'Пројекат није пронађен',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Уклони руководиоца',
removeMember_title: 'Уклони члана',
role: null,
@@ -265,6 +271,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: 'Сложи списак',
stopwatch: 'Штоперица',
story: null,
@@ -276,6 +283,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'Нема прегледа доступног за овај прилог.',
time: 'Време',
title: 'Наслов',
@@ -289,6 +297,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'Корисничке радње',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> је додао ову картицу на {{list}}',
@@ -426,6 +435,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'Сачувај',
sendTestEmail: null,
showActive: null,
showAllAttachments: 'Прикажи све ({{hidden}} сакривене прилоге)',
showCardsWithThisUser: null,

View File

@@ -98,6 +98,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'Boja',
comments: null,
@@ -120,6 +121,7 @@ export default {
date: 'Datum',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'Obriši prilog',
@@ -183,6 +185,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'Sati',
importBoard_title: 'Uvezi tablu',
invalidCurrentPassword: 'Neispravna trenutna lozinka',
@@ -226,8 +229,10 @@ export default {
optional_inline: 'opciono',
organization: 'Organizacija',
others: null,
passwordIsSet: null,
phone: 'Telefon',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'Svojstva',
pressPasteShortcutToAddAttachmentFromClipboard:
'Savet: pritisni Ctrl-V (Cmd-V na Meku) da bi dodao prilog sa beležnice.',
@@ -236,6 +241,7 @@ export default {
projectNotFound_title: 'Projekat nije pronađen',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Ukloni rukovodioca',
removeMember_title: 'Ukloni člana',
role: null,
@@ -262,6 +268,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: 'Složi spisak',
stopwatch: 'Štoperica',
story: null,
@@ -273,6 +280,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'Nema pregleda dostupnog za ovaj prilog.',
time: 'Vreme',
title: 'Naslov',
@@ -286,6 +294,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'Korisničke radnje',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> je dodao ovu karticu na {{list}}',
@@ -423,6 +432,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'Sačuvaj',
sendTestEmail: null,
showActive: null,
showAllAttachments: 'Prikaži sve ({{hidden}} sakrivene priloge)',
showCardsWithThisUser: null,

View File

@@ -100,6 +100,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'Färg',
comments: null,
@@ -122,6 +123,7 @@ export default {
date: 'Datum',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'Ta bort bilaga',
@@ -185,6 +187,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'Timmar',
importBoard_title: null,
invalidCurrentPassword: 'Ogiltigt nuvarande lösenord',
@@ -228,8 +231,10 @@ export default {
optional_inline: 'valfri',
organization: 'Organisation',
others: null,
passwordIsSet: null,
phone: 'Telefon',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'Preferenser',
pressPasteShortcutToAddAttachmentFromClipboard:
'Tips: tryck på Ctrl-V (Cmd-V på Mac) för att lägga till en bilaga från urklipp.',
@@ -238,6 +243,7 @@ export default {
projectNotFound_title: 'Projekt hittades inte',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Ta bort projektledare',
removeMember_title: 'Ta bort medlem',
role: null,
@@ -264,6 +270,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: null,
stopwatch: 'Timer',
story: null,
@@ -275,6 +282,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: null,
time: 'Tid',
title: 'Titel',
@@ -288,6 +296,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'Användaråtgärder',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> lade till detta kort i {{list}}',
@@ -425,6 +434,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'Spara',
sendTestEmail: null,
showActive: null,
showAllAttachments: 'Visa alla bilagor ({{hidden}} dolda)',
showCardsWithThisUser: null,

View File

@@ -98,6 +98,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'renk',
comments: null,
@@ -120,6 +121,7 @@ export default {
date: 'tarih',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: 'Eki Sil',
@@ -183,6 +185,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'saat',
importBoard_title: null,
invalidCurrentPassword: 'Mevcut şifre yanlış',
@@ -226,8 +229,10 @@ export default {
optional_inline: 'İsteğe bağlı',
organization: 'Organizasyon',
others: null,
passwordIsSet: null,
phone: 'telefon',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'Tercihler',
pressPasteShortcutToAddAttachmentFromClipboard:
'İpucu: Panodan bir ek eklemek için CTRL-V ye (Macte Cmd-V) basın.',
@@ -236,6 +241,7 @@ export default {
projectNotFound_title: 'Proje bulunamadı',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Yöneticiyi Kaldır',
removeMember_title: 'Üyeyi Kaldır',
role: null,
@@ -262,6 +268,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: null,
stopwatch: 'kronometre',
story: null,
@@ -273,6 +280,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'Bu ek için önizleme mevcut değil.',
time: 'zaman',
title: 'başlık',
@@ -286,6 +294,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'Kullanıcı İşlemleri',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> bu kartı {{list}} listesine ekledi',
@@ -423,6 +432,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'Kaydet',
sendTestEmail: null,
showActive: null,
showAllAttachments: 'Tüm ekleri göster ({{hidden}} gizli)',
showCardsWithThisUser: null,

View File

@@ -111,6 +111,7 @@ export default {
'Картки з цього списку завершені і готові до архівування.',
cardsOnThisListAreReadyToBeWorkedOn: 'Картки з цього списку готові до роботи.',
clickHereOrRefreshPageToUpdate: '<0>Натисніть тут</0> або оновіть сторінку для оновлення.',
clientHostnameInEhlo: null,
closed: 'Закрито',
color: 'Колір',
comments: 'Коментарі',
@@ -134,6 +135,7 @@ export default {
date: 'Дата',
deactivateUser_title: 'Деактивувати користувача',
defaultCardType_title: 'Тип картки за замовчуванням',
defaultFrom: null,
defaultView_title: 'Вигляд за замовчуванням',
deleteAllBoardsToBeAbleToDeleteThisProject:
'Видаліть усі дошки, щоб мати змогу видалити цей проект',
@@ -198,6 +200,7 @@ export default {
grid: 'Сітка',
hideCompletedTasks: 'Приховати виконані завдання',
hideFromProjectListAndFavorites: 'Приховати зі списку проектів та обраного',
host: null,
hours: 'Години',
importBoard_title: 'Імпортувати Дошку',
invalidCurrentPassword: 'Невірний поточний пароль',
@@ -242,9 +245,11 @@ export default {
optional_inline: 'опціонально',
organization: 'Організація',
others: 'Інші',
passwordIsSet: null,
phone: 'Телефон',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA використовує <1><0>Apprise</0></1> для надсилання сповіщень на понад 100 популярних сервісів.',
port: null,
preferences: 'Уподобання',
pressPasteShortcutToAddAttachmentFromClipboard:
'Порада: натисніть Ctrl-V (⌘V на Mac), щоб додати вкладення з буфера обміну.',
@@ -253,6 +258,7 @@ export default {
projectNotFound_title: 'Проект не знайдено',
projectOwner: 'Власник проекту',
referenceDataAndKnowledgeStorage: 'Довідкові дані та сховище знань.',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: 'Видалити Менеджера',
removeMember_title: 'Видалити Учасника',
role: 'Роль',
@@ -279,6 +285,7 @@ export default {
shared: 'Спільне',
sharedWithMe_title: 'Поділіться зі мною',
showOnFrontOfCard: 'Показати на лицьовій стороні картки',
smtp: null,
sortList_title: 'Сортування списку',
stopwatch: 'Секундомір',
story: 'Історія',
@@ -290,6 +297,7 @@ export default {
taskList_title: 'Список завдань',
team: 'Команда',
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: 'Для цього вкладення немає доступного перегляду.',
time: 'Час',
title: 'Назва',
@@ -303,6 +311,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Завантажені зображення',
url: 'Посилання',
useSecureConnection: null,
userActions_title: 'Дії користувача',
userAddedCardToList: '<0>{{user}}</0> додав(ла) <2>{{card}}</2> до {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> додав(ла) цю картку до {{list}}',
@@ -443,6 +452,7 @@ export default {
restoreToList: 'Відновити до {{list}}',
returnToBoard: 'Повернутися до дошки',
save: 'Зберегти',
sendTestEmail: null,
showActive: 'Показати активний',
showAllAttachments: 'Показати всі вкладення ({{hidden}} приховані)',
showCardsWithThisUser: 'Показати картки з цим користувачем',

View File

@@ -97,6 +97,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: 'Rang',
comments: null,
@@ -119,6 +120,7 @@ export default {
date: 'Sana',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: "Ilovani O'chirish",
@@ -182,6 +184,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: 'Soat',
importBoard_title: null,
invalidCurrentPassword: 'Hozirgi parol xato',
@@ -225,8 +228,10 @@ export default {
optional_inline: 'ixtiyoriy',
organization: 'Tashkilot',
others: null,
passwordIsSet: null,
phone: 'Telefon',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: 'Afzalliklar',
pressPasteShortcutToAddAttachmentFromClipboard:
"Tip: Buferdan ilova qo'shish uchun Ctrl-V (Mac da Cmd-V) ni bosing.",
@@ -235,6 +240,7 @@ export default {
projectNotFound_title: 'Loyiha Topilmadi',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: "Boshqaruvchini O'chirish",
removeMember_title: "A'zoni O'chirish",
role: null,
@@ -261,6 +267,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: null,
stopwatch: 'Taymer',
story: null,
@@ -272,6 +279,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: null,
time: 'Vaqt',
title: 'Sarlavha',
@@ -285,6 +293,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: 'Foydalanuvchi Amallari',
userAddedCardToList: null,
userAddedThisCardToList: "Ushbu kartani {{list}} ga<0>{{user}}</0> qo'shdi",
@@ -422,6 +431,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: 'Saqlash',
sendTestEmail: null,
showActive: null,
showAllAttachments: "Barcha ilovalarni ko'rsatish ({{hidden}} yashirilgan)",
showCardsWithThisUser: null,

View File

@@ -98,6 +98,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: '此列表中的卡片已完成并准备归档',
cardsOnThisListAreReadyToBeWorkedOn: '此列表中的卡片已准备就绪可开始工作',
clickHereOrRefreshPageToUpdate: '<0>点击此处</0>或刷新页面更新',
clientHostnameInEhlo: null,
closed: '已关闭',
color: '颜色',
comments: '评论',
@@ -120,6 +121,7 @@ export default {
date: '日期',
deactivateUser_title: '停用用户',
defaultCardType_title: '默认卡片类型',
defaultFrom: null,
defaultView_title: '默认视图',
deleteAllBoardsToBeAbleToDeleteThisProject: '删除所有面板后方可删除此项目',
deleteAttachment_title: '删除附件',
@@ -183,6 +185,7 @@ export default {
grid: '网格',
hideCompletedTasks: null,
hideFromProjectListAndFavorites: '从项目列表和收藏中隐藏',
host: null,
hours: '小时',
importBoard_title: '导入面板',
invalidCurrentPassword: '当前密码错误',
@@ -226,9 +229,11 @@ export default {
optional_inline: '可选的',
organization: '组织机构',
others: '其他',
passwordIsSet: null,
phone: '电话',
plankaUsesAppriseToSendNotificationsToOver100PopularServices:
'PLANKA使用<1><0>Apprise</0></1>向100多个流行服务发送通知',
port: null,
preferences: '偏好',
pressPasteShortcutToAddAttachmentFromClipboard:
'提示: 按下 Ctrl-V (Mac: Cmd-V) 从剪切板添加附件',
@@ -237,6 +242,7 @@ export default {
projectNotFound_title: '项目未找到',
projectOwner: '项目所有者',
referenceDataAndKnowledgeStorage: '参考数据和知识存储',
rejectUnauthorizedTlsCertificates: null,
removeManager_title: '删除管理员',
removeMember_title: '删除成员',
role: '角色',
@@ -263,6 +269,7 @@ export default {
shared: '共享',
sharedWithMe_title: '与我共享',
showOnFrontOfCard: '在卡片正面显示',
smtp: null,
sortList_title: '排序列表',
stopwatch: '计时器',
story: '故事',
@@ -274,6 +281,7 @@ export default {
taskList_title: '任务列表',
team: '团队',
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: '此附件无法预览',
time: '时间',
title: '标题',
@@ -287,6 +295,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: '已上传图片',
url: '网址',
useSecureConnection: null,
userActions_title: '用户操作',
userAddedCardToList: '<0>{{user}}</0> 将 <2>{{card}}</2> 添加到 {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> 向列表 {{list}} 添加了该卡片',
@@ -424,6 +433,7 @@ export default {
restoreToList: '恢复到 {{list}}',
returnToBoard: '返回面板',
save: '保存',
sendTestEmail: null,
showActive: '显示活跃',
showAllAttachments: '显示所有的附件 ({{hidden}} 隐藏)',
showCardsWithThisUser: '显示包含此用户的卡片',

View File

@@ -95,6 +95,7 @@ export default {
cardsOnThisListAreCompleteAndReadyToBeArchived: null,
cardsOnThisListAreReadyToBeWorkedOn: null,
clickHereOrRefreshPageToUpdate: null,
clientHostnameInEhlo: null,
closed: null,
color: '顏色',
comments: null,
@@ -117,6 +118,7 @@ export default {
date: '日期',
deactivateUser_title: null,
defaultCardType_title: null,
defaultFrom: null,
defaultView_title: null,
deleteAllBoardsToBeAbleToDeleteThisProject: null,
deleteAttachment_title: '刪除附件',
@@ -180,6 +182,7 @@ export default {
grid: null,
hideCompletedTasks: null,
hideFromProjectListAndFavorites: null,
host: null,
hours: '小時',
importBoard_title: '導入看板',
invalidCurrentPassword: '當前密碼錯誤',
@@ -223,8 +226,10 @@ export default {
optional_inline: '可選的',
organization: '組織機構',
others: null,
passwordIsSet: null,
phone: '電話',
plankaUsesAppriseToSendNotificationsToOver100PopularServices: null,
port: null,
preferences: '偏好設定',
pressPasteShortcutToAddAttachmentFromClipboard:
'提示: 按下 Ctrl-V (Mac: Cmd-V) 從剪貼簿添加附件',
@@ -233,6 +238,7 @@ export default {
projectNotFound_title: '專案未找到',
projectOwner: null,
referenceDataAndKnowledgeStorage: null,
rejectUnauthorizedTlsCertificates: null,
removeManager_title: '刪除管理員',
removeMember_title: '刪除成員',
role: null,
@@ -259,6 +265,7 @@ export default {
shared: null,
sharedWithMe_title: null,
showOnFrontOfCard: null,
smtp: null,
sortList_title: null,
stopwatch: '碼表',
story: null,
@@ -270,6 +277,7 @@ export default {
taskList_title: null,
team: null,
terms: null,
testLog_title: null,
thereIsNoPreviewAvailableForThisAttachment: '此附件無法預覽',
time: '時間',
title: '標題',
@@ -283,6 +291,7 @@ export default {
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
useSecureConnection: null,
userActions_title: '使用者操作',
userAddedCardToList: null,
userAddedThisCardToList: '<0>{{user}}</0> 向列表 {{list}} 添加了該卡片',
@@ -419,6 +428,7 @@ export default {
restoreToList: null,
returnToBoard: null,
save: '保存',
sendTestEmail: null,
showActive: null,
showAllAttachments: '顯示所有附件 ({{hidden}} 隱藏)',
showCardsWithThisUser: null,

View File

@@ -7,7 +7,7 @@ import ActionTypes from '../constants/ActionTypes';
const initialState = {
isInitializing: true,
config: null,
bootstrap: null,
};
// eslint-disable-next-line default-param-last
@@ -16,13 +16,13 @@ export default (state = initialState, { type, payload }) => {
case ActionTypes.SOCKET_RECONNECT_HANDLE:
return {
...state,
config: payload.config,
bootstrap: payload.bootstrap,
};
case ActionTypes.LOGIN_INITIALIZE:
return {
...state,
isInitializing: false,
config: payload.config,
bootstrap: payload.bootstrap,
};
case ActionTypes.AUTHENTICATE__SUCCESS:
case ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS:
@@ -36,16 +36,16 @@ export default (state = initialState, { type, payload }) => {
...state,
isInitializing: false,
};
case ActionTypes.CORE_INITIALIZE__CONFIG_FETCH:
case ActionTypes.CORE_INITIALIZE__BOOTSTRAP_FETCH:
return {
...state,
config: payload.config,
bootstrap: payload.bootstrap,
};
case ActionTypes.USER_UPDATE_HANDLE:
if (payload.config) {
if (payload.bootstrap) {
return {
...state,
config: payload.config,
bootstrap: payload.bootstrap,
};
}

View File

@@ -15,6 +15,7 @@ const initialState = {
isFavoritesEnabled: false,
isEditModeEnabled: false,
modal: null,
config: null,
boardId: null,
cardId: null,
recentCardId: null,
@@ -70,13 +71,30 @@ export default (state = initialState, { type, payload }) => {
...state,
isContentFetching: true,
};
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.SOCKET_RECONNECT_HANDLE:
case ActionTypes.USER_UPDATE_HANDLE:
if (payload.config) {
return {
...state,
config: payload.config,
};
}
return state;
case ActionTypes.CORE_INITIALIZE: {
const nextState = {
...state,
isFavoritesEnabled: payload.user.enableFavoritesByDefault,
homeView: payload.user.defaultHomeView,
projectsOrder: payload.user.defaultProjectsOrder,
};
if (payload.config) {
nextState.config = payload.config;
}
return nextState;
}
case ActionTypes.FAVORITES_TOGGLE:
return {
...state,
@@ -102,6 +120,27 @@ export default (state = initialState, { type, payload }) => {
...state,
modal: payload,
};
case ActionTypes.CONFIG_UPDATE:
return {
...state,
config: {
...state.config,
...payload.data,
},
};
case ActionTypes.CONFIG_UPDATE__SUCCESS:
return {
...state,
config: {
...state.config,
...payload.config,
},
};
case ActionTypes.CONFIG_UPDATE_HANDLE:
return {
...state,
config: payload.config,
};
case ActionTypes.PROJECTS_SEARCH:
return {
...state,

View File

@@ -8,9 +8,11 @@ import { combineReducers } from 'redux';
import authenticateForm from './authenticate-form';
import userCreateForm from './user-create-form';
import projectCreateForm from './project-create-form';
import smtpTest from './smtp-test';
export default combineReducers({
authenticateForm,
userCreateForm,
projectCreateForm,
smtpTest,
});

View File

@@ -0,0 +1,35 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import ActionTypes from '../../constants/ActionTypes';
const initialState = {
isLoading: false,
logs: null,
error: null,
};
// eslint-disable-next-line default-param-last
export default (state = initialState, { type, payload }) => {
switch (type) {
case ActionTypes.SMTP_CONFIG_TEST:
return {
...state,
isLoading: true,
};
case ActionTypes.SMTP_CONFIG_TEST__SUCCESS:
return {
...initialState,
logs: payload.logs,
};
case ActionTypes.SMTP_CONFIG_TEST__FAILURE:
return {
...initialState,
error: payload.error,
};
default:
return state;
}
};

View File

@@ -21,14 +21,14 @@ export default function* coreSaga() {
yield take(ActionTypes.LOGOUT);
const oidcConfig = yield select(selectors.selectOidcConfig);
const oidcBootstrap = yield select(selectors.selectOidcBootstrap);
if (oidcConfig && oidcConfig.endSessionUrl !== null) {
if (oidcBootstrap && oidcBootstrap.endSessionUrl !== null) {
const currentUser = yield select(selectors.selectCurrentUser);
if (!currentUser || currentUser.isSsoUser) {
// Redirect the user to the IDP to log out.
window.location.href = oidcConfig.endSessionUrl;
window.location.href = oidcBootstrap.endSessionUrl;
return;
}
}

View File

@@ -18,8 +18,11 @@ export function* fetchCore() {
included: { notificationServices: notificationServices1 },
} = yield call(request, api.getCurrentUser, true);
let config;
let webhooks;
if (user.role === UserRoles.ADMIN) {
({ item: config } = yield call(request, api.getConfig));
({ items: webhooks } = yield call(request, api.getWebhooks));
}
@@ -105,6 +108,7 @@ export function* fetchCore() {
}
return {
config,
user,
board,
webhooks,

View File

@@ -0,0 +1,49 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { call, put } from 'redux-saga/effects';
import request from '../request';
import actions from '../../../actions';
import api from '../../../api';
export function* updateConfig(data) {
yield put(actions.updateConfig(data));
let config;
try {
({ item: config } = yield call(request, api.updateConfig, data));
} catch (error) {
yield put(actions.updateConfig.failure(error));
return;
}
yield put(actions.updateConfig.success(config));
}
export function* handleConfigUpdate(config) {
yield put(actions.handleConfigUpdate(config));
}
export function* testSmtpConfig() {
yield put(actions.testSmtpConfig());
let logs;
try {
({
included: { logs },
} = yield call(request, api.testSmtpConfig));
} catch (error) {
yield put(actions.testSmtpConfig.failure(error));
}
yield put(actions.testSmtpConfig.success(logs));
}
export default {
updateConfig,
handleConfigUpdate,
testSmtpConfig,
};

View File

@@ -14,11 +14,12 @@ import i18n from '../../../i18n';
import { removeAccessToken } from '../../../utils/access-token-storage';
export function* initializeCore() {
const { item: config } = yield call(request, api.getConfig); // TODO: handle error
const { item: bootstrap } = yield call(request, api.getBootstrap); // TODO: handle error
yield put(actions.initializeCore.fetchConfig(config));
yield put(actions.initializeCore.fetchBootstrap(bootstrap));
const {
config,
user,
board,
webhooks,
@@ -49,6 +50,7 @@ export function* initializeCore() {
yield put(
actions.initializeCore(
config,
user,
board,
webhooks,

View File

@@ -7,6 +7,7 @@ import router from './router';
import socket from './socket';
import core from './core';
import modals from './modals';
import config from './config';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
@@ -34,6 +35,7 @@ export default {
...socket,
...core,
...modals,
...config,
...webhooks,
...users,
...projects,

View File

@@ -21,6 +21,7 @@ export function* handleSocketReconnect() {
yield put(actions.handleSocketReconnect.fetchCore(currentUserId, boardId));
let bootstrap;
let config;
let user;
let board;
@@ -47,7 +48,7 @@ export function* handleSocketReconnect() {
let notificationServices;
try {
({ item: config } = yield call(request, api.getConfig));
({ item: bootstrap } = yield call(request, api.getBootstrap));
({
user,
@@ -80,6 +81,7 @@ export function* handleSocketReconnect() {
yield put(
actions.handleSocketReconnect(
bootstrap,
config,
user,
board,

View File

@@ -68,6 +68,7 @@ export function* handleUserUpdate(user) {
const currentUser = yield select(selectors.selectCurrentUser);
const isCurrentUser = user.id === currentUser.id;
let bootstrap;
let config;
let board;
let webhooks;
@@ -102,6 +103,7 @@ export function* handleUserUpdate(user) {
({ items: users1 } = yield call(request, api.getUsers));
if (user.role === UserRoles.ADMIN) {
({ item: bootstrap } = yield call(request, api.getBootstrap));
({ item: config } = yield call(request, api.getConfig));
({ items: webhooks } = yield call(request, api.getWebhooks));
@@ -164,6 +166,7 @@ export function* handleUserUpdate(user) {
user,
projectIds,
boardIds,
bootstrap,
config,
board,
webhooks,
@@ -248,10 +251,10 @@ export function* updateUserPassword(id, data) {
yield put(actions.updateUserPassword(id, data));
let user;
let accessTokens;
let accessToken;
try {
({ item: user, included: { accessTokens } = {} } = yield call(
({ item: user, included: { accessToken } = {} } = yield call(
request,
api.updateUserPassword,
id,
@@ -262,8 +265,6 @@ export function* updateUserPassword(id, data) {
return;
}
const accessToken = accessTokens && accessTokens[0];
if (accessToken) {
yield call(setAccessToken, accessToken);
}

View File

@@ -0,0 +1,21 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { all, takeEvery } from 'redux-saga/effects';
import services from '../services';
import EntryActionTypes from '../../../constants/EntryActionTypes';
export default function* configWatchers() {
yield all([
takeEvery(EntryActionTypes.CONFIG_UPDATE, ({ payload: { data } }) =>
services.updateConfig(data),
),
takeEvery(EntryActionTypes.CONFIG_UPDATE_HANDLE, ({ payload: { config } }) =>
services.handleConfigUpdate(config),
),
takeEvery(EntryActionTypes.SMTP_CONFIG_TEST, () => services.testSmtpConfig()),
]);
}

View File

@@ -7,6 +7,7 @@ import router from './router';
import socket from './socket';
import core from './core';
import modals from './modals';
import config from './config';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
@@ -34,6 +35,7 @@ export default [
socket,
core,
modals,
config,
webhooks,
users,
projects,

View File

@@ -29,6 +29,18 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleConfigUpdate(item));
};
const handleWebhookCreate = ({ item }) => {
emit(entryActions.handleWebhookCreate(item));
};
const handleWebhookUpdate = ({ item }) => {
emit(entryActions.handleWebhookUpdate(item));
};
const handleWebhookDelete = ({ item }) => {
emit(entryActions.handleWebhookDelete(item));
};
const handleUserCreate = ({ item }) => {
emit(entryActions.handleUserCreate(item));
};
@@ -280,6 +292,10 @@ const createSocketEventsChannel = () =>
socket.on('configUpdate', handleConfigUpdate);
socket.on('webhookCreate', handleWebhookCreate);
socket.on('webhookUpdate', handleWebhookUpdate);
socket.on('webhookDelete', handleWebhookDelete);
socket.on('userCreate', handleUserCreate);
socket.on('userUpdate', handleUserUpdate);
socket.on('userDelete', handleUserDelete);
@@ -370,6 +386,10 @@ const createSocketEventsChannel = () =>
socket.off('configUpdate', handleConfigUpdate);
socket.off('webhookCreate', handleWebhookCreate);
socket.off('webhookUpdate', handleWebhookUpdate);
socket.off('webhookDelete', handleWebhookDelete);
socket.off('userCreate', handleUserCreate);
socket.off('userUpdate', handleUserUpdate);
socket.off('userDelete', handleUserDelete);

View File

@@ -16,9 +16,9 @@ import Paths from '../../../constants/Paths';
import AccessTokenSteps from '../../../constants/AccessTokenSteps';
export function* initializeLogin() {
const { item: config } = yield call(api.getConfig); // TODO: handle error
const { item: bootstrap } = yield call(api.getBootstrap); // TODO: handle error
yield put(actions.initializeLogin(config));
yield put(actions.initializeLogin(bootstrap));
}
export function* authenticate(data) {
@@ -42,7 +42,7 @@ export function* authenticate(data) {
}
export function* authenticateWithOidc() {
const oidcConfig = yield select(selectors.selectOidcConfig);
const oidcBootstrap = yield select(selectors.selectOidcBootstrap);
const state = nanoid();
window.localStorage.setItem('oidc-state', state);
@@ -50,7 +50,7 @@ export function* authenticateWithOidc() {
const nonce = nanoid();
window.localStorage.setItem('oidc-nonce', nonce);
let redirectUrl = `${oidcConfig.authorizationUrl}`;
let redirectUrl = `${oidcBootstrap.authorizationUrl}`;
redirectUrl += `&state=${encodeURIComponent(state)}`;
redirectUrl += `&nonce=${encodeURIComponent(nonce)}`;

View File

@@ -49,9 +49,9 @@ export function* handleLocationChange() {
switch (pathsMatch.pattern.path) {
case Paths.LOGIN: {
const oidcConfig = yield select(selectors.selectOidcConfig);
const oidcBootstrap = yield select(selectors.selectOidcBootstrap);
if (oidcConfig) {
if (oidcBootstrap) {
const params = new URLSearchParams(window.location.search);
if (params.has('authenticateWithOidc')) {

View File

@@ -7,11 +7,11 @@ export const selectIsSocketDisconnected = ({ socket: { isDisconnected } }) => is
export const selectIsInitializing = ({ common: { isInitializing } }) => isInitializing;
export const selectConfig = ({ common: { config } }) => config;
export const selectBootstrap = ({ common: { bootstrap } }) => bootstrap;
export const selectOidcConfig = (state) => selectConfig(state).oidc;
export const selectOidcBootstrap = (state) => selectBootstrap(state).oidc;
export const selectActiveUsersLimit = (state) => selectConfig(state).activeUsersLimit;
export const selectActiveUsersLimit = (state) => selectBootstrap(state).activeUsersLimit;
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
@@ -21,14 +21,17 @@ export const selectUserCreateForm = ({ ui: { userCreateForm } }) => userCreateFo
export const selectProjectCreateForm = ({ ui: { projectCreateForm } }) => projectCreateForm;
export const selectSmtpTest = ({ ui: { smtpTest } }) => smtpTest;
export default {
selectIsSocketDisconnected,
selectIsInitializing,
selectConfig,
selectOidcConfig,
selectBootstrap,
selectOidcBootstrap,
selectActiveUsersLimit,
selectAccessToken,
selectAuthenticateForm,
selectUserCreateForm,
selectProjectCreateForm,
selectSmtpTest,
};

View File

@@ -11,6 +11,8 @@ export const selectIsFavoritesEnabled = ({ core: { isFavoritesEnabled } }) => is
export const selectIsEditModeEnabled = ({ core: { isEditModeEnabled } }) => isEditModeEnabled;
export const selectConfig = ({ core: { config } }) => config;
export const selectRecentCardId = ({ core: { recentCardId } }) => recentCardId;
export const selectPrevCardId = ({ core: { prevCardIds } }) => prevCardIds.at(-1);
@@ -29,6 +31,7 @@ export default {
selectIsLogouting,
selectIsFavoritesEnabled,
selectIsEditModeEnabled,
selectConfig,
selectRecentCardId,
selectPrevCardId,
selectHomeView,

View File

@@ -76,14 +76,15 @@ services:
# - OIDC_ENFORCED=true
# Email Notifications (https://nodemailer.com/smtp/)
# These values override and disable configuration in the UI if set.
# - SMTP_HOST=
# - SMTP_PORT=587
# - SMTP_NAME=
# - SMTP_SECURE=true
# - SMTP_TLS_REJECT_UNAUTHORIZED=false
# - SMTP_USER=
# - SMTP_PASSWORD=
# - SMTP_FROM="Demo Demo" <demo@demo.demo>
# - SMTP_TLS_REJECT_UNAUTHORIZED=false
# Using Gravatar directly exposes user IPs and hashed emails to a third party (GDPR risk).
# Use a proxy you control for privacy, or leave commented out or empty to disable.

View File

@@ -94,16 +94,17 @@ services:
# - OIDC_ENFORCED=true
# Email Notifications (https://nodemailer.com/smtp/)
# These values override and disable configuration in the UI if set.
# - SMTP_HOST=
# - SMTP_PORT=587
# - SMTP_NAME=
# - SMTP_SECURE=true
# - SMTP_TLS_REJECT_UNAUTHORIZED=false
# - SMTP_USER=
# - SMTP_PASSWORD=
# Optionally store in secrets - then SMTP_PASSWORD should not be set
# - SMTP_PASSWORD__FILE=/run/secrets/smtp_password
# - SMTP_FROM="Demo Demo" <demo@demo.demo>
# - SMTP_TLS_REJECT_UNAUTHORIZED=false
# Using Gravatar directly exposes user IPs and hashed emails to a third party (GDPR risk).
# Use a proxy you control for privacy, or leave commented out or empty to disable.

View File

@@ -67,14 +67,15 @@ SECRET_KEY=notsecretkey
# OIDC_ENFORCED=true
# Email Notifications (https://nodemailer.com/smtp/)
# These values override and disable configuration in the UI if set.
# SMTP_HOST=
# SMTP_PORT=587
# SMTP_NAME=
# SMTP_SECURE=true
# SMTP_TLS_REJECT_UNAUTHORIZED=false
# SMTP_USER=
# SMTP_PASSWORD=
# SMTP_FROM="Demo Demo" <demo@demo.demo>
# SMTP_TLS_REJECT_UNAUTHORIZED=false
# Using Gravatar directly exposes user IPs and hashed emails to a third party (GDPR risk).
# Use a proxy you control for privacy, or leave commented out or empty to disable.

View File

@@ -0,0 +1,72 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
/**
* @swagger
* /bootstrap:
* get:
* summary: Get application bootstrap
* description: Retrieves the application bootstrap.
* tags:
* - Bootstrap
* operationId: getBootstrap
* responses:
* 200:
* description: Bootstrap retrieved successfully
* content:
* application/json:
* schema:
* type: object
* required:
* - oidc
* - version
* properties:
* oidc:
* type: object
* required:
* - authorizationUrl
* - endSessionUrl
* - isEnforced
* nullable: true
* description: OpenID Connect configuration (null if not configured)
* properties:
* authorizationUrl:
* type: string
* format: uri
* description: OIDC authorization URL for initiating authentication
* example: https://oidc.example.com/auth
* endSessionUrl:
* type: string
* format: uri
* nullable: true
* description: OIDC end session URL for logout (null if not supported by provider)
* example: https://oidc.example.com/logout
* isEnforced:
* type: boolean
* description: Whether OIDC authentication is enforced (users must use OIDC to login)
* example: false
* activeUsersLimit:
* type: number
* nullable: true
* description: Maximum number of active users allowed (conditionally added for admins if configured)
* example: 100
* version:
* type: string
* description: Current version of the PLANKA application
* example: 2.0.0
* security: []
*/
module.exports = {
async fn() {
const { currentUser } = this.req;
const oidc = await sails.hooks.oidc.getBootstrap();
return {
item: sails.helpers.bootstrap.presentOne(oidc, currentUser),
};
},
};

View File

@@ -8,7 +8,7 @@
* /config:
* get:
* summary: Get application configuration
* description: Retrieves the application configuration.
* description: Retrieves the application configuration. Requires admin privileges.
* tags:
* - Config
* operationId: getConfig
@@ -24,39 +24,14 @@
* properties:
* item:
* $ref: '#/components/schemas/Config'
* security: []
*/
module.exports = {
async fn() {
const { currentUser } = this.req;
const oidcClient = await sails.hooks.oidc.getClient();
let oidc = null;
if (oidcClient) {
const authorizationUrlParams = {
scope: sails.config.custom.oidcScopes,
};
if (!sails.config.custom.oidcUseDefaultResponseMode) {
authorizationUrlParams.response_mode = sails.config.custom.oidcResponseMode;
}
oidc = {
authorizationUrl: oidcClient.authorizationUrl(authorizationUrlParams),
endSessionUrl: oidcClient.issuer.end_session_endpoint ? oidcClient.endSessionUrl({}) : null,
isEnforced: sails.config.custom.oidcEnforced,
};
}
const config = await Config.qm.getOneMain();
return {
item: sails.helpers.config.presentOne(
{
oidc,
},
currentUser,
),
item: sails.helpers.config.presentOne(config),
};
},
};

View File

@@ -0,0 +1,117 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
/**
* @swagger
* /config/test-smtp:
* post:
* summary: Test SMTP configuration
* description: Sends a test email to verify the SMTP is configured correctly. Only available when SMTP is configured via the UI.
* tags:
* - Config
* operationId: testSmtpConfig
* responses:
* 200:
* description: Test email sent successfully
* content:
* application/json:
* schema:
* type: object
* required:
* - item
* properties:
* item:
* $ref: '#/components/schemas/Config'
* 401:
* $ref: '#/components/responses/Unauthorized'
* 403:
* $ref: '#/components/responses/Forbidden'
*/
const Errors = {
NOT_AVAILABLE: {
notAvailable: 'Not available',
},
};
module.exports = {
exits: {
notAvailable: {
responseType: 'forbidden',
},
},
async fn() {
const { currentUser } = this.req;
if (sails.config.custom.smtpHost) {
return Errors.NOT_AVAILABLE;
}
const { transporter, config } = await sails.helpers.utils.makeSmtpTransporter({
connectionTimeout: 5000,
greetingTimeout: 5000,
socketTimeout: 10000,
dnsTimeout: 3000,
});
if (!transporter) {
return Errors.NOT_AVAILABLE;
}
const logs = [];
try {
logs.push('📧 Sending test email...');
/* eslint-disable no-underscore-dangle */
const info = await transporter.sendMail({
to: currentUser.email,
subject: this.req.i18n.__('Test Title'),
text: this.req.i18n.__('This is a test text message!'),
html: this.req.i18n.__('This is a <i>test</i> <b>html</b> <code>message</code>!'),
});
/* eslint-enable no-underscore-dangle */
logs.push('✅ Email sent successfully!', '');
logs.push(`📬 Message ID: ${info.messageId}`);
if (info.response) {
logs.push(`📤 Server response: ${info.response.trim()}`);
}
logs.push('', '🎉 Your configuration is working correctly!');
} catch (error) {
logs.push('❌ Failed to send email!', '');
if (error.code) {
logs.push(`⚠️ Error code: ${error.code}`);
}
logs.push(`💬 Reason: ${error.message.trim()}`);
if (error.code === 'EDNS') {
logs.push('', '💡 Hint: Check your host setting.');
} else if (error.code === 'ETIMEDOUT') {
logs.push('', '💡 Hint: Check your host and port settings.');
} else if (error.code === 'EAUTH') {
logs.push('', '💡 Hint: Check your username and password.');
} else if (error.code === 'ESOCKET') {
if (error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')) {
logs.push('', '💡 Hint: Check your host and port settings.');
} else if (error.message.includes('wrong version number')) {
logs.push('', '💡 Hint: Try toggling "Use secure connection".');
} else if (error.message.includes('certificate')) {
logs.push('', '💡 Hint: Try toggling "Reject unauthorized TLS certificates".');
}
}
} finally {
transporter.close();
}
return {
item: sails.helpers.config.presentOne(config),
included: {
logs,
},
};
},
};

View File

@@ -0,0 +1,151 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
/**
* @swagger
* /config:
* patch:
* summary: Update application configuration
* description: Updates the application configuration. Requires admin privileges.
* tags:
* - Config
* operationId: updateConfig
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* smtpHost:
* type: string
* maxLength: 256
* nullable: true
* description: Hostname or IP address of the SMTP server
* example: smtp.example.com
* smtpHost:
* type: number
* minimum: 0
* maximum: 65535
* nullable: true
* description: Port number of the SMTP server
* example: 587
* smtpName:
* type: string
* maxLength: 256
* nullable: true
* description: Client hostname used in the EHLO command for SMTP
* example: localhost
* smtpSecure:
* type: boolean
* description: Whether to use a secure connection for SMTP
* example: false
* smtpTlsRejectUnauthorized:
* type: boolean
* description: Whether to reject unauthorized or self-signed TLS certificates for SMTP connections
* example: true
* smtpUser:
* type: string
* maxLength: 256
* nullable: true
* description: Username for authenticating with the SMTP server
* example: no-reply@example.com
* smtpPassword:
* type: string
* maxLength: 256
* nullable: true
* description: Password for authenticating with the SMTP server
* example: SecurePassword123!
* smtpFrom:
* type: string
* maxLength: 256
* nullable: true
* description: Default "from" used for outgoing SMTP emails
* example: no-reply@example.com
* responses:
* 200:
* description: Configuration updated successfully
* content:
* application/json:
* schema:
* type: object
* required:
* - item
* properties:
* item:
* $ref: '#/components/schemas/Config'
*/
module.exports = {
inputs: {
smtpHost: {
type: 'string',
isNotEmptyString: true,
maxLength: 256,
allowNull: true,
},
smtpPort: {
type: 'number',
min: 0,
max: 65535,
allowNull: true,
},
smtpName: {
type: 'string',
isNotEmptyString: true,
maxLength: 256,
allowNull: true,
},
smtpSecure: {
type: 'boolean',
},
smtpTlsRejectUnauthorized: {
type: 'boolean',
},
smtpUser: {
type: 'string',
isNotEmptyString: true,
maxLength: 256,
allowNull: true,
},
smtpPassword: {
type: 'string',
isNotEmptyString: true,
maxLength: 256,
allowNull: true,
},
smtpFrom: {
type: 'string',
isNotEmptyString: true,
maxLength: 256,
allowNull: true,
},
},
async fn(inputs) {
const { currentUser } = this.req;
const values = _.pick(inputs, [
'smtpHost',
'smtpPort',
'smtpName',
'smtpSecure',
'smtpTlsRejectUnauthorized',
'smtpUser',
'smtpPassword',
'smtpFrom',
]);
const config = await sails.helpers.config.updateMain.with({
values,
actorUser: currentUser,
request: this.req,
});
return {
item: sails.helpers.config.presentOne(config),
};
},
};

View File

@@ -84,10 +84,17 @@ module.exports = {
}
}
await sails.helpers.notificationServices.testOne.with({
record: notificationService,
i18n: this.req.i18n,
/* eslint-disable no-underscore-dangle */
await sails.helpers.utils.sendNotifications.with({
services: [_.pick(notificationService, ['url', 'format'])],
title: this.req.i18n.__('Test Title'),
bodyByFormat: {
text: this.req.i18n.__('This is a test text message!'),
markdown: this.req.i18n.__('This is a *test* **markdown** `message`!'),
html: this.req.i18n.__('This is a <i>test</i> <b>html</b> <code>message</code>!'),
},
});
/* eslint-enable no-underscore-dangle */
return {
item: notificationService,

View File

@@ -54,13 +54,11 @@
* included:
* type: object
* required:
* - accessTokens
* - accessToken
* properties:
* accessTokens:
* type: array
* description: New acces tokens (when updating own password)
* items:
* accessToken:
* type: string
* description: New acces tokens (when updating own password)
* example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ4...
* 400:
* $ref: '#/components/responses/ValidationError'
@@ -180,7 +178,7 @@ module.exports = {
return {
item: sails.helpers.users.presentOne(user, currentUser),
included: {
accessTokens: [accessToken],
accessToken,
},
};
}

View File

@@ -154,9 +154,9 @@ module.exports = {
await sails.helpers.notifications.createOne.with({
values: {
action,
userId: action.data.user.id,
type: action.type,
data: action.data,
userId: action.data.user.id,
creatorUser: values.user,
card: values.card,
},
@@ -179,24 +179,20 @@ module.exports = {
const notifiableUserIds = _.union(cardSubscriptionUserIds, boardSubscriptionUserIds);
await Promise.all(
notifiableUserIds.map((userId) =>
sails.helpers.notifications.createOne.with({
values: {
await sails.helpers.notifications.createMany.with({
arrayOfValues: notifiableUserIds.map((userId) => ({
userId,
action,
type: action.type,
data: action.data,
creatorUser: values.user,
card: values.card,
},
})),
project: inputs.project,
board: inputs.board,
list: inputs.list,
webhooks: inputs.webhooks,
}),
),
);
});
}
}

View File

@@ -0,0 +1,29 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
module.exports = {
sync: true,
inputs: {
oidc: {
type: 'ref',
},
user: {
type: 'ref',
},
},
fn(inputs) {
const data = {
oidc: inputs.oidc,
version: sails.config.custom.version,
};
if (inputs.user && inputs.user.role === User.Roles.ADMIN) {
data.activeUsersLimit = sails.config.custom.activeUsersLimit;
}
return data;
},
};

View File

@@ -124,11 +124,9 @@ module.exports = {
boardSubscriptionUserIds,
);
await Promise.all(
notifiableUserIds.map((userId) =>
sails.helpers.notifications.createOne.with({
await sails.helpers.notifications.createMany.with({
webhooks,
values: {
arrayOfValues: notifiableUserIds.map((userId) => ({
userId,
comment,
type: mentionUserIdsSet.has(userId)
@@ -140,13 +138,11 @@ module.exports = {
},
creatorUser: values.user,
card: values.card,
},
})),
project: inputs.project,
board: inputs.board,
list: inputs.list,
}),
),
);
});
if (values.user.subscribeToCardWhenCommenting) {
let cardSubscription;

View File

@@ -11,20 +11,17 @@ module.exports = {
type: 'ref',
required: true,
},
user: {
type: 'ref',
},
},
fn(inputs) {
const data = {
...inputs.record,
version: sails.config.custom.version,
};
if (inputs.user && inputs.user.role === User.Roles.ADMIN) {
data.activeUsersLimit = sails.config.custom.activeUsersLimit;
if (sails.config.custom.smtpHost) {
return _.omit(inputs.record, Config.SMTP_FIELD_NAMES);
}
return data;
if (inputs.record.smtpPassword) {
return _.omit(inputs.record, 'smtpPassword');
}
return inputs.record;
},
};

View File

@@ -0,0 +1,53 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
module.exports = {
inputs: {
values: {
type: 'json',
required: true,
},
actorUser: {
type: 'ref',
required: true,
},
request: {
type: 'ref',
},
},
async fn(inputs) {
const { values } = inputs;
const config = await Config.qm.updateOneMain(values);
const configRelatedUserIds = await sails.helpers.users.getAllIds(User.Roles.ADMIN);
configRelatedUserIds.forEach((userId) => {
sails.sockets.broadcast(
`user:${userId}`,
'configUpdate',
{
item: sails.helpers.config.presentOne(config),
},
inputs.request,
);
});
const webhooks = await Webhook.qm.getAll();
// TODO: with prevData?
sails.helpers.utils.sendWebhooks.with({
webhooks,
event: Webhook.Events.CONFIG_UPDATE,
buildData: () => ({
item: sails.helpers.config.presentOne(config),
}),
user: inputs.actorUser,
});
return config;
},
};

View File

@@ -1,33 +0,0 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
/* eslint-disable no-underscore-dangle */
module.exports = {
inputs: {
record: {
type: 'ref',
required: true,
},
i18n: {
type: 'ref',
required: true,
},
},
async fn(inputs) {
const { i18n } = inputs;
await sails.helpers.utils.sendNotifications.with({
services: [_.pick(inputs.record, ['url', 'format'])],
title: i18n.__('Test Title'),
bodyByFormat: {
text: i18n.__('This is a test text message!'),
markdown: i18n.__('This is a *test* **markdown** `message`!'),
html: i18n.__('This is a <i>test</i> <b>html</b> <code>message</code>'),
},
});
},
};

View File

@@ -0,0 +1,358 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const escapeMarkdown = require('escape-markdown');
const escapeHtml = require('escape-html');
const { mentionMarkupToText } = require('../../../utils/mentions');
const buildTitle = (notification, t) => {
switch (notification.type) {
case Notification.Types.MOVE_CARD:
return t('Card Moved');
case Notification.Types.COMMENT_CARD:
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;
}
};
const buildBodyByFormat = (board, card, notification, 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>`;
switch (notification.type) {
case Notification.Types.MOVE_CARD: {
const fromListName = sails.helpers.lists.makeName(notification.data.fromList);
const toListName = sails.helpers.lists.makeName(notification.data.toList);
return {
text: t(
'%s moved %s from %s to %s on %s',
actorUser.name,
card.name,
fromListName,
toListName,
board.name,
),
markdown: t(
'%s moved %s from %s to %s on %s',
escapeMarkdown(actorUser.name),
markdownCardLink,
`**${escapeMarkdown(fromListName)}**`,
`**${escapeMarkdown(toListName)}**`,
escapeMarkdown(board.name),
),
html: t(
'%s moved %s from %s to %s on %s',
escapeHtml(actorUser.name),
htmlCardLink,
`<b>${escapeHtml(fromListName)}</b>`,
`<b>${escapeHtml(toListName)}</b>`,
escapeHtml(board.name),
),
};
}
case Notification.Types.COMMENT_CARD: {
const commentText = _.truncate(mentionMarkupToText(notification.data.text));
return {
text: `${t(
'%s left a new comment to %s on %s',
actorUser.name,
card.name,
board.name,
)}:\n${commentText}`,
markdown: `${t(
'%s left a new comment to %s on %s',
escapeMarkdown(actorUser.name),
markdownCardLink,
escapeMarkdown(board.name),
)}:\n\n*${escapeMarkdown(commentText)}*`,
html: `${t(
'%s left a new comment to %s on %s',
escapeHtml(actorUser.name),
htmlCardLink,
escapeHtml(board.name),
)}:\n\n<i>${escapeHtml(commentText)}</i>`,
};
}
case Notification.Types.ADD_MEMBER_TO_CARD:
return {
text: t('%s added you to %s on %s', actorUser.name, card.name, board.name),
markdown: t(
'%s added you to %s on %s',
escapeMarkdown(actorUser.name),
markdownCardLink,
escapeMarkdown(board.name),
),
html: t(
'%s added you to %s on %s',
escapeHtml(actorUser.name),
htmlCardLink,
escapeHtml(board.name),
),
};
case Notification.Types.MENTION_IN_COMMENT: {
const commentText = _.truncate(mentionMarkupToText(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;
}
};
const buildAndSendNotifications = async (services, board, card, notification, actorUser, t) => {
await sails.helpers.utils.sendNotifications(
services,
buildTitle(notification, t),
buildBodyByFormat(board, card, notification, actorUser, t),
);
};
// TODO: use templates (views) to build html
const buildEmail = (board, card, notification, actorUser, notifiableUser, t) => {
const cardLink = `<a href="${sails.config.custom.baseUrl}/cards/${card.id}">${escapeHtml(card.name)}</a>`;
const boardLink = `<a href="${sails.config.custom.baseUrl}/boards/${board.id}">${escapeHtml(board.name)}</a>`;
let html;
switch (notification.type) {
case Notification.Types.MOVE_CARD: {
const fromListName = sails.helpers.lists.makeName(notification.data.fromList);
const toListName = sails.helpers.lists.makeName(notification.data.toList);
html = `<p>${t(
'%s moved %s from %s to %s on %s',
escapeHtml(actorUser.name),
cardLink,
escapeHtml(fromListName),
escapeHtml(toListName),
boardLink,
)}</p>`;
break;
}
case Notification.Types.COMMENT_CARD:
html = `<p>${t(
'%s left a new comment to %s on %s',
escapeHtml(actorUser.name),
cardLink,
boardLink,
)}</p><p>${escapeHtml(mentionMarkupToText(notification.data.text))}</p>`;
break;
case Notification.Types.ADD_MEMBER_TO_CARD:
html = `<p>${t(
'%s added you to %s on %s',
escapeHtml(actorUser.name),
cardLink,
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(mentionMarkupToText(notification.data.text))}</p>`;
break;
default:
return null; // TODO: throw error?
}
return {
html,
to: notifiableUser.email,
subject: buildTitle(notification, t),
};
};
const sendEmails = async (transporter, emails) => {
await Promise.all(
emails.map((email) =>
sails.helpers.utils.sendEmail.with({
...email,
transporter,
}),
),
);
transporter.close();
};
module.exports = {
inputs: {
arrayOfValues: {
type: 'ref',
required: true,
},
project: {
type: 'ref',
required: true,
},
board: {
type: 'ref',
required: true,
},
list: {
type: 'ref',
required: true,
},
webhooks: {
type: 'ref',
required: true,
},
},
async fn(inputs) {
const { arrayOfValues } = inputs;
const ids = await sails.helpers.utils.generateIds(arrayOfValues.length);
const valuesById = {};
const notifications = await Notification.qm.create(
arrayOfValues.map((values) => {
const id = ids.shift();
const isCommentRelated =
values.type === Notification.Types.COMMENT_CARD ||
values.type === Notification.Types.MENTION_IN_COMMENT;
const nextValues = {
...values,
id,
creatorUserId: values.creatorUser.id,
boardId: values.card.boardId,
cardId: values.card.id,
};
if (isCommentRelated) {
nextValues.commentId = values.comment.id;
} else {
nextValues.actionId = values.action.id;
}
valuesById[id] = { ...nextValues }; // FIXME: hack
return nextValues;
}),
);
notifications.forEach((notification) => {
const values = valuesById[notification.id];
sails.sockets.broadcast(`user:${notification.userId}`, 'notificationCreate', {
item: notification,
included: {
users: [sails.helpers.users.presentOne(values.creatorUser, {})], // FIXME: hack
},
});
sails.helpers.utils.sendWebhooks.with({
webhooks: inputs.webhooks,
event: Webhook.Events.NOTIFICATION_CREATE,
buildData: () => ({
item: notification,
included: {
projects: [inputs.project],
boards: [inputs.board],
lists: [inputs.list],
cards: [values.card],
...(notification.commentId
? {
comments: [values.comment],
}
: {
actions: [values.action],
}),
},
}),
user: values.creatorUser,
});
});
const notificationsByUserId = _.groupBy(notifications, 'userId');
const userIds = Object.keys(notificationsByUserId);
const notificationServices = await NotificationService.qm.getByUserIds(userIds);
const { transporter } = await sails.helpers.utils.makeSmtpTransporter();
if (notificationServices.length > 0 || transporter) {
const users = await User.qm.getByIds(userIds);
const userById = _.keyBy(users, 'id');
const notificationServicesByUserId = _.groupBy(notificationServices, 'userId');
Object.keys(notificationsByUserId).forEach(async (userId) => {
const notifiableUser = userById[userId];
const t = sails.helpers.utils.makeTranslator(notifiableUser.language);
const emails = notificationsByUserId[userId].flatMap((notification) => {
const values = valuesById[notification.id];
if (notificationServicesByUserId[userId]) {
const services = notificationServicesByUserId[userId].map((notificationService) =>
_.pick(notificationService, ['url', 'format']),
);
buildAndSendNotifications(
services,
inputs.board,
values.card,
notification,
values.creatorUser,
t,
);
}
if (transporter) {
return buildEmail(
inputs.board,
values.card,
notification,
values.creatorUser,
notifiableUser,
t,
);
}
return [];
});
if (emails.length > 0) {
sendEmails(transporter, emails);
}
});
}
return notifications;
},
};

View File

@@ -137,7 +137,15 @@ const buildAndSendNotifications = async (services, board, card, notification, ac
};
// TODO: use templates (views) to build html
const buildAndSendEmail = async (board, card, notification, actorUser, notifiableUser, t) => {
const buildAndSendEmail = async (
transporter,
board,
card,
notification,
actorUser,
notifiableUser,
t,
) => {
const cardLink = `<a href="${sails.config.custom.baseUrl}/cards/${card.id}">${escapeHtml(card.name)}</a>`;
const boardLink = `<a href="${sails.config.custom.baseUrl}/boards/${board.id}">${escapeHtml(board.name)}</a>`;
@@ -164,7 +172,7 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
escapeHtml(actorUser.name),
cardLink,
boardLink,
)}</p><p>${escapeHtml(notification.data.text)}</p>`;
)}</p><p>${escapeHtml(mentionMarkupToText(notification.data.text))}</p>`;
break;
case Notification.Types.ADD_MEMBER_TO_CARD:
@@ -182,7 +190,7 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
escapeHtml(actorUser.name),
cardLink,
boardLink,
)}</p><p>${escapeHtml(notification.data.text)}</p>`;
)}</p><p>${escapeHtml(mentionMarkupToText(notification.data.text))}</p>`;
break;
default:
@@ -190,10 +198,13 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
}
await sails.helpers.utils.sendEmail.with({
transporter,
html,
to: notifiableUser.email,
subject: buildTitle(notification, t),
});
transporter.close();
};
module.exports = {
@@ -223,10 +234,6 @@ module.exports = {
async fn(inputs) {
const { values } = inputs;
if (values.user) {
values.userId = values.user.id;
}
const isCommentRelated =
values.type === Notification.Types.COMMENT_CARD ||
values.type === Notification.Types.MENTION_IN_COMMENT;
@@ -274,9 +281,10 @@ module.exports = {
});
const notificationServices = await NotificationService.qm.getByUserId(notification.userId);
const { transporter } = await sails.helpers.utils.makeSmtpTransporter();
if (notificationServices.length > 0 || sails.hooks.smtp.isEnabled()) {
const notifiableUser = values.user || (await User.qm.getOneById(notification.userId));
if (notificationServices.length > 0 || transporter) {
const notifiableUser = await User.qm.getOneById(notification.userId);
const t = sails.helpers.utils.makeTranslator(notifiableUser.language);
if (notificationServices.length > 0) {
@@ -294,8 +302,9 @@ module.exports = {
);
}
if (sails.hooks.smtp.isEnabled()) {
if (transporter) {
buildAndSendEmail(
transporter,
inputs.board,
values.card,
notification,

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 nodemailer = require('nodemailer');
module.exports = {
inputs: {
defaultOptions: {
type: 'json',
},
},
async fn(inputs) {
let config;
let sourceConfig;
if (sails.config.custom.smtpHost) {
sourceConfig = sails.config.custom;
} else {
config = await Config.qm.getOneMain();
if (config.smtpHost) {
sourceConfig = config;
}
}
if (!sourceConfig) {
return {
config,
transporter: null,
};
}
const transporter = nodemailer.createTransport(
{
...inputs.defaultOptions,
host: sourceConfig.smtpHost,
port: sourceConfig.smtpPort,
name: sourceConfig.smtpName,
secure: sourceConfig.smtpSecure,
auth: sourceConfig.smtpUser && {
user: sourceConfig.smtpUser,
pass: sourceConfig.smtpPassword,
},
tls: {
rejectUnauthorized: sourceConfig.smtpTlsRejectUnauthorized,
},
},
{
from: sourceConfig.smtpFrom,
},
);
return {
transporter,
config,
};
},
};

View File

@@ -5,6 +5,10 @@
module.exports = {
inputs: {
transporter: {
type: 'ref',
required: true,
},
to: {
type: 'string',
required: true,
@@ -20,13 +24,8 @@ module.exports = {
},
async fn(inputs) {
const transporter = sails.hooks.smtp.getTransporter(); // TODO: check if enabled?
try {
const info = await transporter.sendMail({
...inputs,
from: sails.config.custom.smtpFrom,
});
const info = await inputs.transporter.sendMail(inputs);
sails.log.info(`Email sent: ${info.messageId}`);
} catch (error) {

View File

@@ -59,6 +59,28 @@ module.exports = function defineOidcHook(sails) {
return client;
},
async getBootstrap() {
const instance = await this.getClient();
if (!instance) {
return null;
}
const authorizationUrlParams = {
scope: sails.config.custom.oidcScopes,
};
if (!sails.config.custom.oidcUseDefaultResponseMode) {
authorizationUrlParams.response_mode = sails.config.custom.oidcResponseMode;
}
return {
authorizationUrl: instance.authorizationUrl(authorizationUrlParams),
endSessionUrl: instance.issuer.end_session_endpoint ? instance.endSessionUrl({}) : null,
isEnforced: sails.config.custom.oidcEnforced,
};
},
isEnabled() {
return !!sails.config.custom.oidcIssuer;
},

View File

@@ -9,6 +9,37 @@ const defaultFind = (criteria) => Notification.find(criteria).sort('id DESC');
/* Query methods */
const create = (arrayOfValues) =>
sails.getDatastore().transaction(async (db) => {
const notifications = await Notification.createEach(arrayOfValues).fetch().usingConnection(db);
const userIds = sails.helpers.utils.mapRecords(notifications, 'userId', true, true);
if (userIds.length > 0) {
const queryValues = [];
const inValues = userIds.map((userId) => {
queryValues.push(userId);
return `$${queryValues.length}`;
});
queryValues.push(LIMIT);
const query = `
WITH exceeded_notification AS (
SELECT id, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id DESC) AS rank
FROM notification
WHERE user_id IN (${inValues.join(', ')}) AND is_read = FALSE
)
UPDATE notification
SET is_read = TRUE
WHERE id IN (SELECT id FROM exceeded_notification WHERE rank > $${queryValues.length})
`;
await sails.sendNativeQuery(query, queryValues).usingConnection(db);
}
return notifications;
});
const createOne = (values) => {
if (values.userId) {
return sails.getDatastore().transaction(async (db) => {
@@ -26,7 +57,7 @@ const createOne = (values) => {
)
UPDATE notification
SET is_read = TRUE
WHERE id in (SELECT id FROM exceeded_notification)
WHERE id IN (SELECT id FROM exceeded_notification)
`;
await sails.sendNativeQuery(query, [values.userId, LIMIT]).usingConnection(db);
@@ -66,6 +97,7 @@ const updateOne = (criteria, values) => Notification.updateOne(criteria).set({ .
const delete_ = (criteria) => Notification.destroy(criteria).fetch();
module.exports = {
create,
createOne,
getByIds,
getUnreadByUserId,

View File

@@ -14,6 +14,11 @@ const getByUserId = (userId) =>
userId,
});
const getByUserIds = (userIds) =>
defaultFind({
userId: userIds,
});
const getByBoardId = (boardId) =>
defaultFind({
boardId,
@@ -36,6 +41,7 @@ const deleteOne = (criteria) => NotificationService.destroyOne(criteria);
module.exports = {
createOne,
getByUserId,
getByUserIds,
getByBoardId,
getByBoardIds,
getOneById,

View File

@@ -1,55 +0,0 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
/**
* smtp hook
*
* @description :: A hook definition. Extends Sails by adding shadow routes, implicit actions,
* and/or initialization logic.
* @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks
*/
const nodemailer = require('nodemailer');
module.exports = function defineSmtpHook(sails) {
let transporter = null;
return {
/**
* Runs when this Sails app loads/lifts.
*/
async initialize() {
if (!this.isEnabled()) {
return;
}
sails.log.info('Initializing custom hook (`smtp`)');
transporter = nodemailer.createTransport({
pool: true,
host: sails.config.custom.smtpHost,
port: sails.config.custom.smtpPort,
name: sails.config.custom.smtpName,
secure: sails.config.custom.smtpSecure,
auth: sails.config.custom.smtpUser && {
user: sails.config.custom.smtpUser,
pass: sails.config.custom.smtpPassword,
},
tls: {
rejectUnauthorized: sails.config.custom.smtpTlsRejectUnauthorized,
},
});
},
getTransporter() {
return transporter;
},
isEnabled() {
return !!sails.config.custom.smtpHost;
},
};
};

View File

@@ -17,54 +17,131 @@
* Config:
* type: object
* required:
* - version
* - oidc
* - id
* - isInitialized
* properties:
* version:
* id:
* type: string
* description: Current version of the PLANKA application
* example: 2.0.0
* activeUsersLimit:
* description: Unique identifier for the config (always set to '1')
* example: 1
* smtpHost:
* type: string
* nullable: true
* description: Hostname or IP address of the SMTP server
* example: smtp.example.com
* smtpPort:
* type: number
* nullable: true
* description: Maximum number of active users allowed (conditionally added for admins if configured)
* example: 100
* oidc:
* type: object
* required:
* - authorizationUrl
* - endSessionUrl
* - isEnforced
* nullable: true
* description: OpenID Connect configuration (null if not configured)
* properties:
* authorizationUrl:
* description: Port number of the SMTP server
* example: 587
* smtpName:
* type: string
* format: uri
* description: OIDC authorization URL for initiating authentication
* example: https://oidc.example.com/auth
* endSessionUrl:
* type: string
* format: uri
* nullable: true
* description: OIDC end session URL for logout (null if not supported by provider)
* example: https://oidc.example.com/logout
* isEnforced:
* description: Client hostname used in the EHLO command for SMTP
* example: localhost
* smtpSecure:
* type: boolean
* description: Whether OIDC authentication is enforced (users must use OIDC to login)
* description: Whether to use a secure connection for SMTP
* example: false
* smtpTlsRejectUnauthorized:
* type: boolean
* description: Whether to reject unauthorized or self-signed TLS certificates for SMTP connections
* example: true
* smtpUser:
* type: string
* nullable: true
* description: Username for authenticating with the SMTP server
* example: no-reply@example.com
* smtpPassword:
* type: string
* nullable: true
* description: Password for authenticating with the SMTP server
* example: SecurePassword123!
* smtpFrom:
* type: string
* nullable: true
* description: Default "from" used for outgoing SMTP emails
* example: no-reply@example.com
* isInitialized:
* type: boolean
* description: Whether the PLANKA instance has been initialized
* example: true
* createdAt:
* type: string
* format: date-time
* nullable: true
* description: When the config was created
* example: 2024-01-01T00:00:00.000Z
* updatedAt:
* type: string
* format: date-time
* nullable: true
* description: When the config was last updated
* example: 2024-01-01T00:00:00.000Z
*/
const MAIN_ID = '1';
const SMTP_FIELD_NAMES = [
'smtpHost',
'smtpPort',
'smtpName',
'smtpSecure',
'smtpTlsRejectUnauthorized',
'smtpUser',
'smtpPassword',
'smtpFrom',
];
module.exports = {
MAIN_ID,
SMTP_FIELD_NAMES,
attributes: {
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
smtpHost: {
type: 'string',
allowNull: true,
columnName: 'smtp_host',
},
smtpPort: {
type: 'number',
allowNull: true,
columnName: 'smtp_port',
},
smtpName: {
type: 'string',
allowNull: true,
columnName: 'smtp_name',
},
smtpSecure: {
type: 'boolean',
required: true,
columnName: 'smtp_secure',
},
smtpTlsRejectUnauthorized: {
type: 'boolean',
required: true,
columnName: 'smtp_tls_reject_unauthorized',
},
smtpUser: {
type: 'string',
allowNull: true,
columnName: 'smtp_user',
},
smtpPassword: {
type: 'string',
allowNull: true,
columnName: 'smtp_password',
},
smtpFrom: {
type: 'string',
allowNull: true,
columnName: 'smtp_from',
},
isInitialized: {
type: 'boolean',
required: true,

View File

@@ -107,6 +107,8 @@ const Events = {
COMMENT_UPDATE: 'commentUpdate',
COMMENT_DELETE: 'commentDelete',
CONFIG_UPDATE: 'configUpdate',
CUSTOM_FIELD_CREATE: 'customFieldCreate',
CUSTOM_FIELD_UPDATE: 'customFieldUpdate',
CUSTOM_FIELD_DELETE: 'customFieldDelete',

View File

@@ -102,10 +102,10 @@ module.exports.custom = {
smtpPort: process.env.SMTP_PORT || 587,
smtpName: process.env.SMTP_NAME,
smtpSecure: process.env.SMTP_SECURE === 'true',
smtpTlsRejectUnauthorized: process.env.SMTP_TLS_REJECT_UNAUTHORIZED !== 'false',
smtpUser: process.env.SMTP_USER,
smtpPassword: process.env.SMTP_PASSWORD,
smtpFrom: process.env.SMTP_FROM,
smtpTlsRejectUnauthorized: process.env.SMTP_TLS_REJECT_UNAUTHORIZED !== 'false',
gravatarBaseUrl: process.env.GRAVATAR_BASE_URL,
};

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