Add project cloning option to ProjectList

This commit is contained in:
D. Berge
2023-10-29 19:32:47 +01:00
parent 15242de2d9
commit 2131cdf0c1

View File

@@ -67,9 +67,26 @@
<v-list-item-icon><v-icon>mdi-file-document-edit-outline</v-icon></v-list-item-icon>
<v-list-item-title class="warning--text">Edit project settings</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-list-item @click="cloneDialogOpen = true">
<v-list-item-icon><v-icon>mdi-sheep</v-icon></v-list-item-icon>
<v-list-item-title class="warning--text">Clone project</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-dialog
v-model="cloneDialogOpen"
max-width="600"
>
<dougal-project-settings-name-id-rootpath
v-model="cloneProjectDetails"
@input="cloneProject"
@close="cloneDialogOpen = false"
>
</dougal-project-settings-name-id-rootpath>
</v-dialog>
</v-container>
</template>
@@ -81,10 +98,15 @@ td p:last-of-type {
<script>
import { mapActions, mapGetters } from 'vuex';
import DougalProjectSettingsNameIdRootpath from '@/components/project-settings/name-id-rootpath'
export default {
name: "ProjectList",
components: {
DougalProjectSettingsNameIdRootpath
},
data () {
return {
headers: [
@@ -120,8 +142,17 @@ export default {
items: [],
options: {},
// Whether or not to show archived projects
showArchived: true,
// Cloned project stuff (admin only)
cloneDialogOpen: false,
cloneProjectDetails: {
name: null,
id: null,
path: null
},
// Context menu stuff
contextMenuShow: false,
contextMenuX: 0,
@@ -187,7 +218,136 @@ export default {
this.$nextTick( () => this.contextMenuShow = true );
},
...mapActions(["api"])
async cloneProject () {
/* Plan of action:
* 1. Pop up dialogue asking for new project name, ID and root path
* 2. Get source project configuration
* 3. Blank out non-clonable parameters (ASAQC, …)
* 4. Rewrite paths prefixed with source rootPath with dest rootPath
* 5. Replace name, ID and rootPath
* 6. Set archived=true
* 7. POST new project
* 8. Redirect to new project settings page
*/
// 1. Pop up dialogue asking for new project name, ID and root path
// (already done, that's why we're here)
const pid = this.contextMenuItem?.pid;
if (!pid) return;
const tpl = this.cloneProjectDetails; // Shorter
if (!tpl.id || !tpl.name || !tpl.path) {
this.showSnack(["Missing project details. Cannot proceed", "warning"]);
return;
}
this.cloneDialogOpen = false;
/** Drills down an object and applies function fn(obj, key)
* on each recursive property [...keys].
*/
function drill (obj, keys, fn) {
if (obj) {
if (Array.isArray(keys)) {
if (keys.length) {
const key = keys.shift();
if (keys.length == 0 && key != "*") { // After shift()
if (typeof fn == "function") {
fn(obj, key);
}
}
if (key == "*") {
// Iterate through this object's keys
if (keys.length) {
for (const k in obj) {
drill(obj[k], [...keys], fn);
}
} else {
for (const k in obj) {
drill(obj, [k], fn);
}
}
} else {
drill(obj[key], keys, fn);
}
}
}
}
}
// 2. Get source project configuration
const src = await this.api([`/project/${pid}/configuration`]);
const blankList = [ "id", "name", "schema", "asaqc.id", "archived" ];
const prjPaths = [
"preplots.*.path",
"raw.p111.paths.*",
"raw.p190.paths.*",
"raw.smsrc.paths.*",
"final.p111.paths.*",
"final.p190.paths.*",
"qc.definitions",
"qc.parameters",
"imports.map.layers.*.*.path",
"rootPath" // Needs to go last because of lazy replacer() implementation below
];
// Technically don't need this
const deleter = (obj, key) => {
delete obj[key];
}
const replacer = (obj, key) => {
if (src.rootPath && tpl.path) {
if (obj[key].startsWith(src.rootPath)) {
obj[key] = obj[key].replace(src.rootPath, tpl.path);
}
}
}
// 3. Blank out non-clonable parameters (ASAQC, …)
blankList.forEach( i => drill(src, i.split("."), deleter) );
// 4. Rewrite paths prefixed with source rootPath with dest rootPath
prjPaths.forEach( i => drill(src, i.split("."), replacer) );
// 5. Replace name, ID and rootPath
// Could use deepMerge, but meh!
src.name = tpl.name;
src.id = tpl.id;
// 6. Set archived=true
src.archived = true;
// 7. POST new project
const init = {
method: "POST",
body: src
};
const cb = (err, res) => {
if (!err && res) {
if (res.status == "201") {
// 8. Redirect to new project settings page
const settingsUrl = `/projects/${tpl.id.toLowerCase()}/configuration`;
this.$router.push(settingsUrl);
}
}
};
await this.api(["/project", init, cb]);
},
...mapActions(["api", "showSnack"])
},
mounted () {