From f4c552a79fcd7254bb21c474c59ed8c9707ff204 Mon Sep 17 00:00:00 2001 From: konrad Date: Sat, 4 Sep 2021 19:26:38 +0000 Subject: [PATCH] User Data Export and import (#699) Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/699 Co-authored-by: konrad Co-committed-by: konrad --- public/images/migration/vikunja-file.png | Bin 0 -> 13808 bytes src/components/migrator/migration.vue | 75 +++++++++++++++--- src/components/tasks/partials/attachments.vue | 2 +- src/components/user/settings/data-export.vue | 71 +++++++++++++++++ src/helpers/downloadBlob.ts | 7 ++ src/helpers/migrator.ts | 38 +++++++++ src/i18n/lang/en.json | 15 +++- src/router/index.js | 6 ++ src/services/abstractService.js | 13 +++ src/services/attachment.js | 18 +---- src/services/dataExport.js | 13 +++ ...grationService.js => abstractMigration.js} | 0 .../migrator/abstractMigrationFile.js | 31 ++++++++ src/views/migrator/Migrate.vue | 13 ++- src/views/migrator/MigrateService.vue | 30 +++---- src/views/user/DataExportDownload.vue | 64 +++++++++++++++ src/views/user/Settings.vue | 5 ++ 17 files changed, 347 insertions(+), 54 deletions(-) create mode 100644 public/images/migration/vikunja-file.png create mode 100644 src/components/user/settings/data-export.vue create mode 100644 src/helpers/downloadBlob.ts create mode 100644 src/helpers/migrator.ts create mode 100644 src/services/dataExport.js rename src/services/migrator/{abstractMigrationService.js => abstractMigration.js} (100%) create mode 100644 src/services/migrator/abstractMigrationFile.js create mode 100644 src/views/user/DataExportDownload.vue diff --git a/public/images/migration/vikunja-file.png b/public/images/migration/vikunja-file.png new file mode 100644 index 0000000000000000000000000000000000000000..ee9eb158395df57c0d891241f35aeb3e9b89c270 GIT binary patch literal 13808 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 O0_3EWB&#G$g8v63C39N< literal 0 HcmV?d00001 diff --git a/src/components/migrator/migration.vue b/src/components/migrator/migration.vue index d24c08bd..35a613f1 100644 --- a/src/components/migrator/migration.vue +++ b/src/components/migrator/migration.vue @@ -1,16 +1,34 @@ diff --git a/src/components/tasks/partials/attachments.vue b/src/components/tasks/partials/attachments.vue index d49409f2..aa93494f 100644 --- a/src/components/tasks/partials/attachments.vue +++ b/src/components/tasks/partials/attachments.vue @@ -57,7 +57,7 @@ @click.prevent.stop="downloadAttachment(a)" v-tooltip="$t('task.attachment.downloadTooltip')" > - {{ $t('task.attachment.download') }} + {{ $t('misc.download') }} + +

+ {{ $t('user.export.description') }} +

+

+ {{ $t('user.export.descriptionPasswordRequired') }} +

+
+ +
+ +
+

+ {{ $t('user.deletion.passwordRequired') }} +

+
+ + + {{ $t('user.export.request') }} + +
+ + + diff --git a/src/helpers/downloadBlob.ts b/src/helpers/downloadBlob.ts new file mode 100644 index 00000000..94adcbfe --- /dev/null +++ b/src/helpers/downloadBlob.ts @@ -0,0 +1,7 @@ +export const downloadBlob = (url: string, filename: string) => { + const link = document.createElement('a') + link.href = url + link.setAttribute('download', filename) + link.click() + window.URL.revokeObjectURL(url) +} \ No newline at end of file diff --git a/src/helpers/migrator.ts b/src/helpers/migrator.ts new file mode 100644 index 00000000..8320249d --- /dev/null +++ b/src/helpers/migrator.ts @@ -0,0 +1,38 @@ +export interface Migrator { + name: string + identifier: string + isFileMigrator?: boolean +} + +export const getMigratorFromSlug = (slug: string): Migrator => { + switch (slug) { + case 'wunderlist': + return { + name: 'Wunderlist', + identifier: 'wunderlist', + } + case 'todoist': + return { + name: 'Todoist', + identifier: 'todoist', + } + case 'trello': + return { + name: 'Trello', + identifier: 'trello', + } + case 'microsoft-todo': + return { + name: 'Microsoft Todo', + identifier: 'microsoft-todo', + } + case 'vikunja-file': + return { + name: 'Vikunja Export', + identifier: 'vikunja-file', + isFileMigrator: true, + } + default: + throw Error('Unknown migrator slug ' + slug) + } +} \ No newline at end of file diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json index 0fcf94f3..29788865 100644 --- a/src/i18n/lang/en.json +++ b/src/i18n/lang/en.json @@ -111,6 +111,13 @@ "scheduledCancelText": "To cancel the deletion of your account, please enter your password below:", "scheduledCancelConfirm": "Cancel the deletion of my account", "scheduledCancelSuccess": "We will not delete your account." + }, + "export": { + "title": "Export your Vikunja Data", + "description": "You can request a copy of all your Vikunja data. This include Namespaces, Lists, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.", + "descriptionPasswordRequired": "Please enter your password to proceed:", + "request": "Request a copy of my Vikunja Data", + "success": "You've successfully requested your Vikunja Data! We will send you an email once it's ready to download." } }, "list": { @@ -371,7 +378,9 @@ "inProgress": "Importing in progress…", "alreadyMigrated1": "It looks like you've already imported your stuff from {name} at {date}.", "alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?", - "confirm": "I am sure, please start migrating now!" + "confirm": "I am sure, please start migrating now!", + "importUpload": "To import data from {name} into Vikunja, click the button below to select a file.", + "upload": "Upload file" }, "label": { "title": "Labels", @@ -432,7 +441,8 @@ "saving": "Saving…", "saved": "Saved!", "default": "Default", - "close": "Close" + "close": "Close", + "download": "Download" }, "input": { "resetColor": "Reset Color", @@ -563,7 +573,6 @@ "attachment": { "title": "Attachments", "createdBy": "created {0} by {1}", - "download": "Download", "downloadTooltip": "Download this attachment", "upload": "Upload attachment", "drop": "Drop files here to upload", diff --git a/src/router/index.js b/src/router/index.js index 493d0a5e..a485ab80 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -10,6 +10,7 @@ import About from '../views/About' import LoginComponent from '../views/user/Login' import RegisterComponent from '../views/user/Register' import OpenIdAuth from '../views/user/OpenIdAuth' +import DataExportDownload from '../views/user/DataExportDownload' // Tasks import ShowTasksInRangeComponent from '../views/tasks/ShowTasksInRange' import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth' @@ -149,6 +150,11 @@ export default new Router({ name: 'user.settings', component: UserSettingsComponent, }, + { + path: '/user/export/download', + name: 'user.export.download', + component: DataExportDownload, + }, { path: '/share/:share/auth', name: 'link-share.auth', diff --git a/src/services/abstractService.js b/src/services/abstractService.js index 7855421c..50284b16 100644 --- a/src/services/abstractService.js +++ b/src/services/abstractService.js @@ -319,6 +319,17 @@ export default class AbstractService { }) } + getBlobUrl(url, method = 'GET', data = {}) { + return this.http({ + url: url, + method: method, + responseType: 'blob', + data: data, + }).then(response => { + return window.URL.createObjectURL(new Blob([response.data])) + }) + } + /** * Performs a get request to the url specified before. * The difference between this and get() is this one is used to get a bunch of data (an array), not just a single object. @@ -487,6 +498,8 @@ export default class AbstractService { * @returns {Q.Promise} */ uploadFormData(url, formData) { + console.log(formData, formData._boundary) + const cancel = this.setLoading() return this.http.put( url, diff --git a/src/services/attachment.js b/src/services/attachment.js index b0fa38ba..afd28cdc 100644 --- a/src/services/attachment.js +++ b/src/services/attachment.js @@ -1,6 +1,7 @@ import AbstractService from './abstractService' import AttachmentModel from '../models/attachment' import {formatISO} from 'date-fns' +import {downloadBlob} from '@/helpers/downloadBlob' export default class AttachmentService extends AbstractService { constructor() { @@ -33,23 +34,12 @@ export default class AttachmentService extends AbstractService { } getBlobUrl(model) { - return this.http({ - url: '/tasks/' + model.taskId + '/attachments/' + model.id, - method: 'GET', - responseType: 'blob', - }).then(response => { - return window.URL.createObjectURL(new Blob([response.data])) - }) + return AbstractService.prototype.getBlobUrl.call(this, '/tasks/' + model.taskId + '/attachments/' + model.id) } download(model) { - this.getBlobUrl(model).then(url => { - const link = document.createElement('a') - link.href = url - link.setAttribute('download', model.file.name) - link.click() - window.URL.revokeObjectURL(url) - }) + this.getBlobUrl(model) + .then(url => downloadBlob(url, model.file.name)) } /** diff --git a/src/services/dataExport.js b/src/services/dataExport.js new file mode 100644 index 00000000..22b43693 --- /dev/null +++ b/src/services/dataExport.js @@ -0,0 +1,13 @@ +import AbstractService from './abstractService' +import {downloadBlob} from '../helpers/downloadBlob' + +export default class DataExportService extends AbstractService { + request(password) { + return this.post('/user/export/request', {password: password}) + } + + download(password) { + return this.getBlobUrl('/user/export/download', 'POST', {password}) + .then(url => downloadBlob(url, 'vikunja-export.zip')) + } +} \ No newline at end of file diff --git a/src/services/migrator/abstractMigrationService.js b/src/services/migrator/abstractMigration.js similarity index 100% rename from src/services/migrator/abstractMigrationService.js rename to src/services/migrator/abstractMigration.js diff --git a/src/services/migrator/abstractMigrationFile.js b/src/services/migrator/abstractMigrationFile.js new file mode 100644 index 00000000..dba20937 --- /dev/null +++ b/src/services/migrator/abstractMigrationFile.js @@ -0,0 +1,31 @@ +import AbstractService from '../abstractService' + +// This service builds on top of the abstract service and basically just hides away method names. +// It enables migration services to be created with minimal overhead and even better method names. +export default class AbstractMigrationFileService extends AbstractService { + serviceUrlKey = '' + + constructor(serviceUrlKey) { + super({ + create: '/migration/' + serviceUrlKey + '/migrate', + }) + this.serviceUrlKey = serviceUrlKey + } + + getStatus() { + return this.getM('/migration/' + this.serviceUrlKey + '/status') + } + + useCreateInterceptor() { + return false + } + + migrate(file) { + console.log(file) + return this.uploadFile( + this.paths.create, + file, + 'import', + ) + } +} diff --git a/src/views/migrator/Migrate.vue b/src/views/migrator/Migrate.vue index 32b80ada..adab41fb 100644 --- a/src/views/migrator/Migrate.vue +++ b/src/views/migrator/Migrate.vue @@ -3,15 +3,20 @@

{{ $t('migrate.title') }}

{{ $t('migrate.description') }}

- - - {{ m }} + + + {{ m.name }}
diff --git a/src/views/user/Settings.vue b/src/views/user/Settings.vue index a66ac649..58c8ca63 100644 --- a/src/views/user/Settings.vue +++ b/src/views/user/Settings.vue @@ -235,6 +235,9 @@ + + + @@ -293,6 +296,7 @@ import AvatarSettings from '../../components/user/avatar-settings.vue' import copy from 'copy-to-clipboard' import ListSearch from '@/components/tasks/partials/listSearch.vue' import UserSettingsDeletion from '../../components/user/settings/deletion' +import DataExport from '../../components/user/settings/data-export' export default { name: 'Settings', @@ -325,6 +329,7 @@ export default { UserSettingsDeletion, ListSearch, AvatarSettings, + DataExport, }, created() { this.passwordUpdateService = new PasswordUpdateService()