Add a Project Group list view
This commit is contained in:
parent
c3624b891a
commit
d08c39d7d0
200
src/components/ProjectGroupFilters.vue
Normal file
200
src/components/ProjectGroupFilters.vue
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<template>
|
||||||
|
<div class="filter-bar">
|
||||||
|
<ChipInputDropdown
|
||||||
|
:options="options"
|
||||||
|
:chips="chips"
|
||||||
|
:keepFocus="true"
|
||||||
|
@select="optionSelected"
|
||||||
|
@input="search"
|
||||||
|
@delete="deleteFilter"
|
||||||
|
@keyup.esc="resetSearch"
|
||||||
|
v-model="searchTerm"
|
||||||
|
>
|
||||||
|
<template v-slot:left>
|
||||||
|
<div class="filtering-container">
|
||||||
|
<p>Filtering:</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ChipInputDropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import project from '@/api/project.js'
|
||||||
|
import ChipInputDropdown from './ChipInputDropdown'
|
||||||
|
|
||||||
|
const SEARCH_OPTIONS = {
|
||||||
|
NAME: 'name',
|
||||||
|
PROJECT: 'project_id'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ProjectGroupFilters',
|
||||||
|
components: {
|
||||||
|
ChipInputDropdown
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
searchFilter: {},
|
||||||
|
searchTerm: '',
|
||||||
|
currentSearchResults: [],
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: SEARCH_OPTIONS.NAME,
|
||||||
|
name: 'Name',
|
||||||
|
value: null,
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SEARCH_OPTIONS.PROJECT,
|
||||||
|
name: 'Project',
|
||||||
|
value: null,
|
||||||
|
active: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
options () {
|
||||||
|
if (this.searchFilter.name && !this.currentSearchResults.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentSearchResults.length) {
|
||||||
|
return this.currentSearchResults
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.inactiveFilters.map(filter => ({ id: filter.key, name: filter.name }))
|
||||||
|
},
|
||||||
|
chips () {
|
||||||
|
const activeFilters = this.activeFilters.map(filter => ({ id: filter.key, name: `${filter.name}: ${filter.value.name}` }))
|
||||||
|
if (this.searchFilter.name) {
|
||||||
|
activeFilters.push({ id: this.searchFilter.key, name: this.searchFilter.name })
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeFilters
|
||||||
|
},
|
||||||
|
activeFilters () {
|
||||||
|
return this.filters.filter(filter => filter.active)
|
||||||
|
},
|
||||||
|
inactiveFilters () {
|
||||||
|
return this.filters.filter(filter => !filter.active)
|
||||||
|
},
|
||||||
|
formattedFilters () {
|
||||||
|
return this.activeFilters.reduce((acc, filter) => {
|
||||||
|
acc[filter.key] = filter.value.id
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.filtersFromQuery()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
optionSelected (option) {
|
||||||
|
this.searchTerm = ''
|
||||||
|
const selectedFilter = this.filters.find(filter => filter.key === option.id)
|
||||||
|
if (selectedFilter) {
|
||||||
|
this.searchFilter = selectedFilter
|
||||||
|
this.search()
|
||||||
|
} else {
|
||||||
|
this.addSearchValue(option)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async search () {
|
||||||
|
if (!this.searchFilter.key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let results
|
||||||
|
|
||||||
|
switch (this.searchFilter.key) {
|
||||||
|
case SEARCH_OPTIONS.NAME:
|
||||||
|
case SEARCH_OPTIONS.DESCRIPTION: {
|
||||||
|
results = [{ id: this.searchTerm, name: this.searchTerm }]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case SEARCH_OPTIONS.PROJECT: {
|
||||||
|
const params = {
|
||||||
|
name: this.searchTerm,
|
||||||
|
limit: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
const projects = await project.browse(params)
|
||||||
|
results = projects.map(result => ({ id: result.id, name: result.name }))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentSearchResults = results.filter(result => result.name !== null)
|
||||||
|
},
|
||||||
|
|
||||||
|
addSearchValue (value) {
|
||||||
|
const filterIndex = this.filters.findIndex(filter => filter.key === this.searchFilter.key)
|
||||||
|
this.filters[filterIndex].value = value
|
||||||
|
this.filters[filterIndex].active = true
|
||||||
|
|
||||||
|
this.searchFilter = {}
|
||||||
|
this.currentSearchResults = []
|
||||||
|
this.searchTerm = ''
|
||||||
|
this.$emit('filter-change', this.formattedFilters)
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteFilter (filter) {
|
||||||
|
const filterIndex = this.filters.findIndex(f => f.key === filter.id)
|
||||||
|
if (!this.filters[filterIndex].active) {
|
||||||
|
this.resetSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filters[filterIndex].active = false
|
||||||
|
this.$emit('filter-change', this.formattedFilters)
|
||||||
|
},
|
||||||
|
|
||||||
|
resetSearch () {
|
||||||
|
this.searchFilter = {}
|
||||||
|
this.currentSearchResults = []
|
||||||
|
},
|
||||||
|
|
||||||
|
async filtersFromQuery () {
|
||||||
|
Object.entries(this.$route.query).forEach(async param => {
|
||||||
|
const [key, value] = param
|
||||||
|
const filterIndex = this.filters.findIndex(filter => filter.key === key)
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case SEARCH_OPTIONS.NAME:
|
||||||
|
case SEARCH_OPTIONS.DESCRIPTION: {
|
||||||
|
this.filters[filterIndex].value = {
|
||||||
|
id: value,
|
||||||
|
name: value
|
||||||
|
}
|
||||||
|
this.filters[filterIndex].active = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case SEARCH_OPTIONS.PROJECT: {
|
||||||
|
const filterProject = await project.get(value)
|
||||||
|
this.filters[filterIndex].value = {
|
||||||
|
id: value,
|
||||||
|
name: filterProject.name
|
||||||
|
}
|
||||||
|
this.filters[filterIndex].active = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter-bar {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtering-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
</style>
|
155
src/components/ProjectGroupListItem.vue
Normal file
155
src/components/ProjectGroupListItem.vue
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<div class="project-group flex-row" @click="expanded = !expanded">
|
||||||
|
<div class="flex-row">
|
||||||
|
<span class="expander">
|
||||||
|
<FontAwesomeIcon icon="caret-down" v-if="!expanded" fixed-width />
|
||||||
|
<FontAwesomeIcon icon="caret-up" v-if="expanded" fixed-width />
|
||||||
|
</span>
|
||||||
|
<div class="content">
|
||||||
|
<h3>
|
||||||
|
<router-link :to="'/project-group/' + projectGroup.id">
|
||||||
|
{{ projectGroup.id }}. {{ projectGroup.name }}
|
||||||
|
</router-link>
|
||||||
|
</h3>
|
||||||
|
<div class="metadata" v-if="expanded">
|
||||||
|
<ul>
|
||||||
|
<li v-for="project in projects" :key="project.id">
|
||||||
|
<router-link :to="'/project/' + project.id">
|
||||||
|
{{ project.name }}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metadata">
|
||||||
|
<p>
|
||||||
|
{{ projects.length }}
|
||||||
|
<span class="text-muted">projects</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span class="text-muted">Created</span>
|
||||||
|
<DateInline :date="projectGroup.created_at" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span class="text-muted">Last Updated</span>
|
||||||
|
<DateInline v-if="projectGroup.updated_at" :date="projectGroup.updated_at" />
|
||||||
|
<DateInline v-else :date="projectGroup.created_at" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faCaretDown, faCaretUp } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
|
||||||
|
import project from '@/api/project.js'
|
||||||
|
|
||||||
|
import DateInline from '@/components/DateInline.vue'
|
||||||
|
|
||||||
|
library.add(faCaretDown)
|
||||||
|
library.add(faCaretUp)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ProjectGroupListItem',
|
||||||
|
components: {
|
||||||
|
DateInline,
|
||||||
|
FontAwesomeIcon
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
narrow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
projectGroup: {
|
||||||
|
type: Object,
|
||||||
|
default () {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
expanded: false,
|
||||||
|
projects: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getProjects()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getProjects () {
|
||||||
|
const params = {
|
||||||
|
project_group_id: this.projectGroup.id
|
||||||
|
}
|
||||||
|
|
||||||
|
this.projects = []
|
||||||
|
this.loading = true
|
||||||
|
this.projects = await project.browse(params)
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.project-group {
|
||||||
|
padding: 20px;
|
||||||
|
border-top: solid 1px #ddd;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25em;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.5em 1em 0 1em;
|
||||||
|
|
||||||
|
&.text-muted {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
line-height: 1.5em;
|
||||||
|
margin: 15px 0 0 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expander {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-inline--fa {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: #7c342b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: first baseline;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,5 +1,59 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="about">
|
<div class="project-groups">
|
||||||
<h1>Project Group here</h1>
|
<h1><FontAwesomeIcon icon="cubes" fixed-width />Project Groups</h1>
|
||||||
|
<ProjectGroupFilters @filter-change="getProjectGroups" />
|
||||||
|
<ProjectGroupListItem v-for="group in projectGroups" :key="group.id" :projectGroup="group" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faCubes } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
|
||||||
|
import projectGroup from '@/api/project_group.js'
|
||||||
|
|
||||||
|
import ProjectGroupFilters from '@/components/ProjectGroupFilters.vue'
|
||||||
|
import ProjectGroupListItem from '@/components/ProjectGroupListItem.vue'
|
||||||
|
|
||||||
|
library.add(faCubes)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ProjectGroupListView',
|
||||||
|
components: {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
ProjectGroupFilters,
|
||||||
|
ProjectGroupListItem
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
projectGroups: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.getProjectGroups(this.$route.query)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getProjectGroups (filters = {}) {
|
||||||
|
this.$router.push({ query: filters })
|
||||||
|
const params = {
|
||||||
|
...filters,
|
||||||
|
limit: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
this.projectGroups = []
|
||||||
|
this.loading = true
|
||||||
|
this.projectGroups = await projectGroup.browse(params)
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.svg-inline--fa {
|
||||||
|
margin-right: 30px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user