Populate the Project list view

This adds a component for items in lists of projects, and also adds a
projects-specific wrapper around the ChipInputDropdown component. These
components are used on the project list view to provide a filterable
list of projects.
This commit is contained in:
Adam Coldrick 2020-05-11 18:52:20 +01:00
parent 1c405b54af
commit 0f74f0e567
3 changed files with 355 additions and 1 deletions

View File

@ -0,0 +1,207 @@
<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 projectGroup from '@/api/project_group.js'
import ChipInputDropdown from './ChipInputDropdown'
const SEARCH_OPTIONS = {
NAME: 'name',
DESCRIPTION: 'description',
PROJECT_GROUP: 'project_group_id'
}
export default {
name: 'ProjectFilters',
components: {
ChipInputDropdown
},
data () {
return {
searchFilter: {},
searchTerm: '',
currentSearchResults: [],
filters: [
{
key: SEARCH_OPTIONS.NAME,
name: 'Name',
value: null,
active: false
},
{
key: SEARCH_OPTIONS.DESCRIPTION,
name: 'Description',
value: null,
active: false
},
{
key: SEARCH_OPTIONS.PROJECT_GROUP,
name: 'Project Group',
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_GROUP: {
const params = {
name: this.searchTerm,
limit: 5
}
const groups = await projectGroup.browse(params)
results = groups.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_GROUP: {
const filterProjectGroup = await projectGroup.get(value)
this.filters[filterIndex].value = {
id: value,
name: filterProjectGroup.title
}
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,93 @@
<template>
<div class="project">
<div class="header">
<h3>
<router-link :to="'/project/' + project.id">
{{ project.id }}. {{ project.name }}
</router-link>
</h3>
<div class="metadata">
<p>
<span class="text-muted">Created</span>
<DateInline :date="project.created_at" />
</p>
<p>
<span class="text-muted">Last Updated</span>
<DateInline v-if="project.updated_at" :date="project.updated_at" />
<DateInline v-else :date="project.created_at" />
</p>
</div>
</div>
<div class="metadata">
<p class="text-muted">{{ project.description }}</p>
</div>
</div>
</template>
<script>
import DateInline from '@/components/DateInline.vue'
export default {
name: 'ProjectListItem',
components: {
DateInline
},
props: {
narrow: {
type: Boolean,
default: false
},
project: {
type: Object,
default () {
return {}
}
}
}
}
</script>
<style lang="scss" scoped>
.project {
padding: 20px;
border-top: solid 1px #ddd;
&:hover {
background-color: #fff;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin: 0;
font-size: 1.25em;
font-weight: 300;
}
}
.metadata {
display: flex;
align-items: center;
.text-muted {
color: #999;
}
p {
margin: 0.5em 1em 0 1em;
&.text-muted {
color: #777;
}
}
}
.details-wide {
display: flex;
align-items: center;
}
}
</style>

View File

@ -1,5 +1,59 @@
<template>
<div class="about">
<h1>Project list here</h1>
<h1><FontAwesomeIcon icon="cube" fixed-width />Projects</h1>
<ProjectFilters @filter-change="getProjects" />
<ProjectListItem v-for="project in projects" :key="project.id" :project="project" />
</div>
</template>
<script>
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCube } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import project from '@/api/project.js'
import ProjectFilters from '@/components/ProjectFilters.vue'
import ProjectListItem from '@/components/ProjectListItem.vue'
library.add(faCube)
export default {
name: 'ProjectListView',
components: {
FontAwesomeIcon,
ProjectFilters,
ProjectListItem
},
data () {
return {
loading: false,
projects: []
}
},
created () {
this.getProjects(this.$route.query)
},
methods: {
async getProjects (filters = {}) {
this.$router.push({ query: filters })
const params = {
...filters,
limit: 10
}
this.projects = []
this.loading = true
this.projects = await project.browse(params)
this.loading = false
}
}
}
</script>
<style lang="scss" scoped>
.svg-inline--fa {
margin-right: 30px;
color: #999;
}
</style>