Migration redesign (#119)

* migration redesign init

* new color picker

* changelog

* added UI language selection

* fix layout issue on recipe editor

* remove git as dependency

* added UI editor for original URL

* CI/CD Tests

* test: fixed migration routes

Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
Hayden
2021-01-23 19:53:39 -09:00
committed by GitHub
parent 138093d062
commit 079ebd8ee1
32 changed files with 549 additions and 367 deletions

View File

@@ -58,6 +58,7 @@ export default {
mounted() {
this.$store.dispatch("initTheme")
this.$store.dispatch("requestRecentRecipes")
this.$store.dispatch("initLang")
this.darkModeSystemCheck()
this.darkModeAddEventListener()
},

View File

@@ -2,42 +2,27 @@ import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils";
import { store } from "../store/store";
const migrationBase = baseURL + "migration/";
const migrationBase = baseURL + "migrations/";
const migrationURLs = {
upload: migrationBase + "upload/",
delete: (file) => `${migrationBase}${file}/delete/`,
chowdownURL: migrationBase + "chowdown/repo/",
nextcloudAvaiable: migrationBase + "nextcloud/available/",
nextcloudImport: (selection) =>
`${migrationBase}nextcloud/${selection}/import/`,
// New
all: migrationBase,
delete: (folder, file) => `${migrationBase}/${folder}/${file}/delete/`,
import: (folder, file) => `${migrationBase}/${folder}/${file}/import/`,
};
export default {
async migrateChowdown(repoURL) {
let postBody = { url: repoURL };
let response = await apiReq.post(migrationURLs.chowdownURL, postBody);
async getMigrations() {
let response = await apiReq.get(migrationURLs.all);
return response.data;
},
async delete(folder, file) {
let response = await apiReq.delete(migrationURLs.delete(folder, file));
return response.data;
},
async import(folder, file) {
let response = await apiReq.post(migrationURLs.import(folder, file));
store.dispatch("requestRecentRecipes");
return response.data;
},
async getNextcloudImports() {
let response = await apiReq.get(migrationURLs.nextcloudAvaiable);
return response.data;
},
async importNextcloud(selected) {
let response = await apiReq.post(migrationURLs.nextcloudImport(selected));
return response.data;
},
async uploadFile(form_data) {
let response = await apiReq.post(migrationURLs.upload, form_data, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return response.data;
},
async delete(file_folder_name) {
let response = await apiReq.delete(migrationURLs.delete(file_folder_name));
return response.data;
},
};

View File

@@ -12,26 +12,26 @@
></v-file-input>
</v-col>
<v-col cols="3"></v-col>
<v-row>
<v-col>
<v-text-field
label="Total Time"
v-model="value.totalTime"
></v-text-field>
</v-col>
<v-col
><v-text-field
label="Prep Time"
v-model="value.prepTime"
></v-text-field
></v-col>
<v-col
><v-text-field
label="Cook Time / Perform Time"
v-model="value.performTime"
></v-text-field
></v-col>
</v-row>
</v-row>
<v-row>
<v-col>
<v-text-field
label="Total Time"
v-model="value.totalTime"
></v-text-field>
</v-col>
<v-col
><v-text-field
label="Prep Time"
v-model="value.prepTime"
></v-text-field
></v-col>
<v-col
><v-text-field
label="Cook Time / Perform Time"
v-model="value.performTime"
></v-text-field
></v-col>
</v-row>
<v-text-field
class="my-3"
@@ -206,6 +206,11 @@
<v-icon>mdi-plus</v-icon>
</v-btn>
<BulkAdd @bulk-data="appendSteps" />
<v-text-field
v-model="value.orgURL"
class="mt-10"
label="Original URL"
></v-text-field>
</v-col>
</v-row>
</v-card-text>

View File

@@ -16,11 +16,11 @@
v-for="backup in backups"
:key="backup.name"
>
<v-card @click="openDialog(backup)">
<v-card hover outlined @click="openDialog(backup)">
<v-card-text>
<v-row align="center">
<v-col cols="12" sm="2">
<v-icon color="primary"> mdi-backup-restore </v-icon>
<v-icon large color="primary"> mdi-backup-restore </v-icon>
</v-col>
<v-col cols="12" sm="10">
<div>

View File

@@ -34,6 +34,8 @@
:backups="availableBackups"
/>
<SuccessFailureAlert
ref="report"
title="Back Restore Report"
success-header="Successfully Imported"
:success="successfulImports"
failed-header="Failed Imports"
@@ -91,6 +93,7 @@ export default {
this.backupLoading = false;
this.successfulImports = successful;
this.failedImports = failed;
this.$refs.report.open();
},
},
};

View File

@@ -0,0 +1,50 @@
<template>
<v-card>
<v-card-title> General Settings </v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row>
<v-col>
<v-select
v-model="selectedLang"
:items="langOptions"
item-text="name"
item-value="value"
label="Language"
>
</v-select>
</v-col>
<v-spacer></v-spacer>
<v-spacer></v-spacer>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
export default {
data() {
return {
langOptions: [],
selectedLang: "en",
};
},
mounted() {
this.getOptions();
},
watch: {
selectedLang() {
this.$store.commit("setLang", this.selectedLang);
},
},
methods: {
getOptions() {
this.langOptions = this.$store.getters.getAllLangs;
this.selectedLang = this.$store.getters.getActiveLang;
},
},
};
</script>
<style>
</style>

View File

@@ -0,0 +1,97 @@
<template>
<v-card class="my-2" :loading="loading">
<v-card-title>
{{ title }}
<v-spacer></v-spacer>
<span>
<UploadBtn
class="mt-1"
:url="`/api/migrations/${folder}/upload/`"
@uploaded="$emit('refresh')"
/>
</span>
</v-card-title>
<v-card-text> {{ description }}</v-card-text>
<div v-if="available[0]">
<v-card
outlined
v-for="migration in available"
:key="migration.name"
class="ma-2"
>
<v-card-text>
<v-row align="center">
<v-col cols="12" sm="2">
<v-icon large color="primary"> mdi-import </v-icon>
</v-col>
<v-col cols="12" sm="10">
<div>
<strong>{{ migration.name }}</strong>
</div>
<div>{{ readableTime(migration.date) }}</div>
</v-col>
</v-row>
</v-card-text>
<v-card-actions class="mt-n6">
<v-spacer></v-spacer>
<v-btn color="error" text @click="deleteMigration(migration.name)">
Delete
</v-btn>
<v-btn color="accent" text @click="importMigration(migration.name)">
Import
</v-btn>
</v-card-actions>
</v-card>
</div>
<div v-else>
<v-card class="text-center ma-2">
<v-card-text>
No Migration Data Avaiable
</v-card-text>
</v-card>
</div>
<br />
</v-card>
</template>
<script>
import UploadBtn from "../../UI/UploadBtn";
import utils from "../../../utils";
import api from "../../../api";
export default {
props: {
folder: String,
title: String,
description: String,
available: Array,
},
components: {
UploadBtn,
},
data() {
return {
loading: false,
};
},
methods: {
deleteMigration(file_name) {
api.migrations.delete(this.folder, file_name);
this.$emit("refresh");
},
async importMigration(file_name) {
this.loading == true;
let response = await api.migrations.import(this.folder, file_name);
console.log(response);
this.$emit("imported", response.successful, response.failed);
this.loading == false;
},
readableTime(timestamp) {
let date = new Date(timestamp);
return utils.getDateAsText(date);
},
},
};
</script>
<style>
</style>

View File

@@ -1,44 +1,96 @@
<template>
<v-card :loading="loading">
<v-card-title class="headline"> {{$t('migration.recipe-migration')}} </v-card-title>
<v-divider></v-divider>
<div>
<SuccessFailureAlert
title="Migration Report"
ref="report"
failedHeader="Failed Imports"
:failed="failed"
successHeader="Successful Imports"
:success="success"
/>
<v-card :loading="loading">
<v-card-title class="headline">
{{ $t("migration.recipe-migration") }}
</v-card-title>
<v-divider></v-divider>
</v-card>
<v-tabs v-model="tab">
<v-tab>Chowdown</v-tab>
<v-tab>Nextcloud Recipes</v-tab>
<v-tab-item>
<ChowdownCard @loading="loading = true" @finished="finished" />
</v-tab-item>
<v-tab-item>
<NextcloudCard @loading="loading = true" @finished="finished" />
</v-tab-item>
</v-tabs>
</v-card>
<v-row dense>
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="3"
v-for="migration in migrations"
:key="migration.title"
>
<MigrationCard
:title="migration.title"
:folder="migration.urlVariable"
:description="migration.description"
:available="migration.availableImports"
@refresh="getAvailableMigrations"
@imported="showReport"
/>
</v-col>
</v-row>
</div>
</template>
<script>
import ChowdownCard from "./ChowdownCard";
import NextcloudCard from "./NextcloudCard";
// import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
// import TimePicker from "./Webhooks/TimePicker";
import MigrationCard from "./MigrationCard";
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
import api from "../../../api";
export default {
components: {
ChowdownCard,
NextcloudCard,
MigrationCard,
SuccessFailureAlert,
},
data() {
return {
tab: null,
loading: false,
success: [],
failed: [],
migrations: {
nextcloud: {
title: "Nextcloud Cookbook",
description: "migrate data from a nextcloud cookbook intance",
urlVariable: "nextcloud",
availableImports: [],
},
chowdown: {
title: "Chowdown",
description: "Migrate From Chowdown",
urlVariable: "chowdown",
availableImports: [],
},
},
};
},
mounted() {
this.getAvailableMigrations();
},
methods: {
finished() {
this.loading = false;
this.$store.dispatch("requestRecentRecipes");
},
async getAvailableMigrations() {
let response = await api.migrations.getMigrations();
response.forEach(element => {
if (element.type === "nextcloud") {
this.migrations.nextcloud.availableImports = element.files;
} else if (element.type === "chowdown") {
this.migrations.chowdown.availableImports = element.files;
}
});
},
showReport(successful, failed) {
this.success = successful;
this.failed = failed;
this.$refs.report.open();
},
},
};
</script>

View File

@@ -1,36 +1,28 @@
<template>
<div>
<v-btn block :color="value" @click="dialog = true">
{{ buttonText }}
</v-btn>
<v-dialog v-model="dialog" width="400">
<v-card>
<v-card-title> {{ buttonText }} {{$t('settings.color')}} </v-card-title>
<v-card-text>
<v-text-field v-model="color"> </v-text-field>
<v-row>
<v-col></v-col>
<v-col>
<v-color-picker
dot-size="28"
hide-inputs
hide-mode-switch
mode="hexa"
:show-swatches="swatches"
swatches-max-height="300"
v-model="color"
@change="updateColor"
></v-color-picker>
</v-col>
<v-col></v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-btn text @click="toggleSwatches"> {{$t('settings.swatches')}} </v-btn>
<v-btn text @click="dialog = false"> {{$t('general.select')}} </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<div class="text-center">
<h3>{{ buttonText }}</h3>
</div>
<v-text-field v-model="color" hide-details class="ma-0 pa-0" solo>
<template v-slot:append>
<v-menu
v-model="menu"
top
nudge-bottom="105"
nudge-left="16"
:close-on-content-click="false"
>
<template v-slot:activator="{ on }">
<div :style="swatchStyle" v-on="on" swatches-max-height="300" />
</template>
<v-card>
<v-card-text class="pa-0">
<v-color-picker v-model="color" flat show-swatches />
</v-card-text>
</v-card>
</v-menu>
</template>
</v-text-field>
</div>
</template>
@@ -44,21 +36,30 @@ export default {
return {
dialog: false,
swatches: false,
color: "#FF00FF",
color: "#1976D2",
mask: "!#XXXXXXXX",
menu: false,
};
},
computed: {
swatchStyle() {
const { value, menu } = this;
return {
backgroundColor: value,
cursor: "pointer",
height: "30px",
width: "30px",
borderRadius: menu ? "50%" : "4px",
transition: "border-radius 200ms ease-in-out",
};
},
},
watch: {
color() {
this.updateColor();
},
},
methods: {
toggleSwatches() {
if (this.swatches) {
this.swatches = false;
} else this.swatches = true;
},
updateColor() {
this.$emit("input", this.color);
},

View File

@@ -1,32 +1,50 @@
<template>
<div>
<v-alert v-if="success[0]" outlined dense type="success">
<h4>{{ successHeader }}</h4>
<v-list dense>
<v-list-item v-for="success in this.success" :key="success">
{{ success }}
</v-list-item>
</v-list>
</v-alert>
<v-alert v-if="failed[0]" outlined dense type="error">
<h4>{{ failedHeader }}</h4>
<v-list dense>
<v-list-item v-for="fail in this.failed" :key="fail">
{{ fail }}
</v-list-item>
</v-list>
</v-alert>
</div>
<v-dialog v-model="dialog" max-width="900px">
<v-card>
<v-card-title> {{ title }} </v-card-title>
<v-card-text class="mt-3">
<v-row>
<v-col>
<v-alert outlined dense type="success">
<h4>{{ successHeader }}</h4>
<p v-for="success in this.success" :key="success" class="my-1">
- {{ success }}
</p>
</v-alert>
</v-col>
<v-col>
<v-alert v-if="failed[0]" outlined dense type="error">
<h4>{{ failedHeader }}</h4>
<p v-for="fail in this.failed" :key="fail" class="my-1">
- {{ fail }}
</p>
</v-alert>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
export default {
props: {
title: String,
successHeader: String,
success: Array,
failedHeader: String,
failed: Array,
},
data() {
return {
dialog: false,
};
},
methods: {
open() {
this.dialog = true;
},
},
};
</script>

View File

@@ -1,23 +1,27 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import Vue from "vue";
import VueI18n from "vue-i18n";
Vue.use(VueI18n)
Vue.use(VueI18n);
function loadLocaleMessages () {
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const messages = {}
function loadLocaleMessages() {
const locales = require.context(
"./locales",
true,
/[A-Za-z0-9-_,\s]+\.json$/i
);
const messages = {};
locales.keys().forEach(key => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
const matched = key.match(/([A-Za-z0-9-_]+)\./i);
if (matched && matched.length > 1) {
const locale = matched[1]
messages[locale] = locales(key)
const locale = matched[1];
messages[locale] = locales(key);
}
})
return messages
});
return messages;
}
export default new VueI18n({
locale: process.env.VUE_APP_I18N_LOCALE || 'en',
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
messages: loadLocaleMessages()
})
locale: "en",
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || "en",
messages: loadLocaleMessages(),
});

View File

@@ -4,7 +4,7 @@ import vuetify from "./plugins/vuetify";
import store from "./store/store";
import VueRouter from "vue-router";
import { routes } from "./routes";
import i18n from './i18n'
import i18n from "./i18n";
Vue.config.productionTip = false;
Vue.use(VueRouter);
@@ -14,12 +14,13 @@ const router = new VueRouter({
mode: process.env.NODE_ENV === "production" ? "history" : "hash",
});
new Vue({
vuetify,
store,
router,
i18n,
render: (h) => h(App)
render: h => h(App),
}).$mount("#app");
// Truncate

View File

@@ -13,7 +13,8 @@
"
>
</v-alert>
<Theme />
<General />
<Theme class="mt-2" />
<Backup class="mt-2" />
<Webhooks class="mt-2" />
<Migration class="mt-2" />
@@ -39,6 +40,7 @@
<script>
import Backup from "../components/Settings/Backup";
import General from "../components/Settings/General";
import Webhooks from "../components/Settings/Webhook";
import Theme from "../components/Settings/Theme";
import Migration from "../components/Settings/Migration";
@@ -50,6 +52,7 @@ export default {
Webhooks,
Theme,
Migration,
General,
},
data() {
return {

View File

@@ -0,0 +1,44 @@
import VueI18n from "../../i18n";
const state = {
lang: "en",
allLangs: [
{
name: "English",
value: "en",
},
{
name: "Dutch",
value: "da",
},
{
name: "French",
value: "fr",
},
],
};
const mutations = {
setLang(state, payload) {
VueI18n.locale = payload;
state.lang = payload;
},
};
const actions = {
initLang({ getters }) {
VueI18n.locale = getters.getActiveLang;
},
};
const getters = {
getActiveLang: (state) => state.lang,
getAllLangs: (state) => state.allLangs,
};
export default {
state,
mutations,
actions,
getters,
};

View File

@@ -3,17 +3,19 @@ import Vuex from "vuex";
import api from "../api";
import createPersistedState from "vuex-persistedstate";
import userSettings from "./modules/userSettings";
import language from "./modules/language";
Vue.use(Vuex);
const store = new Vuex.Store({
plugins: [
createPersistedState({
paths: ["userSettings"],
paths: ["userSettings", "language"],
}),
],
modules: {
userSettings,
language,
},
state: {
// Snackbar
@@ -59,11 +61,11 @@ const store = new Vuex.Store({
getters: {
//
getSnackText: (state) => state.snackText,
getSnackActive: (state) => state.snackActive,
getSnackType: (state) => state.snackType,
getSnackText: state => state.snackText,
getSnackActive: state => state.snackActive,
getSnackType: state => state.snackType,
getRecentRecipes: (state) => state.recentRecipes,
getRecentRecipes: state => state.recentRecipes,
},
});