mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 02:52:33 -05:00
used. Continues #1173, the other direction of the same popup. Importing used to mean one thing: a whole new board, from the full-width page at All Boards. A swimlane's menu now imports a swimlane below that swimlane, a list's menu imports a list after it, and a card's menu imports a card below it, into the board that is already open. "After" is to the RIGHT in English and to the LEFT in Arabic, and that is one rule rather than two. The page carries dir (client/components/main/ layouts.jade), so the board's row of lists mirrors itself and "after in sort order" is already "the other side"; a direction branch in the placement would mirror it twice and put the list back where it started. The reason is written in models/lib/insertPosition.js, next to a negative test, because it looks like a missing branch. Placement is a fraction between the target's sort and the next one's, not a renumbering of every sibling - renumbering is what a board with ten thousand cards cannot afford and what two clients doing it at once get wrong. Several imported items spread evenly through that gap, so a swimlane of ten lists does not arrive with nine of them in the same position. The arithmetic is a pure module and is tested as arithmetic, junk input included. The file is what the export writes, at any scope, and the SAME checkboxes decide what comes in: a document full of comments imported with comments unticked brings the cards and leaves the comments. A .zip is unpacked in the browser and its wekan.json handed to the same method, so there is one import path and not two. Nothing is merged. Everything created is new, with new ids, because an import that half-updated a board would be an edit nobody could undo - importing the same file twice gives two copies, which is at least a thing you can delete. A custom field is matched by NAME, since an id from another board matches nothing here, and a comment comes back under the importing user, because the original author may not exist on this server. Importing WRITES, so unlike the exports beside it in the same popup it asks isBoardMember() rather than isVisibleBy(), the Admin Panel's import switch still applies, the format is checked, and the deadline that bounds a board import bounds this one. A read-only member is not offered it at all. tests/scopedImport.test.cjs covers the placement arithmetic and pins the selection, the write check and the single import path. Thanks to xet7 !
70 lines
3.0 KiB
JavaScript
70 lines
3.0 KiB
JavaScript
'use strict';
|
|
|
|
// WHERE an imported thing lands (#1173).
|
|
//
|
|
// A swimlane imported from a swimlane's menu goes BELOW that swimlane. A list
|
|
// imported from a list's menu goes after it - which is to its RIGHT in a
|
|
// left-to-right language and to its LEFT in a right-to-left one, and that is one
|
|
// rule rather than two: the page carries `dir` (client/components/main/
|
|
// layouts.jade), so the board's row of lists mirrors itself and "after in sort
|
|
// order" is already "the other side" in Arabic, Hebrew or Persian. Writing
|
|
// separate LTR and RTL branches here would mirror it twice and put the list back
|
|
// on the wrong side. A card goes below the card it was imported from.
|
|
//
|
|
// All three are the same question - what sort value sits between this one and
|
|
// the next - and `sort` is a Number in every one of the three schemas, so the
|
|
// answer is a fraction rather than a renumbering of every sibling. Renumbering
|
|
// is what a board with ten thousand cards cannot afford, and what a second
|
|
// client doing the same thing at the same time gets wrong.
|
|
|
|
// The sort values for `count` new items placed directly after `targetSort`,
|
|
// spread evenly in the gap before whatever comes next.
|
|
//
|
|
// existingSorts every sibling's sort, in any order
|
|
// targetSort the sort of the item they go after; null/undefined puts them
|
|
// at the very top, before everything
|
|
// count how many are being inserted
|
|
function sortsAfter(existingSorts, targetSort, count = 1) {
|
|
if (!Number.isFinite(count) || count < 1) return [];
|
|
|
|
const sorts = (existingSorts || [])
|
|
.filter(value => Number.isFinite(value))
|
|
.sort((a, b) => a - b);
|
|
|
|
// Nothing to sit between: start at 0 and count up, the way an empty board does.
|
|
if (sorts.length === 0) {
|
|
const from = Number.isFinite(targetSort) ? targetSort + 1 : 0;
|
|
return Array.from({ length: count }, (unused, index) => from + index);
|
|
}
|
|
|
|
if (!Number.isFinite(targetSort)) {
|
|
// Before everything, which is what "no target" means: below the top edge.
|
|
const first = sorts[0];
|
|
return Array.from({ length: count }, (unused, index) =>
|
|
first - (count - index));
|
|
}
|
|
|
|
const next = sorts.find(value => value > targetSort);
|
|
if (next === undefined) {
|
|
// Last: there is no gap to divide, so count on from the end.
|
|
return Array.from({ length: count }, (unused, index) => targetSort + 1 + index);
|
|
}
|
|
|
|
// Evenly through the gap. With one item that is the midpoint; with three it is
|
|
// the quarter points, so importing a swimlane of ten lists does not put nine
|
|
// of them in the same position.
|
|
const step = (next - targetSort) / (count + 1);
|
|
return Array.from({ length: count }, (unused, index) => targetSort + step * (index + 1));
|
|
}
|
|
|
|
// A gap can be divided only so many times before two sorts are the same double.
|
|
// When that happens the caller has to renumber; this says when.
|
|
function gapIsExhausted(sorts) {
|
|
for (let i = 1; i < sorts.length; i += 1) {
|
|
if (!(sorts[i] > sorts[i - 1])) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export { sortsAfter, gapIsExhausted };
|