Add a Project Group list view

This commit is contained in:
Adam Coldrick 2020-09-23 23:00:21 +01:00
parent c3624b891a
commit d08c39d7d0
3 changed files with 411 additions and 2 deletions

View 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>

View 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>

View File

@ -1,5 +1,59 @@
<template>
<div class="about">
<h1>Project Group here</h1>
<div class="project-groups">
<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>
</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>