1 Commits

5 changed files with 82 additions and 38 deletions

View File

@@ -12,11 +12,9 @@
var CAPS = { daily: 0, weekly: 6, monthly_day: 27, monthly_dow: 27, yearly: 364 }; var CAPS = { daily: 0, weekly: 6, monthly_day: 27, monthly_dow: 27, yearly: 364 };
function apply(select) { function apply(select) {
// "days before" ceiling / disable by frequency.
var input = document.querySelector('input[name="recoreco_days_before"]'); var input = document.querySelector('input[name="recoreco_days_before"]');
if (! input) { if (input) {
return;
}
var max = CAPS.hasOwnProperty(select.value) ? CAPS[select.value] : 27; var max = CAPS.hasOwnProperty(select.value) ? CAPS[select.value] : 27;
if (max === 0) { if (max === 0) {
@@ -32,6 +30,14 @@
} }
} }
// The last-day/weekday checkbox only means something for monthly and yearly.
var lastday = document.querySelector(".recoreco-lastday");
if (lastday) {
var monthlyOrYearly = select.value === "monthly_day" || select.value === "monthly_dow" || select.value === "yearly";
lastday.style.display = monthlyOrYearly ? "" : "none";
}
}
document.addEventListener("change", function (e) { document.addEventListener("change", function (e) {
if (e.target && e.target.name === "recoreco_frequency") { if (e.target && e.target.name === "recoreco_frequency") {
apply(e.target); apply(e.target);

View File

@@ -7,29 +7,32 @@ use Kanboard\Model\TaskModel;
/** /**
* The "Recurring schedule" modal: configure a card's calendar recurrence and store it in task * The "Recurring schedule" modal: configure a card's calendar recurrence and store it in task
* metadata. v0.2 is storage only -- no spawning (that is the CLI engine in later versions). * metadata. The CLI engine (RecoRecoModel) does the actual spawning.
* *
* Gate: a card can only be enabled when it has a due date (the recurrence anchor) and is not * Gate: a card can only be enabled when it has a due date (the recurrence anchor), is not already a
* already a native-recurring card (RecoReco and native recurrence are mutually exclusive). * native-recurring card (mutually exclusive), and the board has a column other than the card's own
* (the copy must land elsewhere).
*/ */
class RecurrenceController extends BaseController class RecurrenceController extends BaseController
{ {
public function edit(array $values = array(), array $errors = array()) public function edit(array $values = array(), array $errors = array())
{ {
$task = $this->getTask(); $task = $this->getTask();
$columns = $this->targetColumns($task);
if (empty($values)) { if (empty($values)) {
$values = $this->getStoredValues($task); $values = $this->getStoredValues($task, $columns);
} }
$this->response->html($this->template->render('recoReco:recurrence/edit', array( $this->response->html($this->template->render('recoReco:recurrence/edit', array(
'task' => $task, 'task' => $task,
'values' => $values, 'values' => $values,
'errors' => $errors, 'errors' => $errors,
'columns_list' => $this->columnModel->getList($task['project_id']), 'columns_list' => $columns,
'frequency_list' => $this->getFrequencyList(), 'frequency_list' => $this->getFrequencyList(),
'has_due_date' => ! empty($task['date_due']), 'has_due_date' => ! empty($task['date_due']),
'is_native' => $task['recurrence_status'] != TaskModel::RECURRING_STATUS_NONE, 'is_native' => $task['recurrence_status'] != TaskModel::RECURRING_STATUS_NONE,
'has_target' => ! empty($columns),
))); )));
} }
@@ -37,9 +40,12 @@ class RecurrenceController extends BaseController
{ {
$task = $this->getTask(); $task = $this->getTask();
$input = $this->request->getValues(); $input = $this->request->getValues();
$columns = $this->targetColumns($task);
// Only a plain card (has a due date, not native-recurring) may be enabled. // Only a plain card (a due date, not native-recurring, and a valid target) may be enabled.
$can_recur = ! empty($task['date_due']) && $task['recurrence_status'] == TaskModel::RECURRING_STATUS_NONE; $can_recur = ! empty($task['date_due'])
&& $task['recurrence_status'] == TaskModel::RECURRING_STATUS_NONE
&& ! empty($columns);
$enabled = ($can_recur && isset($input['recoreco_enabled']) && $input['recoreco_enabled'] == 1) ? 1 : 0; $enabled = ($can_recur && isset($input['recoreco_enabled']) && $input['recoreco_enabled'] == 1) ? 1 : 0;
$frequency = isset($input['recoreco_frequency']) && array_key_exists($input['recoreco_frequency'], $this->getFrequencyList()) $frequency = isset($input['recoreco_frequency']) && array_key_exists($input['recoreco_frequency'], $this->getFrequencyList())
@@ -52,23 +58,21 @@ class RecurrenceController extends BaseController
$values = array( $values = array(
'recoreco_enabled' => $enabled, 'recoreco_enabled' => $enabled,
'recoreco_target_column' => isset($input['recoreco_target_column']) ? (int) $input['recoreco_target_column'] : (int) $task['column_id'], 'recoreco_target_column' => $this->resolveTarget(isset($input['recoreco_target_column']) ? $input['recoreco_target_column'] : 0, $columns),
'recoreco_frequency' => $frequency, 'recoreco_frequency' => $frequency,
'recoreco_last_day' => isset($input['recoreco_last_day']) ? 1 : 0, 'recoreco_last_day' => isset($input['recoreco_last_day']) ? 1 : 0,
'recoreco_days_before' => $daysBefore, 'recoreco_days_before' => $daysBefore,
'recoreco_link_copies' => isset($input['recoreco_link_copies']) ? 1 : 0, 'recoreco_link_copies' => isset($input['recoreco_link_copies']) ? 1 : 0,
); );
// Capture the anchor (the due date at enable time) -- the drift-free pattern. The engine // Capture the anchor (the due date at enable time) -- the drift-free pattern.
// (v0.3) reads it to compute occurrences.
if ($enabled) { if ($enabled) {
$values['recoreco_anchor'] = (int) $task['date_due']; $values['recoreco_anchor'] = (int) $task['date_due'];
} }
$this->taskMetadataModel->save($task['id'], $values); $this->taskMetadataModel->save($task['id'], $values);
// Reconcile the template's duplicate links with the setting right away (removes existing // Reconcile the template's duplicate links with the setting right away.
// links to its RecoReco clones when the option is off).
$model = new \Kanboard\Plugin\RecoReco\Model\RecoRecoModel($this->container); $model = new \Kanboard\Plugin\RecoReco\Model\RecoRecoModel($this->container);
$model->syncCloneLinks($task['id'], $values['recoreco_link_copies'] == 1); $model->syncCloneLinks($task['id'], $values['recoreco_link_copies'] == 1);
@@ -77,13 +81,36 @@ class RecurrenceController extends BaseController
return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'])), true); return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'])), true);
} }
private function getStoredValues(array $task) // Columns the copy can land in: every column except the template's own (the copy must move).
private function targetColumns(array $task)
{
$columns = $this->columnModel->getList($task['project_id']);
unset($columns[$task['column_id']]);
return $columns;
}
// A stored/posted target, if still a valid non-current column; otherwise the first one (or 0).
private function resolveTarget($candidate, array $columns)
{
$candidate = (int) $candidate;
if (array_key_exists($candidate, $columns)) {
return $candidate;
}
$keys = array_keys($columns);
return empty($keys) ? 0 : (int) $keys[0];
}
private function getStoredValues(array $task, array $columns)
{ {
$meta = $this->taskMetadataModel->getAll($task['id']); $meta = $this->taskMetadataModel->getAll($task['id']);
return array( return array(
'recoreco_enabled' => isset($meta['recoreco_enabled']) ? (int) $meta['recoreco_enabled'] : 0, 'recoreco_enabled' => isset($meta['recoreco_enabled']) ? (int) $meta['recoreco_enabled'] : 0,
'recoreco_target_column' => isset($meta['recoreco_target_column']) ? (int) $meta['recoreco_target_column'] : (int) $task['column_id'], 'recoreco_target_column' => $this->resolveTarget(isset($meta['recoreco_target_column']) ? $meta['recoreco_target_column'] : 0, $columns),
'recoreco_frequency' => isset($meta['recoreco_frequency']) ? $meta['recoreco_frequency'] : 'monthly_day', 'recoreco_frequency' => isset($meta['recoreco_frequency']) ? $meta['recoreco_frequency'] : 'monthly_day',
'recoreco_last_day' => isset($meta['recoreco_last_day']) ? (int) $meta['recoreco_last_day'] : 0, 'recoreco_last_day' => isset($meta['recoreco_last_day']) ? (int) $meta['recoreco_last_day'] : 0,
'recoreco_days_before' => isset($meta['recoreco_days_before']) ? (int) $meta['recoreco_days_before'] : 0, 'recoreco_days_before' => isset($meta['recoreco_days_before']) ? (int) $meta['recoreco_days_before'] : 0,

View File

@@ -41,7 +41,7 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '1.1.1'; return '1.1.2';
} }
public function getPluginHomepage() public function getPluginHomepage()

View File

@@ -1,13 +1,25 @@
<div class="page-header"> <div class="page-header">
<h2><?= t('Recurring schedule') ?><?php if ($has_due_date): ?> <small>(<?= date('Y-m-d H:i', $task['date_due']) ?>)</small><?php endif ?></h2> <h2>
<?= t('Recurring schedule') ?>
<?php if ($has_due_date): ?>
<small>(<?= $this->url->link(date('Y-m-d H:i', $task['date_due']), 'TaskModificationController', 'edit', array('task_id' => $task['id']), false, 'js-modal-large') ?>)</small>
<?php else: ?>
<small>-- <?= $this->url->link(t('set a due date'), 'TaskModificationController', 'edit', array('task_id' => $task['id']), false, 'js-modal-large') ?></small>
<?php endif ?>
</h2>
</div> </div>
<?php $can_recur = $has_due_date && ! $is_native ?> <?php $can_recur = $has_due_date && ! $is_native && $has_target ?>
<?php if (! $has_due_date): ?> <?php if (! $has_due_date): ?>
<p class="alert alert-info"><?= t('Please set a due date first. RecoReco uses the card due date as the recurrence anchor.') ?></p> <p class="alert alert-info"><?= t('Please set a due date first. RecoReco uses the card due date as the recurrence anchor.') ?></p>
<?php elseif ($is_native): ?> <?php elseif ($is_native): ?>
<p class="alert alert-info"><?= t('This card already uses Kanboard built-in recurrence. RecoReco and native recurrence cannot be used together.') ?></p> <p class="alert alert-info">
<?= t('This card uses Kanboard built-in recurrence.') ?>
<?= $this->url->link(t('Edit built-in recurrence'), 'TaskRecurrenceController', 'edit', array('task_id' => $task['id']), false, 'js-modal-medium') ?>
</p>
<?php elseif (! $has_target): ?>
<p class="alert alert-info"><?= t('Add a target column first.') ?></p>
<?php endif ?> <?php endif ?>
<form method="post" action="<?= $this->url->href('RecurrenceController', 'save', array('plugin' => 'RecoReco', 'task_id' => $task['id'])) ?>" autocomplete="off"> <form method="post" action="<?= $this->url->href('RecurrenceController', 'save', array('plugin' => 'RecoReco', 'task_id' => $task['id'])) ?>" autocomplete="off">
@@ -29,16 +41,15 @@
<?= $this->form->label(t('Frequency'), 'recoreco_frequency') ?> <?= $this->form->label(t('Frequency'), 'recoreco_frequency') ?>
<?= $this->form->select('recoreco_frequency', $frequency_list, $values) ?> <?= $this->form->select('recoreco_frequency', $frequency_list, $values) ?>
<?= $this->form->checkbox('recoreco_last_day', t('Last day of the month (honored only when the due date is the last day / last weekday of its month)'), 1, $values['recoreco_last_day'] == 1) ?> <div class="recoreco-lastday">
<?= $this->form->checkbox('recoreco_last_day', t('Fires on last day/weekday of the month'), 1, $values['recoreco_last_day'] == 1) ?>
</div>
<?= $this->form->label(t('Create the copy this many days before the due date'), 'recoreco_days_before') ?> <?= $this->form->label(t('Create the copy this many days before the due date'), 'recoreco_days_before') ?>
<input type="number" name="recoreco_days_before" min="0" value="<?= $this->text->e($values['recoreco_days_before']) ?>"> <input type="number" name="recoreco_days_before" min="0" value="<?= $this->text->e($values['recoreco_days_before']) ?>">
<?= $this->form->checkbox('recoreco_link_copies', t('Link copies to the template'), 1, $values['recoreco_link_copies'] == 1) ?> <?= $this->form->checkbox('recoreco_link_copies', t('Link copies to the template'), 1, $values['recoreco_link_copies'] == 1) ?>
<p class="form-help"> <p class="form-help"><?= t('Links each copy back to the template (a count and quick navigation). Off by default.') ?></p>
<?= t('When on, the template keeps a link to each copy -- a running count and one-click navigation to every one. Off by default; best left off for frequent (daily/weekly) schedules to avoid piling up links.') ?>
</p>
<?= $this->modal->submitButtons() ?> <?= $this->modal->submitButtons() ?>
</form> </form>

View File

@@ -1 +1 @@
RecoReco v1.1.1 RecoReco v1.1.2