Compare commits

...

5 Commits

Author SHA1 Message Date
Maksim Eltyshev
c058df8fc4 fix: Rename GIN indexes 2025-11-27 19:31:11 +01:00
Maksim Eltyshev
0023c63be8 fix: Optimize query methods 2025-11-27 19:09:10 +01:00
Maksim Eltyshev
26b3cffdab fix: Rename getCards inputs for consistency 2025-11-27 18:41:50 +01:00
Maksim Eltyshev
bf2ab4649e fix: Create isolated i18n instances to prevent locale collision 2025-11-27 18:28:25 +01:00
Maksim Eltyshev
54e230d4c1 ref: Refactoring 2025-11-27 18:24:55 +01:00
91 changed files with 443 additions and 384 deletions

View File

@@ -11,8 +11,8 @@ import { Link } from 'react-router-dom';
import { Comment } from 'semantic-ui-react';
import selectors from '../../../selectors';
import { isUserStatic } from '../../../utils/record-helpers';
import Paths from '../../../constants/Paths';
import { StaticUserIds } from '../../../constants/StaticUsers';
import { ActivityTypes } from '../../../constants/Enums';
import TimeAgo from '../../common/TimeAgo';
import UserAvatar from '../../users/UserAvatar';
@@ -30,12 +30,11 @@ const Item = React.memo(({ id }) => {
const [t] = useTranslation();
const userName =
user.id === StaticUserIds.DELETED
? t(`common.${user.name}`, {
context: 'title',
})
: user.name;
const userName = isUserStatic(user)
? t(`common.${user.name}`, {
context: 'title',
})
: user.name;
const cardName = card ? card.name : activity.data.card.name;

View File

@@ -10,7 +10,7 @@ import { useTranslation, Trans } from 'react-i18next';
import { Comment } from 'semantic-ui-react';
import selectors from '../../../selectors';
import { StaticUserIds } from '../../../constants/StaticUsers';
import { isUserStatic } from '../../../utils/record-helpers';
import { ActivityTypes } from '../../../constants/Enums';
import TimeAgo from '../../common/TimeAgo';
import UserAvatar from '../../users/UserAvatar';
@@ -26,12 +26,11 @@ const Item = React.memo(({ id }) => {
const [t] = useTranslation();
const userName =
user.id === StaticUserIds.DELETED
? t(`common.${user.name}`, {
context: 'title',
})
: user.name;
const userName = isUserStatic(user)
? t(`common.${user.name}`, {
context: 'title',
})
: user.name;
let contentNode;
switch (activity.type) {

View File

@@ -15,13 +15,13 @@ import ListView from './ListView';
const FiniteContent = React.memo(() => {
const board = useSelector(selectors.selectCurrentBoard);
const cardIds = useSelector(selectors.selectFilteredCardIdsForCurrentBoard);
const hasAnyFiniteList = useSelector((state) => !!selectors.selectFirstFiniteListId(state));
const canAddCard = useSelector((state) => !!selectors.selectFirstKanbanListId(state));
const dispatch = useDispatch();
const handleCardCreate = useCallback(
(data, autoOpen) => {
dispatch(entryActions.createCardInFirstFiniteList(data, undefined, autoOpen));
dispatch(entryActions.createCardInCurrentContext(data, undefined, autoOpen));
},
[dispatch],
);
@@ -39,7 +39,7 @@ const FiniteContent = React.memo(() => {
default:
}
return <View cardIds={cardIds} onCardCreate={hasAnyFiniteList ? handleCardCreate : undefined} />;
return <View cardIds={cardIds} onCardCreate={canAddCard ? handleCardCreate : undefined} />;
});
export default FiniteContent;

View File

@@ -23,7 +23,7 @@ import styles from './KanbanContent.module.scss';
import globalStyles from '../../../../styles.module.scss';
const KanbanContent = React.memo(() => {
const listIds = useSelector(selectors.selectFiniteListIdsForCurrentBoard);
const listIds = useSelector(selectors.selectKanbanListIdsForCurrentBoard);
const canAddList = useSelector((state) => {
const isEditModeEnabled = selectors.selectIsEditModeEnabled(state); // TODO: move out?

View File

@@ -21,6 +21,7 @@
top: 2px;
transition: background 85ms ease;
width: 20px;
z-index: 1000;
&:hover {
background: #ebeef0;
@@ -52,6 +53,7 @@
background: #fff;
border-radius: 3px;
box-shadow: 0 1px 0 #ccc;
overflow: hidden;
position: relative;
&:hover {

View File

@@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next';
import { Icon } from 'semantic-ui-react';
import selectors from '../../../selectors';
import { StaticUserIds } from '../../../constants/StaticUsers';
import { isUserStatic } from '../../../utils/record-helpers';
import TimeAgo from '../../common/TimeAgo';
import UserAvatar from '../../users/UserAvatar';
@@ -32,7 +32,7 @@ const CreationDetailsStep = React.memo(({ userId }) => {
</span>
<span className={styles.content}>
<div className={styles.name}>
{user.id === StaticUserIds.DELETED
{isUserStatic(user)
? t(`common.${user.name}`, {
context: 'title',
})

View File

@@ -9,32 +9,32 @@ import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Gallery, Item as GalleryItem } from 'react-photoswipe-gallery';
import { Button, Grid, Icon } from 'semantic-ui-react';
import { useDidUpdate } from '../../../../lib/hooks';
import { useDidUpdate } from '../../../lib/hooks';
import selectors from '../../../../selectors';
import entryActions from '../../../../entry-actions';
import { usePopupInClosableContext } from '../../../../hooks';
import { isUsableMarkdownElement } from '../../../../utils/element-helpers';
import { BoardMembershipRoles, CardTypes, ListTypes } from '../../../../constants/Enums';
import { CardTypeIcons } from '../../../../constants/Icons';
import { ClosableContext } from '../../../../contexts';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { usePopupInClosableContext } from '../../../hooks';
import { isUsableMarkdownElement } from '../../../utils/element-helpers';
import { BoardMembershipRoles, CardTypes, ListTypes } from '../../../constants/Enums';
import { CardTypeIcons } from '../../../constants/Icons';
import { ClosableContext } from '../../../contexts';
import NameField from './NameField';
import Thumbnail from './Thumbnail';
import NameField from '../NameField';
import CustomFieldGroups from '../CustomFieldGroups';
import Communication from '../Communication';
import CreationDetailsStep from '../CreationDetailsStep';
import MoreActionsStep from '../MoreActionsStep';
import Markdown from '../../../common/Markdown';
import EditMarkdown from '../../../common/EditMarkdown';
import ConfirmationStep from '../../../common/ConfirmationStep';
import UserAvatar from '../../../users/UserAvatar';
import BoardMembershipsStep from '../../../board-memberships/BoardMembershipsStep';
import LabelChip from '../../../labels/LabelChip';
import LabelsStep from '../../../labels/LabelsStep';
import ListsStep from '../../../lists/ListsStep';
import Attachments from '../../../attachments/Attachments';
import AddAttachmentStep from '../../../attachments/AddAttachmentStep';
import AddCustomFieldGroupStep from '../../../custom-field-groups/AddCustomFieldGroupStep';
import CustomFieldGroups from './CustomFieldGroups';
import Communication from './Communication';
import CreationDetailsStep from './CreationDetailsStep';
import MoreActionsStep from './MoreActionsStep';
import Markdown from '../../common/Markdown';
import EditMarkdown from '../../common/EditMarkdown';
import ConfirmationStep from '../../common/ConfirmationStep';
import UserAvatar from '../../users/UserAvatar';
import BoardMembershipsStep from '../../board-memberships/BoardMembershipsStep';
import LabelChip from '../../labels/LabelChip';
import LabelsStep from '../../labels/LabelsStep';
import ListsStep from '../../lists/ListsStep';
import Attachments from '../../attachments/Attachments';
import AddAttachmentStep from '../../attachments/AddAttachmentStep';
import AddCustomFieldGroupStep from '../../custom-field-groups/AddCustomFieldGroupStep';
import styles from './StoryContent.module.scss';

View File

@@ -99,7 +99,7 @@
}
.coverWrapper {
padding-bottom: 20px;
padding-bottom: 10px;
}
.cursorPointer {
@@ -283,7 +283,7 @@
}
.moduleWrapperAttachments {
margin-bottom: 20px;
margin-bottom: 10px;
}
.moreActionsButton {

View File

@@ -1,8 +0,0 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import StoryContent from './StoryContent';
export default StoryContent;

View File

@@ -8,7 +8,7 @@ import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Item as GalleryItem } from 'react-photoswipe-gallery';
import selectors from '../../../../selectors';
import selectors from '../../../selectors';
import styles from './Thumbnail.module.scss';

View File

@@ -14,8 +14,7 @@ import { useDidUpdate } from '../../../lib/hooks';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { usePopupInClosableContext } from '../../../hooks';
import { isListArchiveOrTrash } from '../../../utils/record-helpers';
import { StaticUserIds } from '../../../constants/StaticUsers';
import { isListArchiveOrTrash, isUserStatic } from '../../../utils/record-helpers';
import { BoardMembershipRoles } from '../../../constants/Enums';
import { ClosableContext } from '../../../contexts';
import Edit from './Edit';
@@ -107,7 +106,7 @@ const Item = React.memo(({ id }) => {
) : (
<div className={classNames(styles.bubble, isCurrentUser && styles.bubbleRight)}>
<div className={styles.header}>
{user.id === StaticUserIds.DELETED
{isUserStatic(user)
? t(`common.${user.name}`, {
context: 'title',
})

View File

@@ -17,9 +17,9 @@ import AddStep from './AddStep';
import styles from './UsersPane.module.scss';
const UsersPane = React.memo(() => {
const activeUsersTotal = useSelector(selectors.selectActiveUsersTotal);
const activeUsersLimit = useSelector(selectors.selectActiveUsersLimit);
const users = useSelector(selectors.selectUsers);
const activeUsersTotal = useSelector(selectors.selectActiveUsersTotal);
const canAdd = useSelector((state) => {
const oidcBootstrap = selectors.selectOidcBootstrap(state);

View File

@@ -12,7 +12,7 @@ import selectors from '../../../selectors';
import matchPaths from '../../../utils/match-paths';
import Paths from '../../../constants/Paths';
const Linkify = React.memo(({ href, content, stopPropagation, ...props }) => {
const Link = React.memo(({ href, content, stopPropagation, ...props }) => {
const selectCardById = useMemo(() => selectors.makeSelectCardById(), []);
const url = useMemo(() => {
@@ -68,14 +68,14 @@ const Linkify = React.memo(({ href, content, stopPropagation, ...props }) => {
);
});
Linkify.propTypes = {
Link.propTypes = {
href: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
stopPropagation: PropTypes.bool,
};
Linkify.defaultProps = {
Link.defaultProps = {
stopPropagation: false,
};
export default Linkify;
export default Link;

View File

@@ -14,8 +14,8 @@ import { Button } from 'semantic-ui-react';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { mentionMarkupToText } from '../../../utils/mentions';
import { isUserStatic } from '../../../utils/record-helpers';
import Paths from '../../../constants/Paths';
import { StaticUserIds } from '../../../constants/StaticUsers';
import { NotificationTypes } from '../../../constants/Enums';
import TimeAgo from '../../common/TimeAgo';
import UserAvatar from '../../users/UserAvatar';
@@ -42,12 +42,11 @@ const Item = React.memo(({ id, onClose }) => {
dispatch(entryActions.deleteNotification(id));
}, [id, dispatch]);
const creatorUserName =
creatorUser.id === StaticUserIds.DELETED
? t(`common.${creatorUser.name}`, {
context: 'title',
})
: creatorUser.name;
const creatorUserName = isUserStatic(creatorUser)
? t(`common.${creatorUser.name}`, {
context: 'title',
})
: creatorUser.name;
const cardName = card ? card.name : notification.data.card.name;

View File

@@ -13,7 +13,7 @@ import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import selectors from '../../../selectors';
import { StaticUserIds } from '../../../constants/StaticUsers';
import { isUserStatic } from '../../../utils/record-helpers';
import styles from './UserAvatar.module.scss';
@@ -62,7 +62,7 @@ const UserAvatar = React.memo(
const contentNode = (
<span
title={
user.id === StaticUserIds.DELETED
isUserStatic(user)
? t(`common.${user.name}`, {
context: 'title',
})

View File

@@ -186,7 +186,7 @@ export default {
CARDS_IN_CURRENT_LIST_FETCH: `${PREFIX}/CARDS_IN_CURRENT_LIST_FETCH`,
CARDS_UPDATE_HANDLE: `${PREFIX}/CARDS_UPDATE_HANDLE`,
CARD_CREATE: `${PREFIX}/CARD_CREATE`,
CARD_IN_FIRST_FINITE_LIST_CREATE: `${PREFIX}/CARD_IN_FIRST_FINITE_LIST_CREATE`,
CARD_IN_CURRENT_CONTEXT_CREATE: `${PREFIX}/CARD_IN_CURRENT_CONTEXT_CREATE`,
CARD_IN_CURRENT_LIST_CREATE: `${PREFIX}/CARD_IN_CURRENT_LIST_CREATE`,
CARD_CREATE_HANDLE: `${PREFIX}/CARD_CREATE_HANDLE`,
CARD_UPDATE: `${PREFIX}/CARD_UPDATE`,

View File

@@ -28,8 +28,8 @@ const createCard = (listId, data, index, autoOpen = false) => ({
},
});
const createCardInFirstFiniteList = (data, index = 0, autoOpen = false) => ({
type: EntryActionTypes.CARD_IN_FIRST_FINITE_LIST_CREATE,
const createCardInCurrentContext = (data, index = 0, autoOpen = false) => ({
type: EntryActionTypes.CARD_IN_CURRENT_CONTEXT_CREATE,
payload: {
data,
index,
@@ -180,7 +180,7 @@ export default {
fetchCardsInCurrentList,
handleCardsUpdate,
createCard,
createCardInFirstFiniteList,
createCardInCurrentContext,
createCardInCurrentList,
handleCardCreate,
updateCard,

View File

@@ -7,7 +7,7 @@ import { attr, fk, many } from 'redux-orm';
import BaseModel from './BaseModel';
import buildSearchParts from '../utils/build-search-parts';
import { isListFinite } from '../utils/record-helpers';
import { isListKanban } from '../utils/record-helpers';
import ActionTypes from '../constants/ActionTypes';
import Config from '../constants/Config';
import { BoardContexts, BoardViews } from '../constants/Enums';
@@ -285,8 +285,8 @@ export default class extends BaseModel {
return this.lists.orderBy(['position', 'id.length', 'id']);
}
getFiniteListsQuerySet() {
return this.getListsQuerySet().filter((list) => isListFinite(list));
getKanbanListsQuerySet() {
return this.getListsQuerySet().filter((list) => isListKanban(list));
}
getCustomFieldGroupsQuerySet() {
@@ -316,7 +316,7 @@ export default class extends BaseModel {
}
getCardsModelArray() {
return this.getFiniteListsQuerySet()
return this.getKanbanListsQuerySet()
.toModelArray()
.flatMap((listModel) => listModel.getCardsModelArray());
}

View File

@@ -336,13 +336,11 @@ export default class extends BaseModel {
return [];
}
if (searchRegex) {
cardModels = cardModels.filter(
(cardModel) =>
searchRegex.test(cardModel.name) ||
(cardModel.description && searchRegex.test(cardModel.description)),
);
}
cardModels = cardModels.filter(
(cardModel) =>
searchRegex.test(cardModel.name) ||
(cardModel.description && searchRegex.test(cardModel.description)),
);
} else {
const searchParts = buildSearchParts(this.board.search);

View File

@@ -40,8 +40,8 @@ export function* fetchCards(listId) {
try {
response.body = yield call(request, api.getCards, listId, {
search: (search && search.trim()) || undefined,
filterUserIds: filterUserIds.length > 0 ? filterUserIds.join(',') : undefined,
filterLabelIds: filterLabelIds.length > 0 ? filterLabelIds.join(',') : undefined,
userIds: filterUserIds.length > 0 ? filterUserIds.join(',') : undefined,
labelIds: filterLabelIds.length > 0 ? filterLabelIds.join(',') : undefined,
before: lastCard || undefined,
});
} catch (error) {
@@ -167,10 +167,10 @@ export function* createCard(listId, data, index, autoOpen) {
}
}
export function* createCardInFirstFiniteList(data, index, autoOpen) {
const firstFiniteListId = yield select(selectors.selectFirstFiniteListId);
export function* createCardInCurrentContext(data, index, autoOpen) {
const firstKanbanListId = yield select(selectors.selectFirstKanbanListId);
yield call(createCard, firstFiniteListId, data, index, autoOpen);
yield call(createCard, firstKanbanListId, data, index, autoOpen);
}
export function* createCardInCurrentList(data, autoOpen) {
@@ -601,7 +601,7 @@ export default {
fetchCardsInCurrentList,
handleCardsUpdate,
createCard,
createCardInFirstFiniteList,
createCardInCurrentContext,
createCardInCurrentList,
handleCardCreate,
updateCard,

View File

@@ -20,9 +20,9 @@ export default function* cardsWatchers() {
services.createCard(listId, data, index, autoOpen),
),
takeEvery(
EntryActionTypes.CARD_IN_FIRST_FINITE_LIST_CREATE,
EntryActionTypes.CARD_IN_CURRENT_CONTEXT_CREATE,
({ payload: { data, index, autoOpen } }) =>
services.createCardInFirstFiniteList(data, index, autoOpen),
services.createCardInCurrentContext(data, index, autoOpen),
),
takeEvery(EntryActionTypes.CARD_IN_CURRENT_LIST_CREATE, ({ payload: { data, autoOpen } }) =>
services.createCardInCurrentList(data, autoOpen),

View File

@@ -271,7 +271,7 @@ export const selectTrashListIdForCurrentBoard = createSelector(
},
);
export const selectFiniteListIdsForCurrentBoard = createSelector(
export const selectKanbanListIdsForCurrentBoard = createSelector(
orm,
(state) => selectPath(state).boardId,
({ Board }, id) => {
@@ -286,7 +286,7 @@ export const selectFiniteListIdsForCurrentBoard = createSelector(
}
return boardModel
.getFiniteListsQuerySet()
.getKanbanListsQuerySet()
.toRefArray()
.map((list) => list.id);
},
@@ -482,7 +482,7 @@ export default {
selectLabelsForCurrentBoard,
selectArchiveListIdForCurrentBoard,
selectTrashListIdForCurrentBoard,
selectFiniteListIdsForCurrentBoard,
selectKanbanListIdsForCurrentBoard,
selectAvailableListsForCurrentBoard,
selectCardsExceptCurrentForCurrentBoard,
selectFilteredCardIdsForCurrentBoard,

View File

@@ -127,7 +127,7 @@ export const selectCurrentList = createSelector(
},
);
export const selectFirstFiniteListId = createSelector(
export const selectFirstKanbanListId = createSelector(
orm,
(state) => selectPath(state).boardId,
({ Board }, id) => {
@@ -141,7 +141,7 @@ export const selectFirstFiniteListId = createSelector(
return boardModel;
}
const listModel = boardModel.getFiniteListsQuerySet().first();
const listModel = boardModel.getKanbanListsQuerySet().first();
return listModel && listModel.id;
},
);
@@ -174,6 +174,6 @@ export default {
selectIsListWithIdAvailableForCurrentUser,
selectCurrentListId,
selectCurrentList,
selectFirstFiniteListId,
selectFirstKanbanListId,
selectFilteredCardIdsForCurrentList,
};

View File

@@ -75,7 +75,7 @@ export const selectNextListPosition = createSelector(
return boardModel;
}
return nextPosition(boardModel.getFiniteListsQuerySet().toRefArray(), index, excludedId);
return nextPosition(boardModel.getKanbanListsQuerySet().toRefArray(), index, excludedId);
},
);

View File

@@ -3,8 +3,11 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { StaticUserIds } from '../constants/StaticUsers';
import { ListTypes, UserRoles } from '../constants/Enums';
export const isUserStatic = (user) => [StaticUserIds.DELETED].includes(user.id);
export const isUserAdminOrProjectOwner = (user) =>
[UserRoles.ADMIN, UserRoles.PROJECT_OWNER].includes(user.role);
@@ -12,3 +15,5 @@ export const isListArchiveOrTrash = (list) =>
[ListTypes.ARCHIVE, ListTypes.TRASH].includes(list.type);
export const isListFinite = (list) => [ListTypes.ACTIVE, ListTypes.CLOSED].includes(list.type);
export const isListKanban = (list) => [ListTypes.ACTIVE, ListTypes.CLOSED].includes(list.type);

View File

@@ -11,6 +11,7 @@ const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/;
export const isUrl = (string) =>
isURL(string, {
protocols: ['http', 'https'],
require_tld: false,
require_protocol: true,
max_allowed_length: 2048,
});

View File

@@ -23,10 +23,10 @@
* - name: before
* in: query
* required: false
* description: Pagination cursor (JSON object with id and listChangedAt)
* description: Pagination cursor (JSON object with listChangedAt and id)
* schema:
* type: string
* example: '{"id": "1357158568008091269", "listChangedAt": "2024-01-01T00:00:00.000Z"}'
* example: '{"listChangedAt": "2024-01-01T00:00:00.000Z", "id": "1357158568008091269"}'
* - name: search
* in: query
* required: false
@@ -35,14 +35,14 @@
* type: string
* maxLength: 128
* example: bug fix
* - name: filterUserIds
* - name: userIds
* in: query
* required: false
* description: Comma-separated user IDs to filter by members
* description: Comma-separated user IDs to filter by members or task assignees
* schema:
* type: string
* example: 1357158568008091265,1357158568008091266
* - name: filterLabelIds
* - name: labelIds
* in: query
* required: false
* description: Comma-separated label IDs to filter by labels
@@ -179,8 +179,8 @@ module.exports = {
isNotEmptyString: true,
maxLength: 128,
},
filterUserIds: idsInput,
filterLabelIds: idsInput,
userIds: idsInput,
labelIds: idsInput,
},
exits: {
@@ -215,31 +215,31 @@ module.exports = {
}
let filterUserIds;
if (inputs.filterUserIds) {
if (inputs.userIds) {
const boardMemberships = await BoardMembership.qm.getByBoardId(list.boardId);
const availableUserIdsSet = new Set(
sails.helpers.utils.mapRecords(boardMemberships, 'userId'),
);
filterUserIds = _.uniq(inputs.filterUserIds.split(','));
filterUserIds = _.uniq(inputs.userIds.split(','));
filterUserIds = filterUserIds.filter((userId) => availableUserIdsSet.has(userId));
}
let filterLabelIds;
if (inputs.filterLabelIds) {
if (inputs.labelIds) {
const labels = await Label.qm.getByBoardId(list.boardId);
const availableLabelIdsSet = new Set(sails.helpers.utils.mapRecords(labels));
filterLabelIds = _.uniq(inputs.filterLabelIds.split(','));
filterLabelIds = _.uniq(inputs.labelIds.split(','));
filterLabelIds = filterLabelIds.filter((labelId) => availableLabelIdsSet.has(labelId));
}
const cards = await Card.qm.getByEndlessListId(list.id, {
filterUserIds,
filterLabelIds,
before: inputs.before,
search: inputs.search,
userIds: filterUserIds,
labelIds: filterLabelIds,
});
const cardIds = sails.helpers.utils.mapRecords(cards);

View File

@@ -87,7 +87,7 @@ module.exports = {
},
type: {
type: 'string',
isIn: List.FINITE_TYPES,
isIn: List.KANBAN_TYPES,
required: true,
},
position: {

View File

@@ -100,7 +100,7 @@ module.exports = {
throw Errors.LIST_NOT_FOUND; // Forbidden
}
if (!sails.helpers.lists.isFinite(list)) {
if (!sails.helpers.lists.isKanban(list)) {
throw Errors.NOT_ENOUGH_RIGHTS;
}

View File

@@ -97,7 +97,7 @@ module.exports = {
boardId: idInput,
type: {
type: 'string',
isIn: List.FINITE_TYPES,
isIn: List.KANBAN_TYPES,
},
position: {
type: 'number',
@@ -146,7 +146,7 @@ module.exports = {
throw Errors.LIST_NOT_FOUND; // Forbidden
}
if (!sails.helpers.lists.isFinite(list)) {
if (!sails.helpers.lists.isKanban(list)) {
throw Errors.NOT_ENOUGH_RIGHTS;
}

View File

@@ -14,7 +14,7 @@
* operationId: getUsers
* responses:
* 200:
* description: List of users retrieved successfully
* description: Users retrieved successfully
* content:
* application/json:
* schema:

View File

@@ -14,7 +14,7 @@
* operationId: getWebhooks
* responses:
* 200:
* description: List of webhooks retrieved successfully
* description: Webhooks retrieved successfully
* content:
* application/json:
* schema:

View File

@@ -23,7 +23,7 @@ const buildBodyByFormat = (board, card, action, actorUser, t) => {
switch (action.type) {
case Action.Types.CREATE_CARD: {
const listName = sails.helpers.lists.makeName(action.data.list);
const listName = sails.helpers.lists.resolveName(action.data.list, t);
return {
text: t('%s created %s in %s on %s', actorUser.name, card.name, listName, board.name),
@@ -44,8 +44,8 @@ const buildBodyByFormat = (board, card, action, actorUser, t) => {
};
}
case Action.Types.MOVE_CARD: {
const fromListName = sails.helpers.lists.makeName(action.data.fromList);
const toListName = sails.helpers.lists.makeName(action.data.toList);
const fromListName = sails.helpers.lists.resolveName(action.data.fromList, t);
const toListName = sails.helpers.lists.resolveName(action.data.toList, t);
return {
text: t(

View File

@@ -17,7 +17,7 @@ module.exports = {
async fn(inputs) {
return List.qm.getByBoardId(inputs.id, {
exceptIdOrIds: inputs.exceptListIdOrIds,
typeOrTypes: List.FINITE_TYPES,
typeOrTypes: List.KANBAN_TYPES,
});
},
};

View File

@@ -41,12 +41,26 @@ module.exports = {
async fn(inputs) {
const { values } = inputs;
if (sails.helpers.lists.isFinite(inputs.list)) {
if (values.list) {
const typeState = List.TYPE_STATE_BY_TYPE[values.list.type];
if (inputs.record.isClosed) {
if (typeState === List.TypeStates.OPENED) {
values.isClosed = false;
}
} else if (typeState === List.TypeStates.CLOSED) {
values.isClosed = true;
}
}
const list = values.list || inputs.list;
if (sails.helpers.lists.isFinite(list)) {
if (_.isUndefined(values.position)) {
throw 'positionMustBeInValues';
}
const cards = await Card.qm.getByListId(inputs.list.id);
const cards = await Card.qm.getByListId(list.id);
const { position, repositions } = sails.helpers.utils.insertToPositionables(
values.position,
@@ -84,7 +98,6 @@ module.exports = {
let card = await Card.qm.createOne({
..._.pick(inputs.record, [
'boardId',
'listId',
'prevListId',
'type',
'name',
@@ -95,6 +108,7 @@ module.exports = {
'isClosed',
]),
...values,
listId: list.id,
creatorUserId: values.creatorUser.id,
listChangedAt: new Date().toISOString(),
});
@@ -240,7 +254,7 @@ module.exports = {
included: {
projects: [inputs.project],
boards: [inputs.board],
lists: [inputs.list],
lists: [list],
cardMemberships: nextCardMemberships,
cardLabels: nextCardLabels,
taskLists: nextTaskLists,
@@ -277,19 +291,19 @@ module.exports = {
}
await sails.helpers.actions.createOne.with({
list,
webhooks,
values: {
card,
type: Action.Types.CREATE_CARD, // TODO: introduce separate type?
data: {
card: _.pick(card, ['name']),
list: _.pick(inputs.list, ['id', 'type', 'name']),
list: _.pick(list, ['id', 'type', 'name']),
},
user: values.creatorUser,
},
project: inputs.project,
board: inputs.board,
list: inputs.list,
});
return {

View File

@@ -25,7 +25,7 @@ module.exports = {
async fn(inputs) {
const { values } = inputs;
const lists = await sails.helpers.boards.getFiniteListsById(values.board.id);
const lists = await sails.helpers.boards.getKanbanListsById(values.board.id);
const { position, repositions } = sails.helpers.utils.insertToPositionables(
values.position,

View File

@@ -3,7 +3,6 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
// TODO: rename?
module.exports = {
sync: true,
@@ -15,6 +14,6 @@ module.exports = {
},
fn(inputs) {
return inputs.record.name || _.upperFirst(inputs.record.type);
return List.KANBAN_TYPES.includes(inputs.record.type);
},
};

View File

@@ -0,0 +1,27 @@
/*!
* 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: {
record: {
type: 'ref',
required: true,
},
t: {
type: 'ref',
},
},
fn(inputs) {
if (inputs.record.name) {
return inputs.record.name;
}
const name = _.upperFirst(inputs.record.type);
return inputs.t ? inputs.t(name) : name;
},
};

View File

@@ -58,7 +58,7 @@ module.exports = {
const board = values.board || inputs.board;
if (!_.isUndefined(values.position)) {
const lists = await sails.helpers.boards.getFiniteListsById(board.id, inputs.record.id);
const lists = await sails.helpers.boards.getKanbanListsById(board.id, inputs.record.id);
const { position, repositions } = sails.helpers.utils.insertToPositionables(
values.position,

View File

@@ -29,8 +29,8 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
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);
const fromListName = sails.helpers.lists.resolveName(notification.data.fromList, t);
const toListName = sails.helpers.lists.resolveName(notification.data.toList, t);
return {
text: t(
@@ -144,8 +144,8 @@ const buildEmail = (board, card, notification, actorUser, notifiableUser, t) =>
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);
const fromListName = sails.helpers.lists.resolveName(notification.data.fromList, t);
const toListName = sails.helpers.lists.resolveName(notification.data.toList, t);
html = `<p>${t(
'%s moved %s from %s to %s on %s',
@@ -243,10 +243,6 @@ module.exports = {
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,
@@ -254,10 +250,10 @@ module.exports = {
boardId: values.card.boardId,
cardId: values.card.id,
};
if (isCommentRelated) {
if (values.comment) {
nextValues.commentId = values.comment.id;
} else {
}
if (values.action) {
nextValues.actionId = values.action.id;
}

View File

@@ -29,8 +29,8 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
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);
const fromListName = sails.helpers.lists.resolveName(notification.data.fromList, t);
const toListName = sails.helpers.lists.resolveName(notification.data.toList, t);
return {
text: t(
@@ -152,8 +152,8 @@ const buildAndSendEmail = async (
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);
const fromListName = sails.helpers.lists.resolveName(notification.data.fromList, t);
const toListName = sails.helpers.lists.resolveName(notification.data.toList, t);
html = `<p>${t(
'%s moved %s from %s to %s on %s',
@@ -234,13 +234,11 @@ module.exports = {
async fn(inputs) {
const { values } = inputs;
const isCommentRelated =
values.type === Notification.Types.COMMENT_CARD ||
values.type === Notification.Types.MENTION_IN_COMMENT;
if (isCommentRelated) {
if (values.comment) {
values.commentId = values.comment.id;
} else {
}
if (values.action) {
values.actionId = values.action.id;
}
@@ -268,13 +266,12 @@ module.exports = {
boards: [inputs.board],
lists: [inputs.list],
cards: [values.card],
...(isCommentRelated
? {
comments: [values.comment],
}
: {
actions: [values.action],
}),
...(values.comment && {
comments: [values.comment],
}),
...(values.action && {
actions: [values.action],
}),
},
}),
user: values.creatorUser,

View File

@@ -7,6 +7,7 @@ module.exports = {
inputs: {
roleOrRoles: {
type: 'json',
required: true,
},
},

View File

@@ -3,6 +3,9 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const path = require('path');
const I18n = require('i18n-2');
module.exports = {
sync: true,
@@ -14,9 +17,21 @@ module.exports = {
},
fn(inputs) {
const i18n = _.cloneDeep(sails.hooks.i18n);
const i18n = new I18n({
locales: sails.config.i18n.locales,
defaultLocale: sails.config.i18n.defaultLocale,
directory: path.join(sails.config.appPath, sails.config.i18n.localesDirectory),
extension: '.json',
devMode: false,
});
i18n.setLocale(inputs.language || sails.config.i18n.defaultLocale);
return i18n.__.bind(i18n); // eslint-disable-line no-underscore-dangle
/* eslint-disable no-underscore-dangle */
const translator = i18n.__.bind(i18n);
translator.n = i18n.__n.bind(i18n);
/* eslint-enable no-underscore-dangle */
return translator;
},
};

View File

@@ -31,6 +31,14 @@ const makeWhereQueryBuilder = (Model) => (criteria) => {
return ['id = $1', [criteria]];
};
const makeRowToModelTransformer = (Model) => {
// eslint-disable-next-line no-underscore-dangle
const transformations = _.invert(Model._transformer._transformations);
return (row) => _.mapKeys(row, (_, key) => transformations[key]);
};
module.exports = {
makeWhereQueryBuilder,
makeRowToModelTransformer,
};

View File

@@ -172,16 +172,7 @@ const delete_ = (criteria) =>
query += `END END, updated_at = $${queryValues.length} WHERE id IN (${inValues.join(', ')}) AND references_total IS NOT NULL RETURNING *`;
const queryResult = await sails.sendNativeQuery(query, queryValues).usingConnection(db);
uploadedFiles = queryResult.rows.map((row) => ({
id: row.id,
type: row.type,
mimeType: row.mime_type,
size: row.size,
referencesTotal: row.references_total,
createdAt: row.created_at,
updatedAt: row.updated_at,
}));
uploadedFiles = queryResult.rows.map((row) => UploadedFile.qm.transformRowToModel(row));
}
return { attachments, uploadedFiles };
@@ -200,17 +191,7 @@ const deleteOne = (criteria) =>
)
.usingConnection(db);
const [row] = queryResult.rows;
uploadedFile = {
id: row.id,
type: row.type,
mimeType: row.mime_type,
size: row.size,
referencesTotal: row.references_total,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
uploadedFile = UploadedFile.qm.transformRowToModel(queryResult.rows[0]);
}
return { attachment, uploadedFile };

View File

@@ -105,16 +105,7 @@ const delete_ = (criteria) =>
query += `END END, updated_at = $${queryValues.length} WHERE id IN (${inValues.join(', ')}) AND references_total IS NOT NULL RETURNING *`;
const queryResult = await sails.sendNativeQuery(query, queryValues).usingConnection(db);
uploadedFiles = queryResult.rows.map((row) => ({
id: row.id,
type: row.type,
mimeType: row.mime_type,
size: row.size,
referencesTotal: row.references_total,
createdAt: row.created_at,
updatedAt: row.updated_at,
}));
uploadedFiles = queryResult.rows.map((row) => UploadedFile.qm.transformRowToModel(row));
}
return { backgroundImages, uploadedFiles };
@@ -131,17 +122,7 @@ const deleteOne = (criteria) =>
)
.usingConnection(db);
const [row] = queryResult.rows;
uploadedFile = {
id: row.id,
type: row.type,
mimeType: row.mime_type,
size: row.size,
referencesTotal: row.references_total,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
const uploadedFile = UploadedFile.qm.transformRowToModel(queryResult.rows[0]);
return { backgroundImage, uploadedFile };
});

View File

@@ -4,112 +4,17 @@
*/
const buildSearchParts = require('../../../../utils/build-query-parts');
const { makeRowToModelTransformer } = require('../helpers');
const LIMIT = 50;
const transformRowToModel = makeRowToModelTransformer(Card);
const defaultFind = (criteria, { sort = 'id', limit } = {}) =>
Card.find(criteria).sort(sort).limit(limit);
/* Query methods */
const getIdsByEndlessListId = async (
listId,
{ before, search, filterUserIds, filterLabelIds } = {},
) => {
if (filterUserIds && filterUserIds.length === 0) {
return [];
}
if (filterLabelIds && filterLabelIds.length === 0) {
return [];
}
const queryValues = [];
let query = 'SELECT DISTINCT card.id FROM card';
if (filterUserIds) {
query += ' LEFT JOIN card_membership ON card.id = card_membership.card_id';
query += ' LEFT JOIN task_list ON card.id = task_list.card_id';
query += ' LEFT JOIN task ON task_list.id = task.task_list_id';
}
if (filterLabelIds) {
query += ' LEFT JOIN card_label ON card.id = card_label.card_id';
}
queryValues.push(listId);
query += ` WHERE card.list_id = $${queryValues.length}`;
if (before) {
queryValues.push(before.listChangedAt);
query += ` AND (card.list_changed_at < $${queryValues.length} OR (card.list_changed_at = $${queryValues.length}`;
queryValues.push(before.id);
query += ` AND card.id < $${queryValues.length}))`;
}
if (search) {
if (search.startsWith('/')) {
queryValues.push(search.substring(1));
query += ` AND (card.name ~* $${queryValues.length} OR card.description ~* $${queryValues.length})`;
} else {
const searchParts = buildSearchParts(search);
if (searchParts.length > 0) {
let ilikeValues = searchParts.map((searchPart) => {
queryValues.push(searchPart);
return `'%' || $${queryValues.length} || '%'`;
});
query += ` AND ((card.name ILIKE ALL(ARRAY[${ilikeValues.join(', ')}]))`;
ilikeValues = searchParts.map((searchPart) => {
queryValues.push(searchPart);
return `'%' || $${queryValues.length} || '%'`;
});
query += ` OR (card.description ILIKE ALL(ARRAY[${ilikeValues.join(', ')}])))`;
}
}
}
if (filterUserIds) {
const inValues = filterUserIds.map((filterUserId) => {
queryValues.push(filterUserId);
return `$${queryValues.length}`;
});
query += ` AND (card_membership.user_id IN (${inValues.join(', ')}) OR task.assignee_user_id IN (${inValues.join(', ')}))`;
}
if (filterLabelIds) {
const inValues = filterLabelIds.map((filterLabelId) => {
queryValues.push(filterLabelId);
return `$${queryValues.length}`;
});
query += ` AND card_label.label_id IN (${inValues.join(', ')})`;
}
query += ` LIMIT ${LIMIT}`;
let queryResult;
try {
queryResult = await sails.sendNativeQuery(query, queryValues);
} catch (error) {
if (
error.code === 'E_QUERY_FAILED' &&
error.message.includes('Query failed: invalid regular expression')
) {
return [];
}
throw error;
}
return sails.helpers.utils.mapRecords(queryResult.rows);
};
const createOne = (values) => Card.create({ ...values }).fetch();
const getByIds = (ids) => defaultFind(ids);
@@ -133,45 +38,121 @@ const getByListId = async (listId, { exceptIdOrIds, sort = ['position', 'id'] }
return defaultFind(criteria, { sort });
};
const getByEndlessListId = async (listId, { before, search, filterUserIds, filterLabelIds }) => {
const criteria = {};
const options = {
sort: ['listChangedAt DESC', 'id DESC'],
};
if (search || filterUserIds || filterLabelIds) {
criteria.id = await getIdsByEndlessListId(listId, {
before,
search,
filterUserIds,
filterLabelIds,
});
} else {
criteria.and = [{ listId }];
if (before) {
criteria.and.push({
or: [
{
listChangedAt: {
'<': before.listChangedAt,
},
},
{
listChangedAt: before.listChangedAt,
id: {
'<': before.id,
},
},
],
});
const getByEndlessListId = async (listId, { before, search, userIds, labelIds }) => {
if (search || userIds || labelIds) {
if (userIds && userIds.length === 0) {
return [];
}
options.limit = LIMIT;
if (labelIds && labelIds.length === 0) {
return [];
}
const queryValues = [];
let query = 'SELECT DISTINCT card.* FROM card';
if (userIds) {
query += ' LEFT JOIN card_membership ON card.id = card_membership.card_id';
query += ' LEFT JOIN task_list ON card.id = task_list.card_id';
query += ' LEFT JOIN task ON task_list.id = task.task_list_id';
}
if (labelIds) {
query += ' LEFT JOIN card_label ON card.id = card_label.card_id';
}
queryValues.push(listId);
query += ` WHERE card.list_id = $${queryValues.length}`;
if (before) {
queryValues.push(before.listChangedAt);
query += ` AND (card.list_changed_at < $${queryValues.length} OR (card.list_changed_at = $${queryValues.length}`;
queryValues.push(before.id);
query += ` AND card.id < $${queryValues.length}))`;
}
if (search) {
if (search.startsWith('/')) {
queryValues.push(search.substring(1));
query += ` AND (card.name ~* $${queryValues.length} OR card.description ~* $${queryValues.length})`;
} else {
const searchParts = buildSearchParts(search);
if (searchParts.length > 0) {
const ilikeValues = searchParts.map((searchPart) => {
queryValues.push(searchPart);
return `'%' || $${queryValues.length} || '%'`;
});
query += ` AND ((card.name ILIKE ALL(ARRAY[${ilikeValues.join(', ')}])) OR (card.description ILIKE ALL(ARRAY[${ilikeValues.join(', ')}])))`;
}
}
}
if (userIds) {
const inValues = userIds.map((userId) => {
queryValues.push(userId);
return `$${queryValues.length}`;
});
query += ` AND (card_membership.user_id IN (${inValues.join(', ')}) OR task.assignee_user_id IN (${inValues.join(', ')}))`;
}
if (labelIds) {
const inValues = labelIds.map((labelId) => {
queryValues.push(labelId);
return `$${queryValues.length}`;
});
query += ` AND card_label.label_id IN (${inValues.join(', ')})`;
}
query += ` LIMIT ${LIMIT}`;
let queryResult;
try {
queryResult = await sails.sendNativeQuery(query, queryValues);
} catch (error) {
if (
error.code === 'E_QUERY_FAILED' &&
error.message.includes('Query failed: invalid regular expression')
) {
return [];
}
throw error;
}
return queryResult.rows.map((row) => transformRowToModel(row));
}
return defaultFind(criteria, options);
const criteria = {
and: [{ listId }],
};
if (before) {
criteria.and.push({
or: [
{
listChangedAt: {
'<': before.listChangedAt,
},
},
{
listChangedAt: before.listChangedAt,
id: {
'<': before.id,
},
},
],
});
}
return defaultFind(criteria, {
sort: ['listChangedAt DESC', 'id DESC'],
limit: LIMIT,
});
};
const getByListIds = async (listIds, { sort = ['position', 'id'] } = {}) =>
@@ -252,8 +233,6 @@ const delete_ = (criteria) => Card.destroy(criteria).fetch();
const deleteOne = (criteria) => Card.destroyOne(criteria);
module.exports = {
getIdsByEndlessListId,
createOne,
getByIds,
getByBoardId,

View File

@@ -3,6 +3,10 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const { makeRowToModelTransformer } = require('../helpers');
const transformRowToModel = makeRowToModelTransformer(CustomFieldValue);
const defaultFind = (criteria, { customFieldGroupIdOrIds }) => {
if (customFieldGroupIdOrIds) {
criteria.customFieldGroupId = customFieldGroupIdOrIds; // eslint-disable-line no-param-reassign
@@ -32,17 +36,7 @@ const createOrUpdateOne = async (values) => {
new Date().toISOString(),
]);
const [row] = queryResult.rows;
return {
id: row.id,
cardId: row.card_id,
customFieldGroupId: row.custom_field_group_id,
customFieldId: row.custom_field_id,
content: row.content,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
return transformRowToModel(queryResult.rows[0]);
};
const getByIds = (ids) => defaultFind(ids);

View File

@@ -3,9 +3,10 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const { makeWhereQueryBuilder } = require('../helpers');
const { makeRowToModelTransformer, makeWhereQueryBuilder } = require('../helpers');
const buildWhereQuery = makeWhereQueryBuilder(List);
const transformRowToModel = makeRowToModelTransformer(List);
const defaultFind = (criteria, { sort = 'id' } = {}) => List.find(criteria).sort(sort);
@@ -67,10 +68,7 @@ const updateOne = async (criteria, values) => {
return { list: null };
}
const prev = {
boardId: queryResult.rows[0].board_id,
type: queryResult.rows[0].type,
};
const prev = transformRowToModel(queryResult.rows[0]);
const list = await List.updateOne(criteria)
.set({ ...values })

View File

@@ -3,12 +3,16 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const { makeRowToModelTransformer } = require('../helpers');
const COLUMN_NAME_BY_TYPE = {
[UploadedFile.Types.USER_AVATAR]: 'user_avatars',
[UploadedFile.Types.BACKGROUND_IMAGE]: 'background_images',
[UploadedFile.Types.ATTACHMENT]: 'attachments',
};
const transformRowToModel = makeRowToModelTransformer(UploadedFile);
/* Query methods */
const createOne = (values) =>
@@ -47,4 +51,6 @@ const deleteOne = (criteria) =>
module.exports = {
createOne,
deleteOne,
transformRowToModel,
};

View File

@@ -3,7 +3,7 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const { makeWhereQueryBuilder } = require('../helpers');
const { makeRowToModelTransformer, makeWhereQueryBuilder } = require('../helpers');
const hasAvatarChanged = (avatar, prevAvatar) => {
if (!avatar && !prevAvatar) {
@@ -18,6 +18,7 @@ const hasAvatarChanged = (avatar, prevAvatar) => {
};
const buildWhereQuery = makeWhereQueryBuilder(User);
const transformRowToModel = makeRowToModelTransformer(User);
const defaultFind = (criteria) => User.find(criteria).sort('id');
@@ -117,9 +118,7 @@ const updateOne = async (criteria, values) => {
return { user: null };
}
prev = {
avatar: queryResult.rows[0].avatar,
};
prev = transformRowToModel(queryResult.rows[0]);
}
const user = await User.updateOne(criteria)
@@ -136,17 +135,7 @@ const updateOne = async (criteria, values) => {
)
.usingConnection(db);
const [row] = queryResult.rows;
uploadedFile = {
id: row.id,
type: row.type,
mimeType: row.mime_type,
size: row.size,
referencesTotal: row.references_total,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
uploadedFile = UploadedFile.qm.transformRowToModel(queryResult.rows[0]);
}
if (user.avatar) {
@@ -184,17 +173,7 @@ const deleteOne = (criteria) =>
)
.usingConnection(db);
const [row] = queryResult.rows;
uploadedFile = {
id: row.id,
type: row.type,
mimeType: row.mime_type,
size: row.size,
referencesTotal: row.references_total,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
uploadedFile = UploadedFile.qm.transformRowToModel(queryResult.rows[0]);
}
return { user, uploadedFile };

View File

@@ -95,6 +95,8 @@ const SortOrders = {
const FINITE_TYPES = [Types.ACTIVE, Types.CLOSED];
const KANBAN_TYPES = [Types.ACTIVE, Types.CLOSED];
const TYPE_STATE_BY_TYPE = {
[Types.ACTIVE]: TypeStates.OPENED,
[Types.CLOSED]: Types.CLOSED,
@@ -119,6 +121,7 @@ module.exports = {
SortFieldNames,
SortOrders,
FINITE_TYPES,
KANBAN_TYPES,
TYPE_STATE_BY_TYPE,
COLORS,

View File

@@ -1,4 +1,5 @@
{
"Archive": "أرشيف",
"Card Created": "تم إنشاء البطاقة",
"Card Moved": "تم نقل البطاقة",
"New Comment": "تعليق جديد",
@@ -6,6 +7,7 @@
"This is a test text message!": "هذه رسالة نصية تجريبية!",
"This is a *test* **markdown** `message`!": "هذه *رسالة* **markdown** `تجريبية`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "هذه <i>رسالة</i> <b>html</b> <code>تجريبية</code>!",
"Trash": "سلة المهملات",
"You Were Added to Card": "تمت إضافتك إلى البطاقة",
"You Were Mentioned in Comment": "تم ذكرك في تعليق",
"%s added you to %s on %s": "%s أضافك إلى %s في %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Архив",
"Card Created": "Картата е създадена",
"Card Moved": "Картата е преместена",
"New Comment": "Нов коментар",
@@ -6,6 +7,7 @@
"This is a test text message!": "Това е тестово текстово съобщение!",
"This is a *test* **markdown** `message`!": "Това е *тестово* **markdown** `съобщение`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Това е <i>тестово</i> <b>html</b> <code>съобщение</code>!",
"Trash": "Кошче",
"You Were Added to Card": "Бяхте добавени към картата",
"You Were Mentioned in Comment": "Бяхте споменати в коментар",
"%s added you to %s on %s": "%s ви добави към %s на %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Arxivar",
"Card Created": "Targeta creada",
"Card Moved": "Targeta moguda",
"New Comment": "Comentari nou",
@@ -6,6 +7,7 @@
"This is a test text message!": "Aquest és un missatge de text de prova!",
"This is a *test* **markdown** `message`!": "Aquest és un *missatge* **markdown** `de prova`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Aquest és un <i>missatge</i> <b>html</b> <code>de prova</code>!",
"Trash": "Paperera",
"You Were Added to Card": "Has estat afegit a la targeta",
"You Were Mentioned in Comment": "Has estat mencionat en un comentari",
"%s added you to %s on %s": "%s t'ha afegit a %s el %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Archiv",
"Card Created": "Karta vytvořena",
"Card Moved": "Karta přesunuta",
"New Comment": "Nový komentář",
@@ -6,6 +7,7 @@
"This is a test text message!": "Toto je testovací textová zpráva!",
"This is a *test* **markdown** `message`!": "Toto je *testovací* **markdown** `zpráva`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Toto je <i>testovací</i> <b>html</b> <code>zpráva</code>!",
"Trash": "Koš",
"You Were Added to Card": "Byli jste přidáni ke kartě",
"You Were Mentioned in Comment": "Byli jste zmíněni v komentáři",
"%s added you to %s on %s": "%s vás přidal k %s dne %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Arkiv",
"Card Created": "Kort oprettet",
"Card Moved": "Kort flyttet",
"New Comment": "Ny kommentar",
@@ -6,6 +7,7 @@
"This is a test text message!": "Dette er en test tekstbesked!",
"This is a *test* **markdown** `message`!": "Dette er en *test* **markdown** `besked`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Dette er en <i>test</i> <b>html</b> <code>besked</code>!",
"Trash": "Papirkurv",
"You Were Added to Card": "Du blev tilføjet til kortet",
"You Were Mentioned in Comment": "Du blev nævnt i en kommentar",
"%s added you to %s on %s": "%s tilføjede dig til %s den %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Archiv",
"Card Created": "Karte erstellt",
"Card Moved": "Karte verschoben",
"New Comment": "Neuer Kommentar",
@@ -6,6 +7,7 @@
"This is a test text message!": "Dies ist eine Test-Textnachricht!",
"This is a *test* **markdown** `message`!": "Dies ist eine *Test*-**Markdown**-`Nachricht`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Dies ist eine <i>Test</i>-<b>HTML</b>-<code>Nachricht</code>!",
"Trash": "Papierkorb",
"You Were Added to Card": "Sie wurden zur Karte hinzugefügt",
"You Were Mentioned in Comment": "Sie wurden in einem Kommentar erwähnt",
"%s added you to %s on %s": "%s hat Sie zu %s am %s hinzugefügt",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Αρχείο",
"Card Created": "Η κάρτα δημιουργήθηκε",
"Card Moved": "Η κάρτα μετακινήθηκε",
"New Comment": "Νέο σχόλιο",
@@ -6,6 +7,7 @@
"This is a test text message!": "Αυτό είναι ένα δοκιμαστικό μήνυμα!",
"This is a *test* **markdown** `message`!": "Αυτό είναι ένα *δοκιμαστικό* **markdown** `μήνυμα`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Αυτό είναι ένα <i>δοκιμαστικό</i> <b>html</b> <code>μήνυμα</code>!",
"Trash": "Κάδος απορριμμάτων",
"You Were Added to Card": "Προστέθηκες στην κάρτα",
"You Were Mentioned in Comment": "Αναφέρθηκες σε σχόλιο",
"%s added you to %s on %s": "%s σε πρόσθεσε στο %s στο %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Archive",
"Card Created": "Card Created",
"Card Moved": "Card Moved",
"New Comment": "New Comment",
@@ -6,6 +7,7 @@
"This is a test text message!": "This is a test text message!",
"This is a *test* **markdown** `message`!": "This is a *test* **markdown** `message`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "This is a <i>test</i> <b>html</b> <code>message</code>!",
"Trash": "Trash",
"You Were Added to Card": "You Were Added to Card",
"You Were Mentioned in Comment": "You Were Mentioned in Comment",
"%s added you to %s on %s": "%s added you to %s on %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Archive",
"Card Created": "Card Created",
"Card Moved": "Card Moved",
"New Comment": "New Comment",
@@ -6,6 +7,7 @@
"This is a test text message!": "This is a test text message!",
"This is a *test* **markdown** `message`!": "This is a *test* **markdown** `message`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "This is a <i>test</i> <b>html</b> <code>message</code>!",
"Trash": "Trash",
"You Were Added to Card": "You Were Added to Card",
"You Were Mentioned in Comment": "You Were Mentioned in Comment",
"%s added you to %s on %s": "%s added you to %s on %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Archivo",
"Card Created": "Tarjeta creada",
"Card Moved": "Tarjeta movida",
"New Comment": "Nuevo comentario",
@@ -6,6 +7,7 @@
"This is a test text message!": "¡Este es un mensaje de texto de prueba!",
"This is a *test* **markdown** `message`!": "¡Este es un *mensaje* **markdown** `de prueba`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "¡Este es un <i>mensaje</i> <b>html</b> <code>de prueba</code>!",
"Trash": "Papelera",
"You Were Added to Card": "Fuiste añadido a la tarjeta",
"You Were Mentioned in Comment": "Fuiste mencionado en un comentario",
"%s added you to %s on %s": "%s te añadió a %s en %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Arhiiv",
"Card Created": "Kaart loodud",
"Card Moved": "Kaart liigutatud",
"New Comment": "Uus kommentaar",
@@ -6,6 +7,7 @@
"This is a test text message!": "See on testi tekstisõnum!",
"This is a *test* **markdown** `message`!": "See on *testi* **markdown** `sõnum`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "See on <i>testi</i> <b>html</b> <code>sõnum</code>!",
"Trash": "Prügikast",
"You Were Added to Card": "Teid lisati kaardile",
"You Were Mentioned in Comment": "Teid mainiti kommentaaris",
"%s added you to %s on %s": "%s lisas teid %s-le %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "بایگانی",
"Card Created": "کارت ایجاد شد",
"Card Moved": "کارت منتقل شد",
"New Comment": "نظر جدید",
@@ -6,6 +7,7 @@
"This is a test text message!": "این یک پیام متنی آزمایشی است!",
"This is a *test* **markdown** `message`!": "این یک *پیام* **markdown** `آزمایشی` است!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "این یک <i>پیام</i> <b>html</b> <code>آزمایشی</code> است!",
"Trash": "سطل زباله",
"You Were Added to Card": "شما به کارت اضافه شدید",
"You Were Mentioned in Comment": "در نظری از شما نام برده شد",
"%s added you to %s on %s": "%s شما را به %s در %s اضافه کرد",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Arkisto",
"Card Created": "Kortti luotu",
"Card Moved": "Kortti siirretty",
"New Comment": "Uusi kommentti",
@@ -6,6 +7,7 @@
"This is a test text message!": "Tämä on testiviesti!",
"This is a *test* **markdown** `message`!": "Tämä on *testi* **markdown** `viesti`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Tämä on <i>testi</i> <b>html</b> <code>viesti</code>!",
"Trash": "Roskakori",
"You Were Added to Card": "Sinut lisättiin korttiin",
"You Were Mentioned in Comment": "Sinut mainittiin kommentissa",
"%s added you to %s on %s": "%s lisäsi sinut kohteeseen %s kohteessa %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Archive",
"Card Created": "Carte créée",
"Card Moved": "Carte déplacée",
"New Comment": "Nouveau commentaire",
@@ -6,6 +7,7 @@
"This is a test text message!": "Ceci est un message texte de test !",
"This is a *test* **markdown** `message`!": "Ceci est un *message* **markdown** `de test` !",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Ceci est un <i>test</i> <b>html</b> <code>message</code>!",
"Trash": "Corbeille",
"You Were Added to Card": "Vous avez été ajouté à la carte",
"You Were Mentioned in Comment": "Vous avez été mentionné dans un commentaire",
"%s added you to %s on %s": "%s vous a ajouté à %s le %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Archívum",
"Card Created": "Kártya létrehozva",
"Card Moved": "Kártya áthelyezve",
"New Comment": "Új hozzászólás",
@@ -6,6 +7,7 @@
"This is a test text message!": "Ez itt egy szöveges teszt üzenet!",
"This is a *test* **markdown** `message`!": "Ez itt egy **markdown** formátumú *teszt* `üzenet`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Ez egy <b>html</b> formátumú <i>teszt</i> <code>üzenet</code>!",
"Trash": "Kuka",
"You Were Added to Card": "Hozzádrendeltek egy kártyát",
"You Were Mentioned in Comment": "Megemlítettek egy kártyán",
"%s added you to %s on %s": "%s hozzárendelt téged a(z) %s kártyához a(z) %s projektben",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Arsip",
"Card Created": "Kartu dibuat",
"Card Moved": "Kartu dipindahkan",
"New Comment": "Komentar baru",
@@ -6,6 +7,7 @@
"This is a test text message!": "Ini adalah pesan teks tes!",
"This is a *test* **markdown** `message`!": "Ini adalah *pesan* **markdown** `tes`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Ini adalah <i>pesan</i> <b>html</b> <code>tes</code>!",
"Trash": "Sampah",
"You Were Added to Card": "Anda ditambahkan ke kartu",
"You Were Mentioned in Comment": "Anda disebutkan dalam komentar",
"%s added you to %s on %s": "%s menambahkan Anda ke %s pada %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Archivio",
"Card Created": "Nuova task creata",
"Card Moved": "Task spostata",
"New Comment": "Nuovo commento",
@@ -6,6 +7,7 @@
"This is a test text message!": "Questo è un messaggio di testo di test!",
"This is a *test* **markdown** `message`!": "Questo è un *test* **markdown** `messaggio`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Questo è un <i>test</i> <b>html</b> <code>messaggio</code>!",
"Trash": "Cestino",
"You Were Added to Card": "Sei stato aggiunto alla task",
"You Were Mentioned in Comment": "Sei stato menzionato nel commento",
"%s created %s in %s on %s": "%s ha creato %s in %s in %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "アーカイブ",
"Card Created": "カードが作成されました",
"Card Moved": "カードが移動されました",
"New Comment": "新しいコメント",
@@ -6,6 +7,7 @@
"This is a test text message!": "これはテストテキストメッセージです!",
"This is a *test* **markdown** `message`!": "これは*テスト***markdown**`メッセージ`です!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "これは<i>テスト</i><b>html</b><code>メッセージ</code>です!",
"Trash": "ゴミ箱",
"You Were Added to Card": "カードに追加されました",
"You Were Mentioned in Comment": "コメントでメンションされました",
"%s added you to %s on %s": "%sが%sの%sにあなたを追加しました",

View File

@@ -1,4 +1,5 @@
{
"Archive": "보관함",
"Card Created": "카드가 생성됨",
"Card Moved": "카드가 이동됨",
"New Comment": "새 댓글",
@@ -6,6 +7,7 @@
"This is a test text message!": "이것은 테스트 텍스트 메시지입니다!",
"This is a *test* **markdown** `message`!": "이것은 *테스트* **markdown** `메시지`입니다!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "이것은 <i>테스트</i> <b>html</b> <code>메시지</code>입니다!",
"Trash": "휴지통",
"You Were Added to Card": "카드에 추가되었습니다",
"You Were Mentioned in Comment": "댓글에서 언급되었습니다",
"%s added you to %s on %s": "%s님이 %s의 %s에 당신을 추가했습니다",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Archief",
"Card Created": "Kaart aangemaakt",
"Card Moved": "Kaart verplaatst",
"New Comment": "Nieuwe reactie",
@@ -6,6 +7,7 @@
"This is a test text message!": "Dit is een test tekstbericht!",
"This is a *test* **markdown** `message`!": "Dit is een *test* **markdown** `bericht`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Dit is een <i>test</i> <b>html</b> <code>bericht</code>!",
"Trash": "Prullenbak",
"You Were Added to Card": "Je bent toegevoegd aan kaart",
"You Were Mentioned in Comment": "Je bent genoemd in reactie",
"%s added you to %s on %s": "%s heeft je toegevoegd aan %s op %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Archiwum",
"Card Created": "Karta utworzona",
"Card Moved": "Karta przeniesiona",
"New Comment": "Nowy komentarz",
@@ -6,6 +7,7 @@
"This is a test text message!": "To jest testowa wiadomość tekstowa!",
"This is a *test* **markdown** `message`!": "To jest *testowa* **markdown** `wiadomość`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "To jest <i>testowa</i> <b>html</b> <code>wiadomość</code>!",
"Trash": "Kosz",
"You Were Added to Card": "Zostałeś dodany do karty",
"You Were Mentioned in Comment": "Zostałeś wspomniany w komentarzu",
"%s added you to %s on %s": "%s dodał cię do %s w dniu %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Arquivo",
"Card Created": "Cartão criado",
"Card Moved": "Cartão movido",
"New Comment": "Novo comentário",
@@ -6,6 +7,7 @@
"This is a test text message!": "Esta é uma mensagem de texto de teste!",
"This is a *test* **markdown** `message`!": "Esta é uma *mensagem* **markdown** `de teste`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Esta é uma <i>mensagem</i> <b>html</b> <code>de teste</code>!",
"Trash": "Lixeira",
"You Were Added to Card": "Você foi adicionado ao cartão",
"You Were Mentioned in Comment": "Você foi mencionado em comentário",
"%s added you to %s on %s": "%s adicionou você a %s em %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Arquivo",
"Card Created": "Cartão criado",
"Card Moved": "Cartão movido",
"New Comment": "Novo comentário",
@@ -6,6 +7,7 @@
"This is a test text message!": "Esta é uma mensagem de texto de teste!",
"This is a *test* **markdown** `message`!": "Esta é uma *mensagem* **markdown** `de teste`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Esta é uma <i>mensagem</i> <b>html</b> <code>de teste</code>!",
"Trash": "Lixo",
"You Were Added to Card": "Foi adicionado ao cartão",
"You Were Mentioned in Comment": "Foi mencionado num comentário",
"%s added you to %s on %s": "%s adicionou-o a %s em %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Arhivă",
"Card Created": "Card creat",
"Card Moved": "Card mutat",
"New Comment": "Comentariu nou",
@@ -6,6 +7,7 @@
"This is a test text message!": "Acesta este un mesaj text de test!",
"This is a *test* **markdown** `message`!": "Acesta este un *mesaj* **markdown** `de test`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Acesta este un <i>mesaj</i> <b>html</b> <code>de test</code>!",
"Trash": "Coș de gunoi",
"You Were Added to Card": "Ați fost adăugat la card",
"You Were Mentioned in Comment": "Ați fost menționat într-un comentariu",
"%s added you to %s on %s": "%s v-a adăugat la %s pe %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Архив",
"Card Created": "Карточка создана",
"Card Moved": "Карточка перемещена",
"New Comment": "Новый комментарий",
@@ -6,6 +7,7 @@
"This is a test text message!": "Это тестовое сообщение!",
"This is a *test* **markdown** `message`!": "Это *тестовое* **markdown** `сообщение`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Это <i>тестовое</i> <b>html</b> <code>сообщение</code>!",
"Trash": "Корзина",
"You Were Added to Card": "Вы были добавлены к карточке",
"You Were Mentioned in Comment": "Вы были упомянуты в комментарии",
"%s added you to %s on %s": "%s добавил(а) вас к %s на %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Archív",
"Card Created": "Karta vytvorená",
"Card Moved": "Karta presunutá",
"New Comment": "Nový komentár",
@@ -6,6 +7,7 @@
"This is a test text message!": "Toto je testovacia textová správa!",
"This is a *test* **markdown** `message`!": "Toto je *testovacia* **markdown** `správa`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Toto je <i>testovacia</i> <b>html</b> <code>správa</code>!",
"Trash": "Kôš",
"You Were Added to Card": "Boli ste pridaní ku karte",
"You Were Mentioned in Comment": "Boli ste spomenutí v komentári",
"%s added you to %s on %s": "%s vás pridal k %s dňa %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Архива",
"Card Created": "Картица креирана",
"Card Moved": "Картица премештена",
"New Comment": "Нови коментар",
@@ -6,6 +7,7 @@
"This is a test text message!": "Ово је тест текстуална порука!",
"This is a *test* **markdown** `message`!": "Ово је *тест* **markdown** `порука`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Ово је <i>тест</i> <b>html</b> <code>порука</code>!",
"Trash": "Корпа за отпатке",
"You Were Added to Card": "Додати сте на картицу",
"You Were Mentioned in Comment": "Поменути сте у коментару",
"%s added you to %s on %s": "%s вас је додао на %s дана %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Arhiva",
"Card Created": "Kartica kreirana",
"Card Moved": "Kartica premeštena",
"New Comment": "Novi komentar",
@@ -6,6 +7,7 @@
"This is a test text message!": "Ovo je test tekstualna poruka!",
"This is a *test* **markdown** `message`!": "Ovo je *test* **markdown** `poruka`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Ovo je <i>test</i> <b>html</b> <code>poruka</code>!",
"Trash": "Korpa za otpatke",
"You Were Added to Card": "Dodati ste na karticu",
"You Were Mentioned in Comment": "Pomenuti ste u komentaru",
"%s added you to %s on %s": "%s vas je dodao na %s dana %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Arkiv",
"Card Created": "Kort skapat",
"Card Moved": "Kort flyttat",
"New Comment": "Ny kommentar",
@@ -6,6 +7,7 @@
"This is a test text message!": "Detta är ett test textmeddelande!",
"This is a *test* **markdown** `message`!": "Detta är ett *test* **markdown** `meddelande`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Detta är ett <i>test</i> <b>html</b> <code>meddelande</code>!",
"Trash": "Papperskorg",
"You Were Added to Card": "Du lades till på kort",
"You Were Mentioned in Comment": "Du nämndes i kommentar",
"%s added you to %s on %s": "%s lade till dig på %s den %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Arşiv",
"Card Created": "Kart oluşturuldu",
"Card Moved": "Kart taşındı",
"New Comment": "Yeni yorum",
@@ -6,6 +7,7 @@
"This is a test text message!": "Bu bir test metin mesajıdır!",
"This is a *test* **markdown** `message`!": "Bu bir *test* **markdown** `mesajı`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Bu bir <i>test</i> <b>html</b> <code>mesajı</code>!",
"Trash": "Çöp kutusu",
"You Were Added to Card": "Karta eklendiniz",
"You Were Mentioned in Comment": "Bir yorumda bahsedildiniz",
"%s added you to %s on %s": "%s sizi %s'ye %s tarihinde ekledi",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Архів",
"Card Created": "Картку створено",
"Card Moved": "Картку переміщено",
"New Comment": "Новий коментар",
@@ -6,6 +7,7 @@
"This is a test text message!": "Це нове повідомлення!",
"This is a *test* **markdown** `message`!": "Це *тестове* **markdown** `повідомлення`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Це <i>тестове</i> <b>html</b> <code>повідомлення</code>!",
"Trash": "Кошик",
"You Were Added to Card": "Вас було додано до картки",
"You Were Mentioned in Comment": "Вас було згадано у коментарі",
"%s added you to %s on %s": "%s додав(ла) вас до %s на %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "Arxiv",
"Card Created": "Karta yaratildi",
"Card Moved": "Karta ko'chirildi",
"New Comment": "Yangi izoh",
@@ -6,6 +7,7 @@
"This is a test text message!": "Bu test matn xabari!",
"This is a *test* **markdown** `message`!": "Bu *test* **markdown** `xabar`!",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "Bu <i>test</i> <b>html</b> <code>xabar</code>!",
"Trash": "Chiqindi quti",
"You Were Added to Card": "Siz kartaga qo'shildingiz",
"You Were Mentioned in Comment": "Siz izohda eslatildingiz",
"%s added you to %s on %s": "%s sizni %s ga %s da qo'shdi",

View File

@@ -1,4 +1,5 @@
{
"Archive": "归档",
"Card Created": "卡片已创建",
"Card Moved": "卡片已移动",
"New Comment": "新评论",
@@ -6,6 +7,7 @@
"This is a test text message!": "这是一条测试文本消息!",
"This is a *test* **markdown** `message`!": "这是一条*测试***markdown**`消息`",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "这是一条<i>测试</i><b>html</b><code>消息</code>",
"Trash": "回收站",
"You Were Added to Card": "您已被添加到卡片",
"You Were Mentioned in Comment": "您在评论中被提及",
"%s added you to %s on %s": "%s 在 %s 将您添加到 %s",

View File

@@ -1,4 +1,5 @@
{
"Archive": "封存",
"Card Created": "卡片已建立",
"Card Moved": "卡片已移動",
"New Comment": "新留言",
@@ -6,6 +7,7 @@
"This is a test text message!": "這是一則測試文字訊息!",
"This is a *test* **markdown** `message`!": "這是一則*測試***markdown**`訊息`",
"This is a <i>test</i> <b>html</b> <code>message</code>!": "這是一則<i>測試</i><b>html</b><code>訊息</code>",
"Trash": "垃圾桶",
"You Were Added to Card": "您已被加入卡片",
"You Were Mentioned in Comment": "您在留言中被提及",
"%s added you to %s on %s": "%s 在 %s 將您加入 %s",

View File

@@ -0,0 +1,16 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
module.exports.up = (knex) =>
knex.schema.raw(`
ALTER INDEX card_name_index RENAME TO card_name_gin_index;
ALTER INDEX card_description_index RENAME TO card_description_gin_index;
`);
module.exports.down = (knex) =>
knex.schema.raw(`
ALTER INDEX card_name_gin_index RENAME TO card_name_index;
ALTER INDEX card_description_gin_index RENAME TO card_description_index;
`);

View File

@@ -16,6 +16,7 @@
"escape-html": "^1.0.3",
"escape-markdown": "^1.0.4",
"fs-extra": "^11.3.2",
"i18n-2": "^0.7.3",
"ico-to-png": "^0.2.2",
"istextorbinary": "^9.5.0",
"jsonwebtoken": "^9.0.2",

View File

@@ -52,6 +52,7 @@
"escape-html": "^1.0.3",
"escape-markdown": "^1.0.4",
"fs-extra": "^11.3.2",
"i18n-2": "^0.7.3",
"ico-to-png": "^0.2.2",
"istextorbinary": "^9.5.0",
"jsonwebtoken": "^9.0.2",

View File

@@ -16,6 +16,7 @@ const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/;
const isUrl = (value) =>
validator.isURL(value, {
protocols: ['http', 'https'],
require_tld: false,
require_protocol: true,
});