Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle different locale #48

Merged
merged 19 commits into from
Nov 28, 2023
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## UNRELEASED

### Adds

* TODO:

### Changes

* Hide "Duplicates Detected." notification.
Expand Down
2 changes: 2 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"exporting": "Exporting {{ type }}...",
"import": "Import {{ type }}",
"importCleanFailed": "The cleaning of the imported file on server failed: {{ exportPath }}",
"importWithCurrentLocaleHeading": "Different locale detected",
"importWithCurrentLocaleDescription": "This file was exported from the \"{{ docsLocale }}\" locale. Are you sure you want to import it into the \"{{ currentLocale }}\" locale?",
"importDuplicateContinue": "Continue Import",
"importDuplicateDetected": "Duplicates Detected.",
"importDuplicateMessage": "Check the items you'd like to overwrite during import.",
Expand Down
117 changes: 98 additions & 19 deletions lib/methods/import.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,64 @@ const { EJSON } = require('bson');
module.exports = self => {
return {
async import(req, moduleName) {
const {
docs, attachmentsInfo, exportPath, filePath
} = await self.readExportFile(req);
if (!req.user) {
throw self.apos.error('forbidden');
}

const overrideLocale = self.apos.launder.boolean(req.body.overrideLocale);
let exportPath = self.apos.launder.string(req.body.exportPath);

let docs;
let attachmentsInfo;

if (exportPath) {
const filesData = await self.getFilesData(exportPath);

({ docs, attachmentsInfo } = filesData);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what means the fact that this method receives an exportPath? Does it mean that the gzip file has already been cleaned?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gzip file is now cleaned as soon as it's extracted (cf. comment below)

This method is provided exportPath when the user chooses Yes (continue the import with the current locale), because is archive has already been extracted during the import method first call.
It just early returned to display the confirmation modal.
So at the 2nd call, we simply give the method back the exportPath so it knows where the files were extracted during the 1st call

} else {
const exportFile = await self.readExportFile(req);

({ docs, attachmentsInfo } = exportFile);
exportPath = exportFile.exportPath;

await self.cleanFile(exportFile.filePath);
Copy link
Contributor Author

@ETLaurent ETLaurent Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we now clean the archive right after having extracted it so we don't have to think about removing it at several other places

}

const siteHasMultipleLocales = Object.keys(self.apos.i18n.locales).length > 1;
const differentDocsLocale = self.getFirstDifferentLocale(req, docs);

if (siteHasMultipleLocales && differentDocsLocale) {
if (!overrideLocale) {
// Display confirmation modal to ask the user if he wants to
// continue the import with the current locale overriding the docs one:
await self.apos.notify(req, ' ', {
type: 'warning',
event: {
name: 'import-locale-differs',
data: {
moduleName,
exportPath,
content: {
heading: req.t('aposImportExport:importWithCurrentLocaleHeading'),
description: req.t('aposImportExport:importWithCurrentLocaleDescription', {
docsLocale: differentDocsLocale,
currentLocale: req.locale
}),
negativeLabel: 'apostrophe:no',
affirmativeLabel: 'apostrophe:yes'
}
}
},
classes: [ 'apos-notification--hidden' ]
});

return;
}

// The user decided to continue the import (`overrideLocale` param sent)
self.rewriteDocsWithCurrentLocale(req, docs);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this method is called twice, another time with the confirmation to change locale?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is called only during the 2nd call (when the user chooses Yes), see the early return above

}

const total = docs.length + attachmentsInfo.length;
const {
reporting, jobId, notificationId
Expand All @@ -21,6 +72,7 @@ module.exports = self => {
const {
duplicatedDocs, duplicatedIds, failedIds
} = await self.insertDocs(req, docs, reporting);

const importedAttachments = await self.insertAttachments(req, {
attachmentsInfo,
reporting,
Expand Down Expand Up @@ -52,7 +104,6 @@ module.exports = self => {
});

await self.cleanFile(exportPath);
await self.cleanFile(filePath);
return;
}

Expand Down Expand Up @@ -87,8 +138,6 @@ module.exports = self => {
classes: [ 'apos-notification--hidden' ]
});

self.cleanFile(filePath);

return results;
},

Expand Down Expand Up @@ -180,6 +229,36 @@ module.exports = self => {
};
},

getFirstDifferentLocale(req, docs) {
const doc = docs
.find(doc => self.isLocaleDifferent(req, doc));

return doc && self.stripOutMode(doc.aposLocale);
},

rewriteDocsWithCurrentLocale(req, docs) {
for (const doc of docs) {
if (self.isLocaleDifferent(req, doc)) {
self.replaceLocale(req, doc);
}
}
},

isLocaleDifferent(req, doc) {
return doc.aposLocale && self.stripOutMode(doc.aposLocale) !== req.locale;
},

replaceLocale(req, doc) {
const [ id, , mode ] = doc._id.split(':');

doc._id = [ id, req.locale, mode ].join(':');
doc.aposLocale = [ req.locale, mode ].join(':');
},

stripOutMode(aposLocale) {
return aposLocale.split(':').at(0);
},

async insertDocs(req, docs, reporting) {
const duplicatedDocs = [];
const duplicatedIds = [];
Expand Down Expand Up @@ -342,20 +421,20 @@ module.exports = self => {
}
},

async cleanFile(exportPath) {
async cleanFile(path) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor change: cleanFile also cleans the archive path

try {
const stat = await fsp.lstat(exportPath);
const stat = await fsp.lstat(path);
if (stat.isDirectory()) {
await fsp.rm(exportPath, {
await fsp.rm(path, {
recursive: true,
force: true
});
} else {
await fsp.unlink(exportPath);
await fsp.unlink(path);
}
} catch (err) {
self.apos.util.error(
`Error while trying to remove the file or folder: ${exportPath}. You might want to remove it yourself.`
`Error while trying to remove the file or folder: ${path}. You might want to remove it yourself.`
);
}
},
Expand Down Expand Up @@ -428,15 +507,15 @@ module.exports = self => {
const jobId = self.apos.launder.string(req.body.jobId);
const notificationId = self.apos.launder.string(req.body.notificationId);

const jobManager = self.apos.modules['@apostrophecms/job'];
const job = await jobManager.db.findOne({ _id: jobId });

jobManager.end(job, true);
if (jobId) {
const jobManager = self.apos.modules['@apostrophecms/job'];
const job = await jobManager.db.findOne({ _id: jobId });
jobManager.end(job, true);
}
if (notificationId) {
self.apos.notification.dismiss(req, notificationId, 2000).catch(self.apos.util.error);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clean job and notification only if existing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see /api/v1/@apostrophecms/import-export/clean-export call in app.js below, no job nor notification is instantiated at this step, but we still want to remove the export path


self.apos.notification.dismiss(req, notificationId, 2000)
.catch((error) => {
self.apos.util.error(error);
});
await self.cleanFile(exportPath);
}
};
Expand Down
41 changes: 41 additions & 0 deletions ui/apos/apps/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default () => {
apos.bus.$on('export-download', openUrl);
apos.bus.$on('import-started', addBeforeUnloadListener);
apos.bus.$on('import-ended', removeBeforeUnloadListener);
apos.bus.$on('import-locale-differs', handleDifferentLocale);
apos.bus.$on('import-duplicates', handleDuplicates);
}
});
Expand All @@ -25,6 +26,46 @@ export default () => {
window.removeEventListener('beforeunload', warningImport);
}

async function handleDifferentLocale(event) {
const continueImport = await apos.modal.execute('AposModalConfirm', event);

if (continueImport) {
try {
const moduleAction = apos.modules[event.moduleName].action;

await apos.http.post(`${moduleAction}/import`, {
body: {
overrideLocale: true,
exportPath: event.exportPath
}
});
} catch (error) {
apos.notify('aposImportExport:importFailed', {
type: 'danger',
dismiss: true
});
}

return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we agree that in this case the export file is cleaned from the import route directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it should be done at line 107, as in a "normal" case where no locale overriding is involved

}

// If not, we still need to clean the uploaded archive
try {
await apos.http.post('/api/v1/@apostrophecms/import-export/clean-export', {
body: {
exportPath: event.exportPath
}
});
} catch (error) {
apos.notify('aposImportExport:importCleanFailed', {
type: 'warning',
interpolate: {
exportPath: event.exportPath
}
});
}
}

async function handleDuplicates(event) {
if (event.duplicatedDocs.length) {
await apos.modal.execute('AposDuplicateImportModal', event);
Expand Down
4 changes: 2 additions & 2 deletions ui/apos/components/AposDuplicateImportModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export default {
}
});
} catch (error) {
apos.notify(this.$t('aposImportExport:importCleanFailed'), {
apos.notify('aposImportExport:importCleanFailed', {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix interpolation

before after
Screenshot 2023-11-21 at 18 00 02 Screenshot 2023-11-21 at 17 59 38
ell

type: 'warning',
interpolate: {
exportPath: this.exportPath
Expand All @@ -217,7 +217,7 @@ export default {
jobId: this.jobId
}
}).catch(() => {
apos.notify(this.$t('aposImportExport:exportFailed'), {
apos.notify('aposImportExport:exportFailed', {
type: 'danger',
dismiss: true
});
Expand Down
2 changes: 1 addition & 1 deletion ui/apos/components/AposExportModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export default {
}
});
} catch (error) {
apos.notify(this.$t('aposImportExport:exportFailed'), {
apos.notify('aposImportExport:exportFailed', {
type: 'danger',
dismiss: true
});
Expand Down
2 changes: 1 addition & 1 deletion ui/apos/components/AposImportModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export default {
apos.http.post(`${this.moduleAction}/${this.action}`, {
body: formData
}).catch(() => {
apos.notify(this.$t('aposImportExport:importFailed'), {
apos.notify('aposImportExport:importFailed', {
type: 'danger',
dismiss: true
});
Expand Down