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>
|
||||
<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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user