feat: MigrateService script setup (#2432)
Co-authored-by: Dominik Pschenitschni <mail@celement.de> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2432 Reviewed-by: konrad <k@knt.li> Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de> Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
@ -31,8 +31,8 @@ import ListTeamsComponent from '../views/teams/ListTeams.vue'
|
|||||||
import ListLabelsComponent from '../views/labels/ListLabels.vue'
|
import ListLabelsComponent from '../views/labels/ListLabels.vue'
|
||||||
import NewLabelComponent from '../views/labels/NewLabel.vue'
|
import NewLabelComponent from '../views/labels/NewLabel.vue'
|
||||||
// Migration
|
// Migration
|
||||||
import MigrationComponent from '../views/migrator/Migrate.vue'
|
const MigrationComponent = () => import('@/views/migrate/Migration.vue')
|
||||||
import MigrateServiceComponent from '../views/migrator/MigrateService.vue'
|
const MigrationHandlerComponent = () => import('@/views/migrate/MigrationHandler.vue')
|
||||||
// List Views
|
// List Views
|
||||||
import ListList from '../views/list/ListList.vue'
|
import ListList from '../views/list/ListList.vue'
|
||||||
const ListGantt = () => import('../views/list/ListGantt.vue')
|
const ListGantt = () => import('../views/list/ListGantt.vue')
|
||||||
@ -445,7 +445,11 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/migrate/:service',
|
path: '/migrate/:service',
|
||||||
name: 'migrate.service',
|
name: 'migrate.service',
|
||||||
component: MigrateServiceComponent,
|
component: MigrationHandlerComponent,
|
||||||
|
props: route => ({
|
||||||
|
service: route.params.service as string,
|
||||||
|
code: route.params.code as string,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/filters/new',
|
path: '/filters/new',
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import AbstractService from '../abstractService'
|
import AbstractService from '../abstractService'
|
||||||
|
|
||||||
|
export type MigrationConfig = { code: string }
|
||||||
|
|
||||||
// This service builds on top of the abstract service and basically just hides away method names.
|
// 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.
|
// It enables migration services to be created with minimal overhead and even better method names.
|
||||||
export default class AbstractMigrationService extends AbstractService {
|
export default class AbstractMigrationService extends AbstractService<MigrationConfig> {
|
||||||
serviceUrlKey = ''
|
serviceUrlKey = ''
|
||||||
|
|
||||||
constructor(serviceUrlKey) {
|
constructor(serviceUrlKey: string) {
|
||||||
super({
|
super({
|
||||||
update: '/migration/' + serviceUrlKey + '/migrate',
|
update: '/migration/' + serviceUrlKey + '/migrate',
|
||||||
})
|
})
|
||||||
@ -20,7 +22,7 @@ export default class AbstractMigrationService extends AbstractService {
|
|||||||
return this.getM('/migration/' + this.serviceUrlKey + '/status')
|
return this.getM('/migration/' + this.serviceUrlKey + '/status')
|
||||||
}
|
}
|
||||||
|
|
||||||
migrate(data) {
|
migrate(data: MigrationConfig) {
|
||||||
return this.update(data)
|
return this.update(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import type {IFile} from '@/modelTypes/IFile'
|
|
||||||
import AbstractService from '../abstractService'
|
import AbstractService from '../abstractService'
|
||||||
|
|
||||||
// This service builds on top of the abstract service and basically just hides away method names.
|
// This service builds on top of the abstract service and basically just hides away method names.
|
||||||
@ -21,7 +20,7 @@ export default class AbstractMigrationFileService extends AbstractService {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
migrate(file: IFile) {
|
migrate(file: File) {
|
||||||
return this.uploadFile(
|
return this.uploadFile(
|
||||||
this.paths.create,
|
this.paths.create,
|
||||||
file,
|
file,
|
||||||
|
@ -14,9 +14,9 @@
|
|||||||
type="file"
|
type="file"
|
||||||
/>
|
/>
|
||||||
<x-button
|
<x-button
|
||||||
:loading="migrationService.loading"
|
:loading="migrationFileService.loading"
|
||||||
:disabled="migrationService.loading || undefined"
|
:disabled="migrationFileService.loading || undefined"
|
||||||
@click="$refs.uploadInput.click()"
|
@click="uploadInput?.click()"
|
||||||
>
|
>
|
||||||
{{ $t('migrate.upload') }}
|
{{ $t('migrate.upload') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
@ -57,129 +57,118 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<message class="mb-4">
|
<Message class="mb-4">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</message>
|
</Message>
|
||||||
<x-button :to="{name: 'home'}">{{ $t('misc.refresh') }}</x-button>
|
<x-button :to="{name: 'home'}">{{ $t('misc.refresh') }}</x-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent} from 'vue'
|
export default {
|
||||||
|
|
||||||
import AbstractMigrationService from '@/services/migrator/abstractMigration'
|
|
||||||
import AbstractMigrationFileService from '@/services/migrator/abstractMigrationFile'
|
|
||||||
import Logo from '@/assets/logo.svg?component'
|
|
||||||
import Message from '@/components/misc/message.vue'
|
|
||||||
import { setTitle } from '@/helpers/setTitle'
|
|
||||||
|
|
||||||
import {formatDateLong} from '@/helpers/time/formatDate'
|
|
||||||
|
|
||||||
import {MIGRATORS} from './migrators'
|
|
||||||
import { useNamespaceStore } from '@/stores/namespaces'
|
|
||||||
|
|
||||||
const PROGRESS_DOTS_COUNT = 8
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'MigrateService',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Logo,
|
|
||||||
Message,
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
progressDotsCount: PROGRESS_DOTS_COUNT,
|
|
||||||
authUrl: '',
|
|
||||||
isMigrating: false,
|
|
||||||
lastMigrationDate: null,
|
|
||||||
message: '',
|
|
||||||
migratorAuthCode: '',
|
|
||||||
migrationService: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
migrator() {
|
|
||||||
return MIGRATORS[this.$route.params.service]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeRouteEnter(to) {
|
beforeRouteEnter(to) {
|
||||||
if (MIGRATORS[to.params.service] === undefined) {
|
if (MIGRATORS[to.params.service as string] === undefined) {
|
||||||
return {name: 'not-found'}
|
return {name: 'not-found'}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
created() {
|
<script setup lang="ts">
|
||||||
this.initMigration()
|
import {computed, ref, shallowReactive} from 'vue'
|
||||||
},
|
import {useI18n} from 'vue-i18n'
|
||||||
|
|
||||||
mounted() {
|
import Logo from '@/assets/logo.svg?component'
|
||||||
setTitle(this.$t('migrate.titleService', {name: this.migrator.name}))
|
import Message from '@/components/misc/message.vue'
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
import AbstractMigrationService, { type MigrationConfig } from '@/services/migrator/abstractMigration'
|
||||||
formatDateLong,
|
import AbstractMigrationFileService from '@/services/migrator/abstractMigrationFile'
|
||||||
|
|
||||||
async initMigration() {
|
import {formatDateLong} from '@/helpers/time/formatDate'
|
||||||
this.migrationService = this.migrator.isFileMigrator
|
import {parseDateOrNull} from '@/helpers/parseDateOrNull'
|
||||||
? new AbstractMigrationFileService(this.migrator.id)
|
|
||||||
: new AbstractMigrationService(this.migrator.id)
|
|
||||||
|
|
||||||
if (this.migrator.isFileMigrator) {
|
import {MIGRATORS} from './migrators'
|
||||||
return
|
import {useNamespaceStore} from '@/stores/namespaces'
|
||||||
}
|
import {useTitle} from '@/composables/useTitle'
|
||||||
|
|
||||||
this.authUrl = await this.migrationService.getAuthUrl().then(({url}) => url)
|
const PROGRESS_DOTS_COUNT = 8
|
||||||
|
|
||||||
this.migratorAuthCode = location.hash.startsWith('#token=')
|
const props = defineProps<{
|
||||||
? location.hash.substring(7)
|
service: string,
|
||||||
: this.$route.query.code
|
code?: string,
|
||||||
|
}>()
|
||||||
|
|
||||||
if (!this.migratorAuthCode) {
|
const {t} = useI18n({useScope: 'global'})
|
||||||
return
|
|
||||||
}
|
|
||||||
const {time} = await this.migrationService.getStatus()
|
|
||||||
if (time) {
|
|
||||||
this.lastMigrationDate = typeof time === 'string' && time?.startsWith('0001-')
|
|
||||||
? null
|
|
||||||
: new Date(time)
|
|
||||||
|
|
||||||
if (this.lastMigrationDate) {
|
const progressDotsCount = ref(PROGRESS_DOTS_COUNT)
|
||||||
return
|
const authUrl = ref('')
|
||||||
}
|
const isMigrating = ref(false)
|
||||||
}
|
const lastMigrationDate = ref<Date | null>(null)
|
||||||
await this.migrate()
|
const message = ref('')
|
||||||
},
|
const migratorAuthCode = ref('')
|
||||||
|
|
||||||
async migrate() {
|
const migrator = computed(() => MIGRATORS[props.service])
|
||||||
this.isMigrating = true
|
|
||||||
this.lastMigrationDate = null
|
|
||||||
this.message = ''
|
|
||||||
|
|
||||||
let migrationConfig = {code: this.migratorAuthCode}
|
const migrationService = shallowReactive(new AbstractMigrationService(migrator.value.id))
|
||||||
|
const migrationFileService = shallowReactive(new AbstractMigrationFileService(migrator.value.id))
|
||||||
|
|
||||||
if (this.migrator.isFileMigrator) {
|
useTitle(() => t('migrate.titleService', {name: migrator.value.name}))
|
||||||
if (this.$refs.uploadInput.files.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
migrationConfig = this.$refs.uploadInput.files[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
async function initMigration() {
|
||||||
const {message} = await this.migrationService.migrate(migrationConfig)
|
if (migrator.value.isFileMigrator) {
|
||||||
this.message = message
|
return
|
||||||
const namespaceStore = useNamespaceStore()
|
}
|
||||||
return namespaceStore.loadNamespaces()
|
|
||||||
} finally {
|
authUrl.value = await migrationService.getAuthUrl().then(({url}) => url)
|
||||||
this.isMigrating = false
|
|
||||||
}
|
const TOKEN_HASH_PREFIX = '#token='
|
||||||
},
|
migratorAuthCode.value = location.hash.startsWith(TOKEN_HASH_PREFIX)
|
||||||
},
|
? location.hash.substring(TOKEN_HASH_PREFIX.length)
|
||||||
})
|
: props.code as string
|
||||||
|
|
||||||
|
if (!migratorAuthCode.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const {time} = await migrationService.getStatus()
|
||||||
|
if (time) {
|
||||||
|
lastMigrationDate.value = parseDateOrNull(time)
|
||||||
|
|
||||||
|
if (lastMigrationDate.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await migrate()
|
||||||
|
}
|
||||||
|
|
||||||
|
initMigration()
|
||||||
|
|
||||||
|
const uploadInput = ref<HTMLInputElement | null>(null)
|
||||||
|
async function migrate() {
|
||||||
|
isMigrating.value = true
|
||||||
|
lastMigrationDate.value = null
|
||||||
|
message.value = ''
|
||||||
|
|
||||||
|
let migrationConfig: MigrationConfig | File = {code: migratorAuthCode.value}
|
||||||
|
|
||||||
|
if (migrator.value.isFileMigrator) {
|
||||||
|
if (uploadInput.value?.files?.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
migrationConfig = uploadInput.value?.files?.[0] as File
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = migrator.value.isFileMigrator
|
||||||
|
? await migrationFileService.migrate(migrationConfig as File)
|
||||||
|
: await migrationService.migrate(migrationConfig as MigrationConfig)
|
||||||
|
message.value = result.message
|
||||||
|
const namespaceStore = useNamespaceStore()
|
||||||
|
return namespaceStore.loadNamespaces()
|
||||||
|
} finally {
|
||||||
|
isMigrating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 471 B After Width: | Height: | Size: 471 B |
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 745 B |
Before Width: | Height: | Size: 512 B After Width: | Height: | Size: 512 B |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
@ -49,4 +49,4 @@ export const MIGRATORS: IMigratorRecord = {
|
|||||||
icon: tickTickIcon as string,
|
icon: tickTickIcon as string,
|
||||||
isFileMigrator: true,
|
isFileMigrator: true,
|
||||||
},
|
},
|
||||||
}
|
} as const
|