From 85432401ea4bc9f3a305c50b5c61e8dc4223e818 Mon Sep 17 00:00:00 2001 From: Ruben Carlo Benante Date: Tue, 7 Jul 2026 23:52:55 -0300 Subject: [PATCH] modal UX -- target excludes current column, clearer last-day, links (v1.1.2) --- Asset/js/recoreco-modal.js | 34 ++++++++++-------- Controller/RecurrenceController.php | 55 +++++++++++++++++++++-------- Plugin.php | 2 +- Template/recurrence/edit.php | 27 +++++++++----- VERSION | 2 +- 5 files changed, 82 insertions(+), 38 deletions(-) diff --git a/Asset/js/recoreco-modal.js b/Asset/js/recoreco-modal.js index 9672afb..cb599cf 100644 --- a/Asset/js/recoreco-modal.js +++ b/Asset/js/recoreco-modal.js @@ -12,23 +12,29 @@ var CAPS = { daily: 0, weekly: 6, monthly_day: 27, monthly_dow: 27, yearly: 364 }; function apply(select) { + // "days before" ceiling / disable by frequency. var input = document.querySelector('input[name="recoreco_days_before"]'); - if (! input) { - return; + if (input) { + var max = CAPS.hasOwnProperty(select.value) ? CAPS[select.value] : 27; + + if (max === 0) { + input.value = 0; + input.setAttribute("max", "0"); + input.disabled = true; + } else { + input.disabled = false; + input.setAttribute("max", String(max)); + if (parseInt(input.value, 10) > max) { + input.value = max; + } + } } - var max = CAPS.hasOwnProperty(select.value) ? CAPS[select.value] : 27; - - if (max === 0) { - input.value = 0; - input.setAttribute("max", "0"); - input.disabled = true; - } else { - input.disabled = false; - input.setAttribute("max", String(max)); - if (parseInt(input.value, 10) > max) { - input.value = max; - } + // 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"; } } diff --git a/Controller/RecurrenceController.php b/Controller/RecurrenceController.php index 684f72f..b3828ec 100644 --- a/Controller/RecurrenceController.php +++ b/Controller/RecurrenceController.php @@ -7,29 +7,32 @@ use Kanboard\Model\TaskModel; /** * 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 - * already a native-recurring card (RecoReco and native recurrence are mutually exclusive). + * Gate: a card can only be enabled when it has a due date (the recurrence anchor), is not already a + * 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 { public function edit(array $values = array(), array $errors = array()) { $task = $this->getTask(); + $columns = $this->targetColumns($task); if (empty($values)) { - $values = $this->getStoredValues($task); + $values = $this->getStoredValues($task, $columns); } $this->response->html($this->template->render('recoReco:recurrence/edit', array( 'task' => $task, 'values' => $values, 'errors' => $errors, - 'columns_list' => $this->columnModel->getList($task['project_id']), + 'columns_list' => $columns, 'frequency_list' => $this->getFrequencyList(), 'has_due_date' => ! empty($task['date_due']), 'is_native' => $task['recurrence_status'] != TaskModel::RECURRING_STATUS_NONE, + 'has_target' => ! empty($columns), ))); } @@ -37,9 +40,12 @@ class RecurrenceController extends BaseController { $task = $this->getTask(); $input = $this->request->getValues(); + $columns = $this->targetColumns($task); - // Only a plain card (has a due date, not native-recurring) may be enabled. - $can_recur = ! empty($task['date_due']) && $task['recurrence_status'] == TaskModel::RECURRING_STATUS_NONE; + // 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 + && ! empty($columns); $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()) @@ -52,23 +58,21 @@ class RecurrenceController extends BaseController $values = array( '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_last_day' => isset($input['recoreco_last_day']) ? 1 : 0, 'recoreco_days_before' => $daysBefore, '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 - // (v0.3) reads it to compute occurrences. + // Capture the anchor (the due date at enable time) -- the drift-free pattern. if ($enabled) { $values['recoreco_anchor'] = (int) $task['date_due']; } $this->taskMetadataModel->save($task['id'], $values); - // Reconcile the template's duplicate links with the setting right away (removes existing - // links to its RecoReco clones when the option is off). + // Reconcile the template's duplicate links with the setting right away. $model = new \Kanboard\Plugin\RecoReco\Model\RecoRecoModel($this->container); $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); } - 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']); return array( '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_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, diff --git a/Plugin.php b/Plugin.php index e98950f..04dff8f 100644 --- a/Plugin.php +++ b/Plugin.php @@ -41,7 +41,7 @@ class Plugin extends Base public function getPluginVersion() { - return '1.1.1'; + return '1.1.2'; } public function getPluginHomepage() diff --git a/Template/recurrence/edit.php b/Template/recurrence/edit.php index e047c90..92f33fe 100644 --- a/Template/recurrence/edit.php +++ b/Template/recurrence/edit.php @@ -1,13 +1,25 @@ - +

-

+

+ + url->link(t('Edit built-in recurrence'), 'TaskRecurrenceController', 'edit', array('task_id' => $task['id']), false, 'js-modal-medium') ?> +

+ +

@@ -29,16 +41,15 @@ form->label(t('Frequency'), 'recoreco_frequency') ?> form->select('recoreco_frequency', $frequency_list, $values) ?> - 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) ?> +
+ form->checkbox('recoreco_last_day', t('Fires on last day/weekday of the month'), 1, $values['recoreco_last_day'] == 1) ?> +
form->label(t('Create the copy this many days before the due date'), 'recoreco_days_before') ?> form->checkbox('recoreco_link_copies', t('Link copies to the template'), 1, $values['recoreco_link_copies'] == 1) ?> -

- -

- +

modal->submitButtons() ?>
diff --git a/VERSION b/VERSION index e127cbb..19b0cbe 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -RecoReco v1.1.1 +RecoReco v1.1.2