Skip to content
This repository has been archived by the owner on Aug 12, 2021. It is now read-only.

Feature/search form #106

Merged
merged 43 commits into from
Aug 15, 2018
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3b8cdef
add vue.js & begin scaffolding search form
thatbudakguy Jul 19, 2018
a5b7ec5
books search form reactivity/serialization (#95)
thatbudakguy Jul 23, 2018
b9a4843
add filter components and search/rolodex (#95)
thatbudakguy Jul 23, 2018
057aa9b
first pass at active filter display (#95)
thatbudakguy Jul 24, 2018
e1ffb47
update dependencies, allow ES7
thatbudakguy Jul 25, 2018
54e9521
manage form state using vuex (#95)
thatbudakguy Jul 26, 2018
50087c8
rework vuex configuration (#95)
thatbudakguy Jul 27, 2018
8e84910
active facets display (#95)
thatbudakguy Jul 27, 2018
3ae623d
querystring mutations and parser (#95)
thatbudakguy Aug 1, 2018
d198b6e
basic range facet component (#95)
thatbudakguy Aug 2, 2018
e329f1a
Merge branch 'develop' into feature/search-form
thatbudakguy Aug 2, 2018
cec6722
facet changes dynamically update choice counts (#95)
thatbudakguy Aug 2, 2018
605b734
working result fetching (#101)
thatbudakguy Aug 2, 2018
0f286db
total result count updates (#101)
thatbudakguy Aug 2, 2018
2c253eb
ajax result rendering (#101)
thatbudakguy Aug 6, 2018
80ed90a
adjust sorting behavior for empty fields
thatbudakguy Aug 6, 2018
d873bbd
setup sorting and keyword queries (#101)
thatbudakguy Aug 6, 2018
bc33f8c
adjust zero result test to match UI text
thatbudakguy Aug 6, 2018
ad3baa1
Merge branch 'develop' into feature/search-form
thatbudakguy Aug 7, 2018
acc85b4
remove flat file example data
thatbudakguy Aug 7, 2018
dabcf0d
load initial results without js; add noscript tag
thatbudakguy Aug 7, 2018
28f4a3d
disable result count test
thatbudakguy Aug 7, 2018
9d4f15d
Add Vary header to book list view to address ajax result caching issue
rlskoeser Aug 7, 2018
9059917
Add book count to noscript fallback section and restore tests
rlskoeser Aug 7, 2018
04e5600
close <input> tags
thatbudakguy Aug 8, 2018
a654b60
Merge branch 'feature/search-form' of github.com:Princeton-CDH/winthr…
thatbudakguy Aug 8, 2018
1ea2c44
add more explanatory comments
thatbudakguy Aug 8, 2018
9e06c33
update formState getter
thatbudakguy Aug 8, 2018
6219d8a
remove publisher, translator, and filters
thatbudakguy Aug 8, 2018
3abbd9f
refactor and document mutations & actions (#106)
thatbudakguy Aug 9, 2018
774a40f
finish documentation, resolve routing issues
thatbudakguy Aug 13, 2018
988c488
pin dal to < 3.3
thatbudakguy Aug 13, 2018
9625676
update semantic-ui-vue; refactor html to be more declarative (#109)
thatbudakguy Aug 13, 2018
22902e6
range facet handling
thatbudakguy Aug 13, 2018
85e8fc9
pass in form label names from django (#109)
thatbudakguy Aug 14, 2018
35de578
function formatting
thatbudakguy Aug 14, 2018
203b2e3
show disabled menu items
thatbudakguy Aug 14, 2018
c3f090e
add json_dumps filter
thatbudakguy Aug 14, 2018
804adf4
add "clear all" button
thatbudakguy Aug 14, 2018
a126e38
disable enter key submission for site search
thatbudakguy Aug 15, 2018
b799f8d
show site search by default on books search page
thatbudakguy Aug 15, 2018
8060dcc
make checkbox labels clickable
thatbudakguy Aug 15, 2018
adfd226
improve range facet handling and display
thatbudakguy Aug 15, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,174 changes: 6,174 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

42 changes: 26 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
{
"name": "winthrop-django",
"description": "Django web application for the Winthrop Family on the Page Project",
"repository": "https://github.com/Princeton-CDH/winthrop-django.git",
"author": "The Center for Digital Humanities at Princeton <cdhdevteam@princeton.edu>",
"license": "Apache-2.0",
"main": "sitemedia/js/index.js",
"dependencies": {
"autoprefixer": "^8.5.0",
"babel-core": "^6.26.3",
"babel-preset-es2015": "^6.24.1",
"babelify": "^8.0.0",
"browserify": "^16.2.2",
"node-sass": "^4.9.0",
"postcss-cli": "^5.0.0"
}
"name": "winthrop-django",
"description": "Django web application for the Winthrop Family on the Page Project",
"repository": "https://github.com/Princeton-CDH/winthrop-django.git",
"author": "The Center for Digital Humanities at Princeton <cdhdevteam@princeton.edu>",
"license": "Apache-2.0",
"main": "sitemedia/js/index.js",
"dependencies": {
"babel-polyfill": "^6.26.0",
"lodash": "^4.17.10",
"rxjs": "^6.2.2",
"rxjs-compat": "^6.2.2",
"semantic-ui-vue": "^0.2.11",
"vue-router": "^3.0.1",
"vue2-filters": "^0.3.0",
"vuex": "^3.0.1",
"vuex-router-sync": "^5.0.0"
},
"devDependencies": {
"@babel/core": "^7.0.0-beta.54",
"@babel/preset-env": "^7.0.0-beta.54",
"autoprefixer": "^8.5.0",
"babelify": "^9.0.0",
"browserify": "^16.2.2",
"node-sass": "^4.9.0",
"postcss-cli": "^5.0.0"
}
}
25 changes: 25 additions & 0 deletions sitemedia/js/components/FilterChoice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export default Vue.component ('FilterChoice', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should components have a doc string or comment? It seems like it would be helpful to orient in terms of context and use case or goals for a component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea; makes sense to document them at that level. I'll add a docstring-like comment at the top of the files.

template: `
<label :active="active" is="sui-button" role="checkbox" basic circular compact>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing class="..." here for basic circular compact?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the is="sui-button" tells Vue that I'm using a different tag (label) when I actually want to render a button (sui-button), which would receive the basic circular compact boolean attributes at the end of the tag as props to sui-button, which ultimately renders:

<label class="ui button basic circular compact"></label>

this is coming from semantic-ui-vue. we can talk more about the library - it seems like it might not be well-maintained, but I'm not sure if there is an alternative to make the semantic UI behaviors play well with Vue.

{{ label }}
<input
type="checkbox"
:value="value"
@input="$emit('input', $event.target.value)"
:name="name"
v-model="active"
hidden
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer closed tags. Not sure if it actually matters here (I know it does for django templates if we want to use the assertContains with html)

</label>
`,
props: {
name: String,
label: String,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess label and value are different to allow for displaying facet counts? Maybe warrants a comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"label" is the user-friendly display name; "value" is the value attribute that gets used on the <input> element. I think if I document the components' props with a docstring like you mentioned this will be clearer.

value: String
},
data() {
return {
active: false
}
}
})
38 changes: 38 additions & 0 deletions sitemedia/js/components/RangeFacet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { mapActions } from 'vuex'

export default Vue.component('RangeFacet', {
template: `
<div class="range-facet">
<div class="inputs">
<sui-input :placeholder="minVal" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like sui-input is a semantic-ui vue element. I wonder if the approach & combination of tools needs to be written up briefly somewhere? Not sure where yet. Why sui-input here but not in FilterChoice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FilterChoice actually isn't used currently since filters aren't implemented. I went back and forth between having a component to represent a single choice/checkbox (essentially a custom wrapper around <input>) vs just using <sui-input>...most of that struggle was prior/during/the cause of me implementing Vuex, because having the checkbox maintain its own state and simultaneously publish that state to the form wasn't working well.

The reason using semantic-ui vue is necessary at all is because the behaviors that semantic UI provides (like the js used for the interactivity on the site search bar and mobile menu) might not play well if used inside a Vue component.

<label>to</label>
<sui-input :placeholder="maxVal" />
</div>
<div class="histogram">
</div>
</div>
`,
props: {
label: String,
name: String,
width: Number,
choices: Array
},
computed: {
minVal() {
return this.choices
.map(choice => parseInt(choice.value))
.reduce((acc, cur) => cur < acc ? cur : acc, Infinity)
},
maxVal() {
return this.choices
.map(choice => parseInt(choice.value))
.reduce((acc, cur) => cur > acc ? cur : acc, -Infinity)
}
},
methods: {
...mapActions([
'setRangeFacet',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am probably missing something here, but I see getters for setRangeFacetMin and setRangeFacetMax but not setRangeFacet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, these are placeholders - I started out using a similar approach to the normal facets (one set function) but realized I needed two and haven't changed it yet. Coming soon...

]),
},
})
99 changes: 99 additions & 0 deletions sitemedia/js/components/SearchFacet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { mapActions } from 'vuex'
import RangeFacet from './RangeFacet'

export default Vue.component('SearchFacet', {
template: `
<sui-grid-column :width="width" class="field">
<label :for="label">{{ label }}</label>
<template v-if="type === 'text'">
<sui-input v-if="search" iconPosition="left" icon="search" placeholder="Search or select" v-model="filter" />
<div v-if="search" class="rolodex">
<button
v-for="letter in alphabet"
:key="letter"
class="letter"
:class="{active: alphaFilter == letter}"
@click.prevent="alphaFilter = alphaFilter == letter ? '' : letter"
>
{{ letter }}
</button>
</div>
<div class="facets">
<label v-if="availableChoices.length == 0">No results</label>
<div
class="ui checkbox"
v-for="choice of choices"
v-show="availableChoices.includes(choice.value)"
:key="choice.value"
>
<input
type="checkbox"
@input="toggleFacetChoice(choice)"
:name="choice.facet"
:value="choice.value"
:checked="choice.active"
>
<label>{{ choice.value }} <span class="count">{{ choice.count }}</span></label>
</div>
</div>
</template>
<range-facet v-if="type === 'range'" :label="label" :name="name" :width="width" :choices="choices">
</range-facet>
</sui-grid-column>
`,
components: {
RangeFacet
},
data() {
return {
filter: '',
alphaFilter: '',
alphabet: String.fromCharCode(...Array(91).keys()).slice(65)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need a bit more here to determine which letters are actually present in the data so you can disable the letter choices that aren't.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this would be good to extrapolate away as a Vuex getter that takes an alphabetical facet's results and returns an alphabet with letters not present marked (or vice-versa)?

}
},
computed: {
availableChoices() {
return this.choices
.map(choice => choice.value)
.filter(value => this.normalize(value).startsWith(this.alphaFilter))
.filter(value => this.match(value, this.filter))
}
},
props: {
type: String,
label: String,
name: String,
search: {
type: Boolean,
default: false
},
width: Number,
choices: Array
},
methods: {
...mapActions([
'toggleFacetChoice',
]),
/**
* Utility that normalizes strings for simple comparison.
* Removes special characters, trims whitespace, and uppercases.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why you're removing whitespace. This is probably reasonable enough for a simple facet filter for now.

*
* @param {String} str
* @returns {String}
*/
normalize(str) {
return str.replace(/[^\w\s]/gi, '').trim().toUpperCase()
},
/**
* Compares two strings using normalize().
* Return true if str2 matches str1.
*
* @param {String} str1
* @param {String} str2
* @returns {Boolean}
*/
match(str1, str2) {
return this.normalize(str1).includes(this.normalize(str2))
},
}
})
31 changes: 31 additions & 0 deletions sitemedia/js/components/SearchFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import FilterChoice from './FilterChoice'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments or docstrings would be really helpful to help understand the difference between SearchFilter and SearchFacet


export default Vue.component('SearchFilter', {
template: `
<sui-segment>
<label :for="label">{{ label }}</label>
<filter-choice
v-for="choice in choices"
@input="$emit('input')"
:key="choice"
:name="label"
:label="fieldLabels[choice]"
:value="choice"
/>
<label>books</label>
</sui-segment>
`,
components: {
FilterChoice
},
props: {
label: String,
fieldLabels: Object,
choices: Array
},
data() {
return {
activeFilters: []
}
}
})
11 changes: 11 additions & 0 deletions sitemedia/js/components/SearchResults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { mapState } from 'vuex'

export default Vue.component('SearchResults', {
template: `<div class="search-results" v-html="results" />`,
computed: {
...mapState([
'results',
'totalResults',
]),
}
})
51 changes: 51 additions & 0 deletions sitemedia/js/components/SearchSort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { mapState, mapActions } from 'vuex'

export default Vue.component('SearchSort', {
template: `
<div class="search-sort">
<label>Sort By</label>
<sui-dropdown
:value="activeSort"
:options="options"
@input="changeSort($event)"
selection
/>
</div>
`,
computed: {
...mapState([
'activeSort',
'query'
]),
options() {
return [
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this be pulled from / generated via the django form in future so we don't have to duplicate options?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's my hope. Should be able to just pass it into the component using html.

text: 'Author A-Z',
value: 'author_asc'
},
{
text: 'Author Z-A',
value: 'author_desc'
},
{
text: 'Year Oldest-Newest',
value: 'pub_year_asc'
},
{
text: 'Year Newest-Oldest',
value: 'pub_year_desc'
},
{
text: 'Relevance',
value: 'relevance',
disabled: this.query ? false : true
}
]
},
},
methods: {
...mapActions([
'changeSort'
]),
},
})
Loading