diff --git a/charts/planka/values.yaml b/charts/planka/values.yaml index e68785db..8b084ff8 100644 --- a/charts/planka/values.yaml +++ b/charts/planka/values.yaml @@ -214,19 +214,21 @@ oidc: ## extraEnv: [] -## Example extraEnv for configure SMTP +## Example extraEnv for configuring SMTP ## extraEnv: ## - name: SMTP_HOST -## value: "your_smtp_server" +## value: "smtp.example.com" ## - name: SMTP_PORT ## value: "587" +## - name: SMTP_NAME +## value: "Your Name" +## - name: SMTP_SECURE +## value: "true" ## - name: SMTP_USER ## value: "your_email@example.com" ## - name: SMTP_PASSWORD ## value: "your_password" ## - name: SMTP_FROM ## value: "your_email@example.com" -## - name: SMTP_NAME -## value: "your_name_or_login" -## - name: SMTP_SECURE +## - name: SMTP_TLS_REJECT_UNAUTHORIZED ## value: "false" diff --git a/client/src/components/cards/Card/TaskList/TaskList.jsx b/client/src/components/cards/Card/TaskList/TaskList.jsx index 7eda3773..e9497848 100644 --- a/client/src/components/cards/Card/TaskList/TaskList.jsx +++ b/client/src/components/cards/Card/TaskList/TaskList.jsx @@ -20,14 +20,14 @@ const TaskList = React.memo(({ id }) => { const tasks = useSelector((state) => selectTasksByTaskListId(state, id)); + const [isOpened, toggleOpened] = useToggle(); + // TODO: move to selector? const completedTasksTotal = useMemo( () => tasks.reduce((result, task) => (task.isCompleted ? result + 1 : result), 0), [tasks], ); - const [isOpened, toggleOpened] = useToggle(); - const handleToggleClick = useCallback( (event) => { event.stopPropagation(); diff --git a/client/src/constants/Enums.js b/client/src/constants/Enums.js index ca4704ba..3de0090e 100755 --- a/client/src/constants/Enums.js +++ b/client/src/constants/Enums.js @@ -71,6 +71,11 @@ export const ListTypes = { TRASH: 'trash', }; +export const ListTypeStates = { + OPENED: 'opened', + CLOSED: 'closed', +}; + export const ListSortFieldNames = { NAME: 'name', DUE_DATE: 'dueDate', diff --git a/client/src/constants/ListTypeStateByType.js b/client/src/constants/ListTypeStateByType.js new file mode 100644 index 00000000..345e6a8e --- /dev/null +++ b/client/src/constants/ListTypeStateByType.js @@ -0,0 +1,11 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import { ListTypes, ListTypeStates } from './Enums'; + +export default { + [ListTypes.ACTIVE]: ListTypeStates.OPENED, + [ListTypes.CLOSED]: ListTypeStates.CLOSED, +}; diff --git a/client/src/models/List.js b/client/src/models/List.js index 97b140d0..f27436be 100755 --- a/client/src/models/List.js +++ b/client/src/models/List.js @@ -10,7 +10,8 @@ import buildSearchParts from '../utils/build-search-parts'; import { isListFinite } from '../utils/record-helpers'; import ActionTypes from '../constants/ActionTypes'; import Config from '../constants/Config'; -import { ListSortFieldNames, ListTypes, SortOrders } from '../constants/Enums'; +import { ListSortFieldNames, ListTypes, ListTypeStates, SortOrders } from '../constants/Enums'; +import LIST_TYPE_STATE_BY_TYPE from '../constants/ListTypeStateByType'; const POSITION_BY_LIST_TYPE = { [ListTypes.ARCHIVE]: Number.MAX_SAFE_INTEGER - 1, @@ -28,6 +29,21 @@ const prepareList = (list) => { }; }; +const getChangedTypeState = (prevList, list) => { + const prevTypeState = LIST_TYPE_STATE_BY_TYPE[prevList.type]; + const typeState = LIST_TYPE_STATE_BY_TYPE[list.type]; + + if (prevTypeState === ListTypeStates.OPENED && typeState === ListTypeStates.CLOSED) { + return ListTypeStates.CLOSED; + } + + if (prevTypeState === ListTypeStates.CLOSED && typeState === ListTypeStates.OPENED) { + return ListTypeStates.OPENED; + } + + return null; +}; + export default class extends BaseModel { static modelName = 'List'; @@ -121,12 +137,12 @@ export default class extends BaseModel { let isClosed; if (payload.data.type) { - if (payload.data.type === ListTypes.CLOSED) { - if (listModel.type === ListTypes.ACTIVE) { - isClosed = true; - } - } else if (listModel.type === ListTypes.CLOSED) { + const changedTypeState = getChangedTypeState(listModel, payload.data); + + if (changedTypeState === ListTypeStates.OPENED) { isClosed = false; + } else if (changedTypeState === ListTypeStates.CLOSED) { + isClosed = true; } } @@ -150,13 +166,13 @@ export default class extends BaseModel { const listModel = List.withId(payload.list.id); if (listModel) { + const changedTypeState = getChangedTypeState(listModel, payload.list); + let isClosed; - if (payload.list.type === ListTypes.CLOSED) { - if (listModel.type === ListTypes.ACTIVE) { - isClosed = true; - } - } else if (listModel.type === ListTypes.CLOSED) { + if (changedTypeState === ListTypeStates.OPENED) { isClosed = false; + } else if (changedTypeState === ListTypeStates.CLOSED) { + isClosed = true; } listModel.update(prepareList(payload.list)); diff --git a/client/src/sagas/core/services/cards.js b/client/src/sagas/core/services/cards.js index 788295e4..c668f8c9 100644 --- a/client/src/sagas/core/services/cards.js +++ b/client/src/sagas/core/services/cards.js @@ -14,7 +14,8 @@ import api from '../../../api'; import { createLocalId } from '../../../utils/local-id'; import { isListArchiveOrTrash, isListFinite } from '../../../utils/record-helpers'; import ActionTypes from '../../../constants/ActionTypes'; -import { BoardViews, ListTypes } from '../../../constants/Enums'; +import { BoardViews, ListTypes, ListTypeStates } from '../../../constants/Enums'; +import LIST_TYPE_STATE_BY_TYPE from '../../../constants/ListTypeStateByType'; // eslint-disable-next-line no-underscore-dangle const _preloadImage = (url) => @@ -137,7 +138,7 @@ export function* createCard(listId, data, autoOpen) { id: localId, boardId: list.boardId, creatorUserId: currentUserMembership.userId, - isClosed: list.type === ListTypes.CLOSED, + isClosed: LIST_TYPE_STATE_BY_TYPE[list.type] === ListTypeStates.CLOSED, }, autoOpen, ), @@ -242,11 +243,13 @@ export function* updateCard(id, data) { prevListId = null; } + const typeState = LIST_TYPE_STATE_BY_TYPE[list.type]; + if (card.isClosed) { - if (list.type === ListTypes.ACTIVE) { + if (typeState === ListTypeStates.OPENED) { isClosed = false; } - } else if (list.type === ListTypes.CLOSED) { + } else if (typeState === ListTypeStates.CLOSED) { isClosed = true; } } diff --git a/server/api/controllers/tasks/create.js b/server/api/controllers/tasks/create.js index 887a7578..f1b1bb78 100755 --- a/server/api/controllers/tasks/create.js +++ b/server/api/controllers/tasks/create.js @@ -15,6 +15,9 @@ const Errors = { LINKED_CARD_NOT_FOUND: { linkedCardNotFound: 'Linked card not found', }, + LINKED_CARD_OR_NAME_MUST_BE_PRESENT: { + linkedCardOrNameMustBePresent: 'Linked card or name must be present', + }, }; module.exports = { @@ -31,8 +34,9 @@ module.exports = { }, name: { type: 'string', + isNotEmptyString: true, maxLength: 1024, - // required: true, + allowNull: true, }, isCompleted: { type: 'boolean', @@ -49,6 +53,9 @@ module.exports = { linkedCardNotFound: { responseType: 'notFound', }, + linkedCardOrNameMustBePresent: { + responseType: 'unprocessableEntity', + }, }, async fn(inputs) { @@ -100,19 +107,24 @@ module.exports = { const values = _.pick(inputs, ['position', 'name', 'isCompleted']); - const task = await sails.helpers.tasks.createOne.with({ - project, - board, - list, - card, - values: { - ...values, - taskList, - linkedCard, - }, - actorUser: currentUser, - request: this.req, - }); + const task = await sails.helpers.tasks.createOne + .with({ + project, + board, + list, + card, + values: { + ...values, + taskList, + linkedCard, + }, + actorUser: currentUser, + request: this.req, + }) + .intercept( + 'linkedCardOrNameMustBeInValues', + () => Errors.LINKED_CARD_OR_NAME_MUST_BE_PRESENT, + ); return { item: task, diff --git a/server/api/helpers/cards/create-one.js b/server/api/helpers/cards/create-one.js index b997231f..c86a1d86 100644 --- a/server/api/helpers/cards/create-one.js +++ b/server/api/helpers/cards/create-one.js @@ -67,7 +67,7 @@ module.exports = { delete values.position; } - if (values.list.type === List.Types.CLOSED) { + if (List.TYPE_STATE_BY_TYPE[values.list.type] === List.TypeStates.CLOSED) { values.isClosed = true; } diff --git a/server/api/helpers/cards/update-one.js b/server/api/helpers/cards/update-one.js index 345a32e6..b202466c 100644 --- a/server/api/helpers/cards/update-one.js +++ b/server/api/helpers/cards/update-one.js @@ -386,11 +386,13 @@ module.exports = { values.prevListId = null; } + const typeState = List.TYPE_STATE_BY_TYPE[values.list.type]; + if (inputs.record.isClosed) { - if (values.list.type === List.Types.ACTIVE) { + if (typeState === List.TypeStates.OPENED) { values.isClosed = false; } - } else if (values.list.type === List.Types.CLOSED) { + } else if (typeState === List.TypeStates.CLOSED) { values.isClosed = true; } diff --git a/server/api/helpers/tasks/create-one.js b/server/api/helpers/tasks/create-one.js index b6cc2a86..5eba941e 100644 --- a/server/api/helpers/tasks/create-one.js +++ b/server/api/helpers/tasks/create-one.js @@ -34,9 +34,17 @@ module.exports = { }, }, + exists: { + linkedCardOrNameMustBeInValues: {}, + }, + async fn(inputs) { const { values } = inputs; + if (!values.linkedCard && !values.name) { + throw 'linkedCardOrNameMustBeInValues'; + } + const tasks = await Task.qm.getByTaskListId(values.taskList.id); const { position, repositions } = sails.helpers.utils.insertToPositionables( diff --git a/server/api/hooks/query-methods/models/List.js b/server/api/hooks/query-methods/models/List.js index 2b3d674a..655c36d3 100644 --- a/server/api/hooks/query-methods/models/List.js +++ b/server/api/hooks/query-methods/models/List.js @@ -58,10 +58,16 @@ const updateOne = async (criteria, values) => { let tasks = []; if (list) { + const prevTypeState = List.TYPE_STATE_BY_TYPE[prevList.type]; + const typeState = List.TYPE_STATE_BY_TYPE[list.type]; + let isClosed; - if (list.type === List.Types.ACTIVE) { + if (prevTypeState === List.TypeStates.CLOSED && typeState === List.TypeStates.OPENED) { isClosed = false; - } else if (list.type === List.Types.CLOSED) { + } else if ( + prevTypeState === List.TypeStates.OPENED && + typeState === List.TypeStates.CLOSED + ) { isClosed = true; } diff --git a/server/api/hooks/query-methods/models/Task.js b/server/api/hooks/query-methods/models/Task.js index a6bf40de..32c3380b 100644 --- a/server/api/hooks/query-methods/models/Task.js +++ b/server/api/hooks/query-methods/models/Task.js @@ -36,11 +36,6 @@ const getByTaskListIds = async (taskListIds, { sort = ['position', 'id'] } = {}) { sort }, ); -const getByLinkedCardId = (linkedCardId) => - defaultFind({ - linkedCardId, - }); - const getOneById = (id, { taskListId } = {}) => { const criteria = { id, @@ -68,7 +63,6 @@ module.exports = { getByIds, getByTaskListId, getByTaskListIds, - getByLinkedCardId, getOneById, update, updateOne, diff --git a/server/api/models/Card.js b/server/api/models/Card.js index 3ac61c65..6b440370 100755 --- a/server/api/models/Card.js +++ b/server/api/models/Card.js @@ -48,16 +48,16 @@ module.exports = { stopwatch: { type: 'json', }, - isClosed: { - type: 'boolean', - defaultsTo: false, - columnName: 'is_closed', - }, commentsTotal: { type: 'number', defaultsTo: 0, columnName: 'comments_total', }, + isClosed: { + type: 'boolean', + defaultsTo: false, + columnName: 'is_closed', + }, listChangedAt: { type: 'ref', columnName: 'list_changed_at', diff --git a/server/api/models/List.js b/server/api/models/List.js index b43693e5..bd3b18e7 100755 --- a/server/api/models/List.js +++ b/server/api/models/List.js @@ -17,6 +17,11 @@ const Types = { TRASH: 'trash', }; +const TypeStates = { + OPENED: 'opened', + CLOSED: 'closed', +}; + const SortFieldNames = { NAME: 'name', DUE_DATE: 'dueDate', @@ -31,6 +36,11 @@ const SortOrders = { const FINITE_TYPES = [Types.ACTIVE, Types.CLOSED]; +const TYPE_STATE_BY_TYPE = { + [Types.ACTIVE]: TypeStates.OPENED, + [Types.CLOSED]: Types.CLOSED, +}; + const COLORS = [ 'berry-red', 'pumpkin-orange', @@ -46,9 +56,11 @@ const COLORS = [ module.exports = { Types, + TypeStates, SortFieldNames, SortOrders, FINITE_TYPES, + TYPE_STATE_BY_TYPE, COLORS, attributes: {