Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions lib/Controller/CardApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ public function get() {
*
* Get a specific card.
*/
public function create($title, $type = 'plain', $order = 999, $description = '', $duedate = null, $labels = [], $users = []) {
$card = $this->cardService->create($title, $this->request->getParam('stackId'), $type, $order, $this->userId, $description, $duedate);
public function create($title, $type = 'plain', $order = 999, $description = '', $duedate = null, $startdate = null, $labels = [], $users = []) {
$card = $this->cardService->create($title, $this->request->getParam('stackId'), $type, $order, $this->userId, $description, $duedate, $startdate);

foreach ($labels as $labelId) {
$this->cardService->assignLabel($card->getId(), $labelId);
Expand All @@ -88,9 +88,9 @@ public function create($title, $type = 'plain', $order = 999, $description = '',
#[NoAdminRequired]
#[CORS]
#[NoCSRFRequired]
public function update(string $title, $type, string $owner, string $description = '', int $order = 0, $duedate = null, $archived = null): DataResponse {
public function update(string $title, $type, string $owner, string $description = '', int $order = 0, $duedate = null, $startdate = null, $archived = null): DataResponse {
$done = array_key_exists('done', $this->request->getParams()) ? new OptionalNullableValue($this->request->getParam('done', null)) : null;
$card = $this->cardService->update($this->request->getParam('cardId'), $title, $this->request->getParam('stackId'), $type, $owner, $description, $order, $duedate, 0, $archived, $done);
$card = $this->cardService->update($this->request->getParam('cardId'), $title, $this->request->getParam('stackId'), $type, $owner, $description, $order, $duedate, 0, $archived, $done, $startdate);
return new DataResponse($card, HTTP::STATUS_OK);
}

Expand Down
8 changes: 4 additions & 4 deletions lib/Controller/CardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ public function rename(int $cardId, string $title): Card {
}

#[NoAdminRequired]
public function create(string $title, int $stackId, string $type = 'plain', int $order = 999, string $description = '', $duedate = null, array $labels = [], array $users = []): Card {
$card = $this->cardService->create($title, $stackId, $type, $order, $this->userId, $description, $duedate);
public function create(string $title, int $stackId, string $type = 'plain', int $order = 999, string $description = '', $duedate = null, $startdate = null, array $labels = [], array $users = []): Card {
$card = $this->cardService->create($title, $stackId, $type, $order, $this->userId, $description, $duedate, $startdate);

foreach ($labels as $label) {
$this->assignLabel($card->getId(), $label);
Expand All @@ -64,11 +64,11 @@ public function create(string $title, int $stackId, string $type = 'plain', int
* @param $duedate
*/
#[NoAdminRequired]
public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, $archived = null): Card {
public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, $archived = null, $startdate = null): Card {
$done = array_key_exists('done', $this->request->getParams())
? new OptionalNullableValue($this->request->getParam('done', null))
: null;
return $this->cardService->update($id, $title, $stackId, $type, $this->userId, $description, $order, $duedate, $deletedAt, $archived, $done);
return $this->cardService->update($id, $title, $stackId, $type, $this->userId, $description, $order, $duedate, $deletedAt, $archived, $done, $startdate);
}

#[NoAdminRequired]
Expand Down
9 changes: 5 additions & 4 deletions lib/Controller/CardOcsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function __construct(
#[PublicPage]
#[NoCSRFRequired]
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
public function create(string $title, int $stackId, ?int $boardId = null, ?string $type = 'plain', ?string $owner = null, ?int $order = 999, ?string $description = '', $duedate = null, ?array $labels = [], ?array $users = []) {
public function create(string $title, int $stackId, ?int $boardId = null, ?string $type = 'plain', ?string $owner = null, ?int $order = 999, ?string $description = '', $duedate = null, $startdate = null, ?array $labels = [], ?array $users = []) {
if ($boardId) {
$board = $this->boardService->find($boardId, false);
if ($board->getExternalId()) {
Expand All @@ -49,7 +49,7 @@ public function create(string $title, int $stackId, ?int $boardId = null, ?strin
if (!$owner) {
$owner = $this->userId;
}
$card = $this->cardService->create($title, $stackId, $type, $order, $owner, $description, $duedate);
$card = $this->cardService->create($title, $stackId, $type, $order, $owner, $description, $duedate, $startdate);

// foreach ($labels as $label) {
// $this->assignLabel($card->getId(), $label);
Expand Down Expand Up @@ -95,7 +95,7 @@ public function removeLabel(?int $boardId, int $cardId, int $labelId): DataRespo
#[PublicPage]
#[NoCSRFRequired]
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, int $boardId, array|string|null $owner = null, $archived = null): DataResponse {
public function update(int $id, string $title, int $stackId, string $type, int $order, string $description, $duedate, $deletedAt, int $boardId, array|string|null $owner = null, $archived = null, $startdate = null): DataResponse {
$done = array_key_exists('done', $this->request->getParams())
? new OptionalNullableValue($this->request->getParam('done', null))
: null;
Expand Down Expand Up @@ -135,7 +135,8 @@ public function update(int $id, string $title, int $stackId, string $type, int $
$duedate,
$deletedAt,
$archived,
$done
$done,
$startdate
));
}
}
7 changes: 7 additions & 0 deletions lib/Db/Card.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
* @method bool getNotified()
* @method ?DateTime getDone()
* @method void setDone(?DateTime $done)
* @method ?DateTime getStartdate()
* @method void setStartdate(?DateTime $startdate)
*
* @method void setLabels(Label[] $labels)
* @method null|Label[] getLabels()
Expand Down Expand Up @@ -80,6 +82,7 @@ class Card extends RelationalEntity {
protected $archived = false;
protected $done = null;
protected $duedate;
protected $startdate;
protected $notified = false;
protected $deletedAt = 0;
protected $commentsUnread = 0;
Expand All @@ -106,6 +109,7 @@ public function __construct() {
$this->addType('notified', 'boolean');
$this->addType('deletedAt', 'integer');
$this->addType('duedate', 'datetime');
$this->addType('startdate', 'datetime');
$this->addRelation('labels');
$this->addRelation('assignedUsers');
$this->addRelation('attachments');
Expand Down Expand Up @@ -133,6 +137,9 @@ public function getCalendarObject(): VCalendar {
$event->DTSTAMP = $creationDate;
$event->DUE = new DateTime($this->getDuedate()->format('c'), new DateTimeZone('UTC'));
}
if ($this->getStartdate()) {
$event->DTSTART = new DateTime($this->getStartdate()->format('c'), new DateTimeZone('UTC'));
}
$event->add('RELATED-TO', 'deck-stack-' . $this->getStackId());

// FIXME: For write support: CANCELLED / IN-PROCESS handling
Expand Down
29 changes: 29 additions & 0 deletions lib/Migration/Version11002Date20260312000000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);
namespace OCA\Deck\Migration;

use Closure;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version11002Date20260312000000 extends SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
$schema = $schemaClosure();

if ($schema->hasTable('deck_cards')) {
$table = $schema->getTable('deck_cards');
if (!$table->hasColumn('startdate')) {
$table->addColumn('startdate', 'datetime', [
'notnull' => false,
]);
}
}
return $schema;
}
}
6 changes: 4 additions & 2 deletions lib/Service/CardService.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public function findCalendarEntries(int $boardId): array {
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadrequestException
*/
public function create(string $title, int $stackId, string $type, int $order, string $owner, string $description = '', $duedate = null): Card {
public function create(string $title, int $stackId, string $type, int $order, string $owner, string $description = '', $duedate = null, $startdate = null): Card {
$this->cardServiceValidator->check(compact('title', 'stackId', 'type', 'order', 'owner'));

$this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_EDIT);
Expand All @@ -191,6 +191,7 @@ public function create(string $title, int $stackId, string $type, int $order, st
$card->setOwner($owner);
$card->setDescription($description);
$card->setDuedate($duedate);
$card->setStartdate($startdate);
$card = $this->cardMapper->insert($card);

$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $card, ActivityManager::SUBJECT_CARD_CREATE, [], $card->getOwner());
Expand Down Expand Up @@ -233,7 +234,7 @@ public function delete(int $id): Card {
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/
public function update(int $id, string $title, int $stackId, string $type, string $owner, string $description = '', int $order = 0, ?string $duedate = null, ?int $deletedAt = null, ?bool $archived = null, ?OptionalNullableValue $done = null): Card {
public function update(int $id, string $title, int $stackId, string $type, string $owner, string $description = '', int $order = 0, ?string $duedate = null, ?int $deletedAt = null, ?bool $archived = null, ?OptionalNullableValue $done = null, ?string $startdate = null): Card {
$this->cardServiceValidator->check(compact('id', 'title', 'stackId', 'type', 'owner', 'order'));

$this->permissionService->checkPermission($this->cardMapper, $id, Acl::PERMISSION_EDIT, allowDeletedCard: true);
Expand Down Expand Up @@ -276,6 +277,7 @@ public function update(int $id, string $title, int $stackId, string $type, strin
$card->setOrder($order);
$card->setOwner($owner);
$card->setDuedate($duedate ? new \DateTime($duedate) : null);
$card->setStartdate($startdate ? new \DateTime($startdate) : null);
$resetDuedateNotification = false;
if (
$card->getDuedate() === null
Expand Down
18 changes: 18 additions & 0 deletions src/components/card/CardSidebarTabDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
@select="assignUserToCard"
@remove="removeUserFromCard" />

<StartDateSelector :card="card"
:can-edit="canEdit"
@change="updateCardStartDate"
@input="debouncedUpdateCardStartDate" />

<DueDateSelector :card="card"
:can-edit="canEdit"
@change="updateCardDue"
Expand Down Expand Up @@ -52,12 +57,14 @@ import Description from './Description.vue'
import TagSelector from './TagSelector.vue'
import AssignmentSelector from './AssignmentSelector.vue'
import DueDateSelector from './DueDateSelector.vue'
import StartDateSelector from './StartDateSelector.vue'
import { debounce } from 'lodash'

export default {
name: 'CardSidebarTabDetails',
components: {
DueDateSelector,
StartDateSelector,
AssignmentSelector,
TagSelector,
Description,
Expand Down Expand Up @@ -151,6 +158,17 @@ export default {
this.updateCardDue(val)
}, 500),

updateCardStartDate(val) {
this.$store.dispatch('updateCardStartDate', {
...this.copiedCard,
startdate: val ? (new Date(val)).toISOString() : null,
})
},

debouncedUpdateCardStartDate: debounce(function(val) {
this.updateCardStartDate(val)
}, 500),

addLabelToCard(newLabel) {
this.copiedCard.labels.push(newLabel)
const data = {
Expand Down
111 changes: 111 additions & 0 deletions src/components/card/StartDateSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<CardDetailEntry :label="t('deck', 'Assign a start date to this card…')">
<CalendarStart slot="icon" :size="20" />
<template v-if="!card.done && !card.archived">
<NcDateTimePickerNative v-if="startdate"
id="card-startdate-picker"
v-model="startdate"
:placeholder="t('deck', 'Set a start date')"
:hide-label="true"
type="datetime-local" />
<NcActions v-if="canEdit"
:menu-title="!startdate ? t('deck', 'Add start date') : null"
type="tertiary">
<template v-if="!startdate" #icon>
<Plus :size="20" />
</template>
<NcActionButton v-if="!startdate"
close-after-click
@click="initDate">
<template #icon>
<Plus :size="20" />
</template>
{{ t('deck', 'Choose a date') }}
</NcActionButton>
<NcActionButton v-else
icon="icon-delete"
close-after-click
@click="removeStartDate">
{{ t('deck', 'Remove start date') }}
</NcActionButton>
</NcActions>
</template>
<template v-else>
<div v-if="startdate" class="start-info">
{{ formatReadableDate(startdate) }}
</div>
</template>
</CardDetailEntry>
</template>

<script>
import { defineComponent } from 'vue'
import {
NcActionButton,
NcActions,
NcDateTimePickerNative,
} from '@nextcloud/vue'
import readableDate from '../../mixins/readableDate.js'
import Plus from 'vue-material-design-icons/Plus.vue'
import CalendarStart from 'vue-material-design-icons/CalendarArrowLeft.vue'
import CardDetailEntry from './CardDetailEntry.vue'

export default defineComponent({
name: 'StartDateSelector',
components: {
CardDetailEntry,
Plus,
CalendarStart,
NcActions,
NcActionButton,
NcDateTimePickerNative,
},
mixins: [
readableDate,
],
props: {
card: {
type: Object,
default: null,
},
canEdit: {
type: Boolean,
default: false,
},
},
computed: {
startdate: {
get() {
return this.card?.startdate ? new Date(this.card.startdate) : null
},
set(val) {
this.$emit('input', val ? new Date(val) : null)
},
},
},
methods: {
initDate() {
if (this.startdate === null) {
const now = new Date()
now.setHours(8)
now.setMinutes(0)
now.setMilliseconds(0)
this.startdate = now
}
},
removeStartDate() {
this.startdate = null
this.$emit('change', null)
},
},
})
</script>
<style scoped lang="scss">
.start-info {
flex-grow: 1;
}
</style>
5 changes: 5 additions & 0 deletions src/store/card.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ export default function cardModuleFactory() {
const updatedCard = await apiClient.updateCard(card, stack.boardId)
commit('updateCardProperty', { property: 'duedate', card: updatedCard })
},
async updateCardStartDate({ commit, getters }, card) {
const stack = getters.stackById(card.stackId)
const updatedCard = await apiClient.updateCard(card, stack.boardId)
commit('updateCardProperty', { property: 'startdate', card: updatedCard })
},

addCardData({ commit }, cardData) {
const card = { ...cardData }
Expand Down
23 changes: 23 additions & 0 deletions tests/integration/features/decks.feature
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ Feature: decks
|duedate||
|overdue|0|

Scenario: Setting a startdate on a card
Given acting as user "user0"
And creates a board named "MyBoard" with color "000000"
And create a stack named "ToDo"
And create a card named "Scheduled task"
When get the card details
And the response should be a JSON array with the following mandatory values
|key|value|
|title|Scheduled task|
|startdate||
And set the card attribute "startdate" to "2026-03-01 09:00:00"
When get the card details
And the response should be a JSON array with the following mandatory values
|key|value|
|title|Scheduled task|
|startdate|2026-03-01T09:00:00+00:00|
And set the card attribute "startdate" to ""
When get the card details
And the response should be a JSON array with the following mandatory values
|key|value|
|title|Scheduled task|
|startdate||

Scenario: Cannot access card on a deleted board
Given acting as user "user0"
And creates a board named "MyBoard" with color "000000"
Expand Down
Loading
Loading