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:
parent
1c405b54af
commit
0f74f0e567
207
src/components/ProjectFilters.vue
Normal file
207
src/components/ProjectFilters.vue
Normal 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>
|
93
src/components/ProjectListItem.vue
Normal file
93
src/components/ProjectListItem.vue
Normal 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>
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user