mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 01:20:07 -05:00
Fix #6506: restore the import "map members" step; unmapped members become virtual users, not the importer.
Regression from c0a1a2784 (v10.12): it dropped `importMapMembers` from the import
wizard's `steps` array and hardcoded an empty membersMapping in finishImport, so the
map-users screen never rendered and every imported card/member/comment was assigned
to the importing user even when the person existed in WeKan.
Client (client/components/import/import.js):
- restore `steps = ['importTextarea', 'importMapMembers']` so the (auto-suggesting)
map-members step shows again;
- finishImport builds membersMapping from the members the user mapped
(member.wekanId), instead of sending `{}`. The step stays OPTIONAL — the Skip
button and the textarea "import without mapping" button still bypass it.
Server — unmapped members become virtual users instead of collapsing onto the importer:
- models/trelloCreator.js: new createPlaceholderUsers(board), run in create() before
the board is built, so it covers BOTH the Trello JSON import and the Trello API
(all-boards background) import. For each member not already mapped it maps to an
existing WeKan user by username / recorded importUsername, else inserts an inert
virtual user (loginDisabled, isActive:false, authenticationMethod:'imported')
reusing the Trello member id, so board membership and authorship keep the original
person;
- models/wekanCreator.js: createPlaceholderUsers no longer overwrites an explicit UI
mapping (skip members already in this.members) so a member mapped to a real account
is not clobbered by the identity placeholder.
A board admin can still map a virtual member to a real user later from the sidebar
(that feature is unchanged). Tests updated: tests/importVirtualMembers.test.cjs and
tests/importMemberMapping.test.cjs.
Thanks to AmigaAbattoir and xet7 !
Fixes #6506,
This commit is contained in:
1 parent
a1f93c8015
commit
0e0391f6c1
5 files changed
+132
-19
No files matched your search
@@ -173,11 +173,13 @@ function assignBoardToNamedWorkspace(boardId, wsName, parentId = null) {
|
||||
|
||||
Template.import.onCreated(function () {
|
||||
this.error = new ReactiveVar('');
|
||||
// Import never asks for member mapping up front: every imported member is brought
|
||||
// in as a virtual (placeholder) user carrying its avatar / username / full name,
|
||||
// and a board admin maps a virtual member to an existing board user later from the
|
||||
// sidebar member-avatar popup. So there is a single step; import runs immediately.
|
||||
this.steps = ['importTextarea'];
|
||||
// #6506: import shows the "map members" step so imported members can be mapped to
|
||||
// EXISTING WeKan users (auto-suggested by username). It is OPTIONAL — a Skip button
|
||||
// (and the textarea "import without mapping" button) bypasses it. Whatever is not
|
||||
// mapped is brought in as a virtual (placeholder) user carrying its username / full
|
||||
// name — NOT collapsed onto the importing user — and a board admin can still map a
|
||||
// virtual member to an existing user later from the sidebar member-avatar popup.
|
||||
this.steps = ['importTextarea', 'importMapMembers'];
|
||||
this._currentStepIndex = new ReactiveVar(0);
|
||||
this.importedData = new ReactiveVar();
|
||||
this.membersToMap = new ReactiveVar([]);
|
||||
@@ -315,10 +317,17 @@ Template.import.onCreated(function () {
|
||||
};
|
||||
|
||||
this.finishImport = async () => {
|
||||
// No import-time member mapping: every imported member becomes a virtual
|
||||
// (placeholder) user. Mapping a virtual member to an existing board user is done
|
||||
// later from the board sidebar (privilege-bounded), so pass an empty mapping.
|
||||
// #6506: build the member mapping from the (optional) map-members step —
|
||||
// { importedMemberId: existingWekanUserId } for members the user mapped (or that
|
||||
// auto-matched by username). Members left unmapped, or skipped entirely, are sent
|
||||
// WITHOUT an entry: the server creator brings them in as virtual (placeholder)
|
||||
// users instead of collapsing them onto the importing user.
|
||||
const mappingById = {};
|
||||
(this.membersToMap.get() || []).forEach(member => {
|
||||
if (member && member.id && member.wekanId) {
|
||||
mappingById[member.id] = member.wekanId;
|
||||
}
|
||||
});
|
||||
const importedData = this.importedData.get();
|
||||
|
||||
// Trello: import over HTTP (see postTrelloImport) so the realtime DDP
|
||||
|
||||
@@ -1026,6 +1026,11 @@ export class TrelloCreator {
|
||||
const currentBoard = await ReactiveCache.getBoard(currentBoardId);
|
||||
await currentBoard.archive();
|
||||
}
|
||||
// #6506: bring every imported member in as its own user (mapped to an existing
|
||||
// account or a virtual placeholder) BEFORE creating the board, so board members,
|
||||
// card authors, comments and activities keep the original person instead of
|
||||
// collapsing onto the importer.
|
||||
await this.createPlaceholderUsers(board);
|
||||
this.parseActions(board.actions);
|
||||
const boardId = await this.createBoardAndLabels(board);
|
||||
// Create the default swimlane first so lists can be attached to it (see
|
||||
@@ -1041,6 +1046,79 @@ export class TrelloCreator {
|
||||
return boardId;
|
||||
}
|
||||
|
||||
// Pick a free username: keep the original if it is not taken, else suffix it so we
|
||||
// never rename/steal a different existing account's username.
|
||||
async _uniquePlaceholderUsername(base) {
|
||||
const name = (String(base || '').trim()) || 'imported';
|
||||
let candidate = name;
|
||||
let n = 0;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
while (await Meteor.users.findOneAsync({ username: candidate }, { fields: { _id: 1 } })) {
|
||||
n += 1;
|
||||
candidate = `${name}-${n}`;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
// #6506: for every Trello member NOT already mapped (by the map-members step or the
|
||||
// API importer's buildMembersMapping), either map it to an EXISTING WeKan user with
|
||||
// the same username / recorded importUsername, or create an inert virtual
|
||||
// (placeholder) user carrying the original username / full name. This keeps imported
|
||||
// authorship on the original person instead of the importing user, for BOTH the
|
||||
// Trello JSON import and the Trello API (all-boards) import. Placeholders cannot log
|
||||
// in (loginDisabled), are inactive until reconciled (isActive:false) and carry no
|
||||
// secrets. Server-only; best-effort so one bad member never fails the import.
|
||||
async createPlaceholderUsers(board) {
|
||||
if (!Meteor.isServer) return;
|
||||
for (const member of board.members || []) {
|
||||
if (!member || !member.id) continue;
|
||||
if (this.members[member.id]) continue; // already mapped to an existing user
|
||||
|
||||
if (member.username) {
|
||||
const existing =
|
||||
(await ReactiveCache.getUser({ username: member.username })) ||
|
||||
(await ReactiveCache.getUser({ importUsernames: member.username }));
|
||||
if (existing) {
|
||||
this.members[member.id] = existing._id;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Reuse the Trello member id as the WeKan user _id so every reference resolves.
|
||||
const existingById = await ReactiveCache.getUser(member.id);
|
||||
if (!existingById) {
|
||||
const username = await this._uniquePlaceholderUsername(
|
||||
member.username || member.fullName || `imported-${member.id}`,
|
||||
);
|
||||
try {
|
||||
// .direct bypasses the after.insert hooks (Sandstorm identity sync, the
|
||||
// registration/invitation gate) that would reject a non-account placeholder.
|
||||
await Users.direct.insertAsync({
|
||||
_id: member.id,
|
||||
username,
|
||||
profile: {
|
||||
fullname: member.fullName || '',
|
||||
initials: String(member.initials || '').slice(0, 4),
|
||||
},
|
||||
authenticationMethod: 'imported',
|
||||
loginDisabled: true,
|
||||
isActive: false,
|
||||
importUsernames: member.username ? [member.username] : [],
|
||||
importedFromBoardId: board.id || null,
|
||||
importedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
services: {},
|
||||
});
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.warn('trello placeholder user insert:', member.id, e && e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.members[member.id] = member.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the original Trello usernames so user mapping can happen later:
|
||||
// members that were mapped to a WeKan user get the Trello username added to
|
||||
// that user's importUsernames (so future imports auto-map); members that were
|
||||
|
||||
@@ -1317,6 +1317,10 @@ export class WekanCreator {
|
||||
|
||||
for (const u of users) {
|
||||
if (!u || !u._id) continue;
|
||||
// #6506: a member the map-members step mapped to an EXISTING WeKan user is
|
||||
// already in this.members (id -> real user id); keep that and do NOT create a
|
||||
// placeholder or overwrite it with the identity mapping below.
|
||||
if (this.members[u._id]) continue;
|
||||
const profile = u.profile || {};
|
||||
let existing = await ReactiveCache.getUser(u._id);
|
||||
if (!existing) {
|
||||
|
||||
@@ -30,7 +30,7 @@ test('selecting a searched user maps its _id (not the never-set __originalId)',
|
||||
// The click handler maps the clicked user document by _id.
|
||||
const clickIdx = js.indexOf("'click .js-select-import'");
|
||||
assert.ok(clickIdx > -1, 'the select-import click handler exists');
|
||||
const clickBlock = js.slice(clickIdx, clickIdx + 300);
|
||||
const clickBlock = js.slice(clickIdx, clickIdx + 600);
|
||||
assert.ok(/mapSelectedMember|importMapToUser/.test(clickBlock), 'the click maps the selected member');
|
||||
assert.ok(/Template\.currentData\(\)\._id/.test(js), 'maps the search result by its _id');
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
// Wiring guards for the "import always creates virtual users, map later" feature:
|
||||
// - Import never asks for member mapping up front (single step, empty mapping), so
|
||||
// every imported member is brought in as a virtual (placeholder) user.
|
||||
// Wiring guards for import member handling:
|
||||
// - #6506: import shows the OPTIONAL "map members" step so imported members can be
|
||||
// mapped to existing WeKan users; members left unmapped (or skipped) are brought in
|
||||
// as virtual (placeholder) users, NOT collapsed onto the importing user.
|
||||
// - Import is bounded by a hang-mitigation watchdog (client + server) so it can never
|
||||
// spin forever.
|
||||
// - A board admin maps a virtual member to an existing real board user LATER from the
|
||||
// sidebar member-avatar popup, via a board-scoped, no-escalation server method.
|
||||
// - A board admin can also map a virtual member to an existing real board user LATER
|
||||
// from the sidebar member-avatar popup, via a board-scoped, no-escalation method.
|
||||
//
|
||||
// Run: node tests/importVirtualMembers.test.cjs
|
||||
|
||||
@@ -20,13 +21,34 @@ const read = rel => fs.readFileSync(path.join(__dirname, '..', rel), 'utf8');
|
||||
|
||||
console.log('importVirtualMembers:');
|
||||
|
||||
test('import runs immediately with no member-mapping step', () => {
|
||||
test('#6506: import shows the map-members step and builds the mapping from it', () => {
|
||||
const js = read('client/components/import/import.js');
|
||||
assert.ok(/this\.steps = \['importTextarea'\]/.test(js), 'only the textarea step remains (no importMapMembers step)');
|
||||
// finishImport must not build a mapping from membersToMap — every member is virtual.
|
||||
assert.ok(/this\.steps = \['importTextarea', 'importMapMembers'\]/.test(js),
|
||||
'the wizard includes the importMapMembers step again');
|
||||
// finishImport builds the mapping from the members the user mapped (member.wekanId).
|
||||
const fi = js.slice(js.indexOf('this.finishImport'));
|
||||
assert.ok(/const mappingById = \{\};/.test(fi), 'finishImport passes an empty membersMapping');
|
||||
assert.ok(!/membersMapping\.forEach/.test(fi), 'no per-member wekanId mapping is applied at import');
|
||||
assert.ok(/this\.membersToMap\.get\(\)/.test(fi) && /member\.wekanId/.test(fi),
|
||||
'finishImport builds membersMapping from the mapped members');
|
||||
assert.ok(/mappingById\[member\.id\] = member\.wekanId/.test(fi),
|
||||
'each mapped member maps its imported id to the chosen existing user');
|
||||
// The skip button and the textarea "without mapping" button both still bypass it.
|
||||
assert.ok(/js-import-skip-mapping/.test(js) && /js-import-without-mapping/.test(js),
|
||||
'skip / import-without-mapping remain available');
|
||||
});
|
||||
|
||||
test('#6506: unmapped Trello members become virtual users (both JSON and API import)', () => {
|
||||
const tc = read('models/trelloCreator.js');
|
||||
assert.ok(/async createPlaceholderUsers\(board\)/.test(tc), 'trelloCreator creates placeholder users');
|
||||
assert.ok(/await this\.createPlaceholderUsers\(board\)/.test(tc), 'create() runs it before building the board');
|
||||
// Unmapped members map to an existing user by username, else a virtual placeholder.
|
||||
assert.ok(/if \(this\.members\[member\.id\]\) continue;/.test(tc), 'already-mapped members are kept');
|
||||
assert.ok(/ReactiveCache\.getUser\(\{ username: member\.username \}\)/.test(tc), 'auto-maps by username');
|
||||
assert.ok(/loginDisabled: true/.test(tc) && /isActive: false/.test(tc) && /authenticationMethod: 'imported'/.test(tc),
|
||||
'placeholders are inert imported users');
|
||||
// wekanCreator keeps an explicit UI mapping instead of overwriting it with a placeholder.
|
||||
const wc = read('models/wekanCreator.js');
|
||||
assert.ok(/if \(this\.members\[u\._id\]\) continue;/.test(wc),
|
||||
'wekanCreator does not clobber an explicit mapping with the identity placeholder');
|
||||
});
|
||||
|
||||
test('import is bounded by a client-side watchdog (no infinite spinner)', () => {
|
||||
|
||||
Reference in new issue
Block a user