trigger column (cross-cutting) + move-mode; move relocates instead of copies (v1.9)
This commit is contained in:
@@ -61,6 +61,23 @@
|
||||
limit.disabled = checkbox.checked;
|
||||
}
|
||||
|
||||
// Trigger column: it can never be the target, so hide+disable that option and reset to Any if it
|
||||
// was selected; the invert checkbox only means something for a specific column.
|
||||
function applyTrigger() {
|
||||
var target = document.querySelector('select[name="recoreco_target_column"]');
|
||||
var trigger = document.querySelector('select[name="recoreco_trigger_column"]');
|
||||
var invert = document.querySelector('input[name="recoreco_trigger_invert"]');
|
||||
if (!target || !trigger) { return; }
|
||||
|
||||
for (var i = 0; i < trigger.options.length; i++) {
|
||||
var isTarget = trigger.options[i].value === target.value;
|
||||
trigger.options[i].hidden = isTarget;
|
||||
trigger.options[i].disabled = isTarget;
|
||||
}
|
||||
if (trigger.value === target.value) { trigger.value = "any"; }
|
||||
if (invert) { invert.disabled = (trigger.value === "any"); }
|
||||
}
|
||||
|
||||
document.addEventListener("change", function (e) {
|
||||
if (e.target && e.target.name === "recoreco_frequency") {
|
||||
apply(e.target);
|
||||
@@ -68,6 +85,9 @@
|
||||
if (e.target && e.target.name === "recoreco_follow_finance") {
|
||||
applyFollow(e.target);
|
||||
}
|
||||
if (e.target && (e.target.name === "recoreco_target_column" || e.target.name === "recoreco_trigger_column")) {
|
||||
applyTrigger();
|
||||
}
|
||||
});
|
||||
|
||||
if (window.MutationObserver) {
|
||||
@@ -76,6 +96,7 @@
|
||||
if (select) {
|
||||
select.setAttribute("data-recoreco-init", "1");
|
||||
apply(select);
|
||||
applyTrigger();
|
||||
}
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ class RecurrenceController extends BaseController
|
||||
'values' => $values,
|
||||
'errors' => $errors,
|
||||
'columns_list' => $columns,
|
||||
'all_columns' => $this->columnModel->getList($task['project_id']),
|
||||
'frequency_list' => $this->getFrequencyList(),
|
||||
'has_due_date' => ! empty($task['date_due']),
|
||||
'is_native' => $task['recurrence_status'] != TaskModel::RECURRING_STATUS_NONE,
|
||||
@@ -98,6 +99,16 @@ class RecurrenceController extends BaseController
|
||||
$values['recoreco_count'] = (isset($input['recoreco_count']) && ctype_digit((string) $input['recoreco_count']) && (int) $input['recoreco_count'] >= 1) ? (int) $input['recoreco_count'] : 1;
|
||||
}
|
||||
|
||||
// Trigger column + move-mode (v1.9).
|
||||
$allColumns = $this->columnModel->getList($task['project_id']);
|
||||
$trig = isset($input['recoreco_trigger_column']) ? (string) $input['recoreco_trigger_column'] : 'any';
|
||||
$values['recoreco_trigger_column'] = ($trig !== 'any' && array_key_exists((int) $trig, $allColumns)) ? (int) $trig : 'any';
|
||||
$values['recoreco_trigger_invert'] = isset($input['recoreco_trigger_invert']) ? 1 : 0;
|
||||
$values['recoreco_move'] = isset($input['recoreco_move']) ? 1 : 0;
|
||||
if ($values['recoreco_move'] === 1) {
|
||||
$values['recoreco_follow_finance'] = 0; // Follow-FinanceBuddy is N/A in move-mode
|
||||
}
|
||||
|
||||
$this->taskMetadataModel->save($task['id'], $values);
|
||||
|
||||
// Reconcile the template's duplicate links with the setting right away.
|
||||
@@ -145,6 +156,9 @@ class RecurrenceController extends BaseController
|
||||
'recoreco_follow_finance' => isset($meta['recoreco_follow_finance']) ? (int) $meta['recoreco_follow_finance'] : ($fb_enabled ? 1 : 0),
|
||||
'recoreco_limit' => isset($meta['recoreco_limit']) ? (int) $meta['recoreco_limit'] : 0,
|
||||
'recoreco_count' => isset($meta['recoreco_count']) ? (int) $meta['recoreco_count'] : 1,
|
||||
'recoreco_move' => isset($meta['recoreco_move']) ? (int) $meta['recoreco_move'] : 0,
|
||||
'recoreco_trigger_column' => isset($meta['recoreco_trigger_column']) ? $meta['recoreco_trigger_column'] : 'any',
|
||||
'recoreco_trigger_invert' => isset($meta['recoreco_trigger_invert']) ? (int) $meta['recoreco_trigger_invert'] : 0,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,49 @@ class RecoRecoModel extends Base
|
||||
// counter is FinanceBuddy's live installment_current instead.
|
||||
$count = isset($meta['recoreco_count']) && ctype_digit((string) $meta['recoreco_count']) ? (int) $meta['recoreco_count'] : 1;
|
||||
|
||||
// Trigger gate (cross-cutting, both modes): fire only while the card is in a triggering column,
|
||||
// and never while it sits in the target (Gate 2, uniform).
|
||||
$col = (int) $task['column_id'];
|
||||
$trig = isset($meta['recoreco_trigger_column']) ? $meta['recoreco_trigger_column'] : 'any';
|
||||
$invert = isset($meta['recoreco_trigger_invert']) && $meta['recoreco_trigger_invert'] == 1;
|
||||
$base = ($trig === 'any') ? true : ($invert ? ($col !== (int) $trig) : ($col === (int) $trig));
|
||||
|
||||
if (! ($base && $col !== $targetColumn)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Move-mode: relocate the single card to the target instead of spawning a clone. No back-fill,
|
||||
// no clone metadata. Honors the standalone limit (save() forced follow=0, so $limit is it).
|
||||
if (isset($meta['recoreco_move']) && $meta['recoreco_move'] == 1) {
|
||||
$cursor = (int) $task['date_due'];
|
||||
|
||||
if ($cursor - $daysBefore * 86400 >= $horizon) {
|
||||
return 0; // not due yet
|
||||
}
|
||||
|
||||
if ($limit > 0 && $count > $limit) {
|
||||
$this->completePlan($task, $task_id); // safety: re-armed after completion
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->taskPositionModel->movePosition($task['project_id'], $task_id, $targetColumn, 1, $task['swimlane_id'], false);
|
||||
|
||||
$next = $this->calculator()->occurrenceFrom($anchor, $frequency, $lastDay, $cursor, false);
|
||||
if ($next !== null && $next > $cursor) {
|
||||
$this->setDue($task_id, $next); // advance one period
|
||||
}
|
||||
|
||||
if ($limit > 0) {
|
||||
$count++;
|
||||
$this->setCount($task_id, $count);
|
||||
if ($count > $limit) {
|
||||
$this->completePlan($task, $task_id);
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Phase 1 -- walk forward from the cursor, collecting every occurrence whose fire time has
|
||||
// arrived. The loop exits on the first occurrence still ahead of the horizon, which is then
|
||||
// the "next future" one to park on.
|
||||
|
||||
@@ -62,7 +62,7 @@ class Plugin extends Base
|
||||
|
||||
public function getPluginVersion()
|
||||
{
|
||||
return '1.6.2';
|
||||
return '1.9.0';
|
||||
}
|
||||
|
||||
public function getPluginHomepage()
|
||||
|
||||
24
README.md
24
README.md
@@ -68,12 +68,32 @@ When a followed plan ends, the parked template is left one past the last install
|
||||
three-installment plan) as a visible "spent" marker, its recurrence off, with an activity-stream
|
||||
entry recording the completion.
|
||||
|
||||
## Trigger column and move-mode
|
||||
|
||||
Two options in the Recurring schedule dialog change *where* and *how* a card recurs:
|
||||
|
||||
- **Trigger column.** By default (`Any`) a card fires wherever it sits. Set a **Trigger column** and
|
||||
it only fires while parked there -- so a template spawns only from its "active" column, and moving
|
||||
it elsewhere quietly pauses it. The **`any but selected`** checkbox inverts the choice (fire in
|
||||
every column *except* the selected one, e.g. "any but Archive"). The **target column never
|
||||
triggers**, in any mode -- this is what lets a moved card come to rest in the target without
|
||||
re-firing. (One edge to know: a *duplicate* template dragged into its own target column no longer
|
||||
spawns; previously it fired from any column. Templates normally sit elsewhere, so this rarely
|
||||
bites.)
|
||||
- **Move instead of copy.** Tick **"Move the card instead of creating a copy"** and RecoReco
|
||||
relocates the *one* card to the target column each period instead of spawning a clone -- made for a
|
||||
recurring chore you don't want piling up copies. Pair it with a trigger column: e.g. trigger =
|
||||
`Done`, target = `Todo` -- cron moves the card `Done -> Todo` when it's due; you do the task and
|
||||
drag it back to `Done`; repeat. Move-mode honors the recurrence limit (stop after N moves) but not
|
||||
the FinanceBuddy follow (there is no copy to advance an installment on). Moves are silent (no
|
||||
"moved to column" notification each period).
|
||||
|
||||
## Status
|
||||
|
||||
All five frequencies work (yearly, monthly by day, monthly by weekday, weekly, daily) with the
|
||||
last-day rule, board recurrence icons, backfill with the 12-occurrence cap, the Run-now button, a
|
||||
recurrence limit (standalone or following FinanceBuddy installments), and the FinanceBuddy
|
||||
installment hand-off.
|
||||
recurrence limit (standalone or following FinanceBuddy installments), the FinanceBuddy installment
|
||||
hand-off, a per-card trigger column, and move-mode (relocate instead of copy).
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -46,9 +46,21 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<?= $this->form->label(t('Target column (where the copy appears)'), 'recoreco_target_column') ?>
|
||||
<?= $this->form->label(t('Target column'), 'recoreco_target_column') ?>
|
||||
<?= $this->form->select('recoreco_target_column', $columns_list, $values) ?>
|
||||
|
||||
<?= $this->form->label(t('Trigger column'), 'recoreco_trigger_column') ?>
|
||||
<div class="recoreco-trigger" style="display:flex; align-items:center; gap:8px;">
|
||||
<?= $this->form->select('recoreco_trigger_column', array('any' => t('Any')) + $all_columns, $values) ?>
|
||||
<label style="white-space:nowrap;">
|
||||
<input type="checkbox" name="recoreco_trigger_invert" value="1" <?= $values['recoreco_trigger_invert'] == 1 ? 'checked="checked"' : '' ?>> <?= t('any but selected') ?>
|
||||
</label>
|
||||
</div>
|
||||
<p class="form-help"><?= t('Recurrence fires only while the card sits in a triggering column; the target column never triggers.') ?></p>
|
||||
|
||||
<?= $this->form->checkbox('recoreco_move', t('Move the card instead of creating a copy'), 1, $values['recoreco_move'] == 1) ?>
|
||||
<p class="form-help"><?= t('Relocate this one card to the target column each period instead of spawning a copy.') ?></p>
|
||||
|
||||
<?= $this->form->label(t('Frequency'), 'recoreco_frequency') ?>
|
||||
<?= $this->form->select('recoreco_frequency', $frequency_list, $values) ?>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user