2020-08-08 23:59:13 +02:00
< template >
< v-container fluid >
2023-10-29 13:25:37 +01:00
2020-08-08 23:59:13 +02:00
< v-data-table
: headers = "headers"
2023-10-29 13:25:37 +01:00
: items = "displayItems"
2020-08-08 23:59:13 +02:00
: options . sync = "options"
: loading = "loading"
2023-10-29 15:25:45 +01:00
@ contextmenu : row = "contextMenu"
2020-08-08 23:59:13 +02:00
>
2020-08-26 20:19:59 +02:00
< template v -slot : item.pid = " { item , value } " >
2023-10-29 13:25:37 +01:00
< template v-if = "item.archived" >
< a class = "secondary--text" title = "This project has been archived" :href = "`/projects/${item.pid}`" > { { value } } < / a >
< v-icon
class = "ml-1 secondary--text"
small
title = "This project has been archived"
> mdi - archive - outline < / v-icon >
< / template >
< template v-else >
2025-07-11 22:45:05 +02:00
< a :href = "`/projects/${item.pid}`" :title = "title(item)" > { { value } } < / a >
2023-10-29 13:25:37 +01:00
< / template >
2020-08-08 23:59:13 +02:00
< / template >
2023-10-29 19:33:52 +01:00
< template v -slot : item.groups = " { item , value } " >
< v-chip v-for = "group in value"
label
small
> { { group } } < / v-chip >
< / template >
2020-08-26 20:19:59 +02:00
< template v -slot :item.shots = "{item}" >
{ { item . total ? ( item . prime + item . other ) : "" } }
< / template >
< template v -slot :item.progress = "{item}" >
{ {
item . total
? ( ( 1 - ( item . remaining / item . total ) ) * 100 ) . toFixed ( 1 ) + "%"
: ""
} }
< v-progress-linear v-if = "item.total"
2020-08-08 23:59:13 +02:00
height = "2"
2020-08-26 20:19:59 +02:00
: value = "((1 - (item.remaining / item.total))*100)"
2020-08-08 23:59:13 +02:00
/ >
< / template >
2023-10-29 13:25:37 +01:00
< template v -slot : footer.prepend >
< v-checkbox
class = "mr-3"
v - model = "showArchived"
dense
hide - details
title = "Projects that have been marked as archived by an administrator no longer receive updates from external sources, such as the project's file repository or the navigation system, but they may still be interacted with via Dougal, including adding or editing log comments."
>
< template v -slot : label >
< span class = "subtitle-2" > Show archived projects < / span >
< / template >
< / v-checkbox >
< / template >
2020-08-08 23:59:13 +02:00
< / v-data-table >
2025-07-12 11:22:33 +02:00
< v-menu v-if = "adminaccess(contextMenuItem)"
2023-10-29 15:25:45 +01:00
v - model = "contextMenuShow"
: position - x = "contextMenuX"
: position - y = "contextMenuY"
absolute
offset - y
>
< v-list dense v-if = "contextMenuItem" >
< v-list-item :href = "`${contextMenuItem.pid}/configuration`" >
< 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 >
2023-10-29 19:32:47 +01:00
< 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 >
2023-10-29 15:25:45 +01:00
< / v-list >
< / v-menu >
2023-10-29 19:32:47 +01:00
< 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 >
2020-08-08 23:59:13 +02:00
< / v-container >
< / template >
< style >
td p : last - of - type {
margin - bottom : 0 ;
}
< / style >
< script >
2020-08-26 17:48:55 +02:00
import { mapActions , mapGetters } from 'vuex' ;
2023-10-29 19:32:47 +01:00
import DougalProjectSettingsNameIdRootpath from '@/components/project-settings/name-id-rootpath'
2025-07-12 11:22:33 +02:00
import AccessMixin from '@/mixins/access' ;
2020-08-08 23:59:13 +02:00
export default {
name : "ProjectList" ,
2023-10-29 19:32:47 +01:00
components : {
DougalProjectSettingsNameIdRootpath
} ,
2025-07-12 11:22:33 +02:00
mixins : [
AccessMixin
] ,
2020-08-08 23:59:13 +02:00
data ( ) {
return {
headers : [
{
value : "pid" ,
text : "Project ID"
} ,
{
value : "name" ,
text : "Name"
} ,
2023-10-29 19:33:52 +01:00
{
value : "groups" ,
text : "Groups"
} ,
2020-08-08 23:59:13 +02:00
{
value : "lines" ,
text : "Lines"
} ,
{
2020-08-26 20:19:59 +02:00
value : "seq_final" ,
2020-08-08 23:59:13 +02:00
text : "Sequences"
} ,
{
2020-08-26 20:19:59 +02:00
value : "total" ,
text : "Preplot points"
} ,
{
value : "shots" ,
text : "Shots acquired"
2020-08-08 23:59:13 +02:00
} ,
{
value : "progress" ,
text : "% Complete"
} ,
] ,
items : [ ] ,
2025-07-25 20:07:40 +02:00
options : { sortBy : [ "pid" ] , sortDesc : [ true ] } ,
2023-10-29 13:25:37 +01:00
2023-10-29 19:32:47 +01:00
// Whether or not to show archived projects
2023-10-29 13:25:37 +01:00
showArchived : true ,
2023-10-29 19:32:47 +01:00
// Cloned project stuff (admin only)
cloneDialogOpen : false ,
cloneProjectDetails : {
name : null ,
id : null ,
path : null
} ,
2023-10-29 15:25:45 +01:00
// Context menu stuff
contextMenuShow : false ,
contextMenuX : 0 ,
contextMenuY : 0 ,
contextMenuItem : null
2020-08-08 23:59:13 +02:00
}
} ,
2020-08-26 17:48:55 +02:00
computed : {
2023-10-29 13:25:37 +01:00
displayItems ( ) {
return this . showArchived
? this . items
: this . items . filter ( i => ! i . archived ) ;
} ,
2025-08-06 10:59:17 +02:00
... mapGetters ( [ 'loading' , 'projects' ] )
2020-08-26 17:48:55 +02:00
} ,
2020-08-08 23:59:13 +02:00
methods : {
2023-10-29 15:27:40 +01:00
2020-08-08 23:59:13 +02:00
async list ( ) {
2023-10-29 15:27:40 +01:00
this . items = [ ... this . projects ] ;
2020-08-08 23:59:13 +02:00
} ,
async summary ( item ) {
const details = await this . api ( [ ` /project/ ${ item . pid } /summary ` ] ) ;
if ( details ) {
return Object . assign ( { } , details , item ) ;
}
} ,
2025-07-11 22:45:05 +02:00
title ( item ) {
if ( item . organisations ) {
return "Access:\n" + Object . entries ( item . organisations ) . map ( org =>
` • ${ org [ 0 ] } ( ${ Object . entries ( org [ 1 ] ) . filter ( access => access [ 1 ] ) . map ( access => access [ 0 ] ) . join ( ", " ) } ) `
) . join ( "\n" )
}
return "" ;
} ,
2020-08-08 23:59:13 +02:00
async load ( ) {
2025-08-06 10:59:17 +02:00
await this . refreshProjects ( ) ;
2020-08-08 23:59:13 +02:00
await this . list ( ) ;
const promises = [ ] ;
for ( const key in this . items ) {
const item = this . items [ key ] ;
const promise = this . summary ( item )
. then ( expanded => {
if ( expanded ) {
this . $set ( this . items , key , expanded ) ;
}
} ) ;
promises . push ( promise ) ;
}
} ,
2025-08-06 10:59:17 +02:00
registerNotificationHandlers ( ) {
this . $store . dispatch ( 'registerHandler' , {
table : 'project`' ,
handler : ( context , message ) => {
if ( message . payload ? . table == "public" ) {
this . load ( ) ;
}
}
} ) ;
} ,
2023-10-29 15:25:45 +01:00
contextMenu ( e , { item } ) {
e . preventDefault ( ) ;
this . contextMenuShow = false ;
this . contextMenuX = e . clientX ;
this . contextMenuY = e . clientY ;
this . contextMenuItem = item ;
this . $nextTick ( ( ) => this . contextMenuShow = true ) ;
} ,
2023-10-29 19:32:47 +01:00
async cloneProject ( ) {
/ * P l a n o f a c t i o n :
* 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 ;
/ * * D r i l l s d o w n a n o b j e c t a n d a p p l i e s f u n c t i o n f n ( o b j , k e y )
* 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 ] ) ;
} ,
2025-08-06 10:59:17 +02:00
... mapActions ( [ "api" , "showSnack" , "refreshProjects" ] )
2020-08-08 23:59:13 +02:00
} ,
mounted ( ) {
2025-08-06 10:59:17 +02:00
this . registerNotificationHandlers ( ) ;
2020-08-08 23:59:13 +02:00
this . load ( ) ;
}
}
< / script >