Skip to content

Added multiselection for categories and keywords to recipe editor #402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
[#390](https://github.com./nextcloud/cookbook/pull/390) @christianlupus
- Automatic deployment of new releases to the nextcloud app store
[#433](https://github.com./nextcloud/cookbook/pull/433) @christianlupus
- Category and keyword selection from list of existing ones in recipe editor
[#402](https://github.com./nextcloud/cookbook/pull/402/) @seyfeb

### Changed
- Switch of project ownership to neextcloud organization in GitHub
Expand Down
87 changes: 87 additions & 0 deletions src/components/EditMultiselect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<template>
<fieldset>
<label>{{ fieldLabel }}</label>
<Multiselect
class="edit-multiselect"
v-bind="$attrs"
v-on="$listeners"
/>
</fieldset>
</template>

<script>
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
export default {
name: "EditMultiselect",
components: {
Multiselect
},
props: {
fieldLabel: String,
},
data () {
return {
}
}
}
</script>

<style scoped>

fieldset {
margin-bottom: 1em;
width: 100%;
}

fieldset > * {
margin: 0;
float: left;
}
@media(max-width:1199px) { fieldset > label {
display: block;
float: none;
}}
fieldset > label {
display: inline-block;
width: 10em;
line-height: 18px;
font-weight: bold;
word-spacing: initial;
vertical-align: top;
}

.edit-multiselect {
width: calc(100% - 11em + 10px);
}

@media(max-width:1199px) { .edit-multiselect {
width: 100%;
}}
</style>

<style>
#app
.edit-multiselect
.multiselect__tags {
height: auto;
min-height: 34px;
}

#app
.edit-multiselect
.multiselect__tags
.multiselect__tags-wrap {
display: flex;
flex-wrap: wrap;
padding-bottom: 0px;
}

#app
.edit-multiselect
.multiselect__tags
.multiselect__tags-wrap
.multiselect__tag {
flex-basis: 50px;
margin-bottom: 3px;
}
</style>
96 changes: 94 additions & 2 deletions src/components/RecipeEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<EditTimeField :fieldName="'prepTime'" :fieldLabel="t('cookbook', 'Preparation time')" />
<EditTimeField :fieldName="'cookTime'" :fieldLabel="t('cookbook', 'Cooking time')" />
<EditTimeField :fieldName="'totalTime'" :fieldLabel="t('cookbook', 'Total time')" />
<EditInputField :fieldName="'recipeCategory'" :fieldType="'text'" :fieldLabel="t('cookbook', 'Category')" />
<EditInputField :fieldName="'keywords'" :fieldType="'text'" :fieldLabel="t('cookbook', 'Keywords (comma separated)')" />
<EditMultiselect :fieldLabel="t('cookbook', 'Category')" :placeholder="t('cookbook', 'Choose category')" v-model="recipe['recipeCategory']" :options="allCategories" :taggable="true" :multiple="false" :loading="isFetchingCategories" @tag="addCategory" />
<EditMultiselect :fieldLabel="t('cookbook', 'Keywords')" :placeholder="t('cookbook', 'Choose keywords')" v-model="selectedKeywords" :options="allKeywords" :taggable="true" :multiple="true" :tagWidth="60" :loading="isFetchingKeywords" @tag="addKeyword" />
<EditInputField :fieldName="'recipeYield'" :fieldType="'number'" :fieldLabel="t('cookbook', 'Servings')" />
<EditInputGroup :fieldName="'tool'" :fieldType="'text'" :fieldLabel="t('cookbook', 'Tools')" v-bind:createFieldsOnNewlines="true" />
<EditInputGroup :fieldName="'recipeIngredient'" :fieldType="'text'" :fieldLabel="t('cookbook', 'Ingredients')" v-bind:createFieldsOnNewlines="true" />
Expand All @@ -20,6 +20,7 @@
import EditImageField from './EditImageField'
import EditInputField from './EditInputField'
import EditInputGroup from './EditInputGroup'
import EditMultiselect from './EditMultiselect'
import EditTimeField from './EditTimeField'

export default {
Expand All @@ -28,6 +29,7 @@ export default {
EditImageField,
EditInputField,
EditInputGroup,
EditMultiselect,
EditTimeField,
},
props: ['id'],
Expand Down Expand Up @@ -57,6 +59,11 @@ export default {
prepTime: [0, 0],
cookTime: [0, 0],
totalTime: [0, 0],
allCategories: [],
isFetchingCategories: true,
isFetchingKeywords: true,
allKeywords: [],
selectedKeywords: [],
}
},
watch: {
Expand All @@ -75,14 +82,83 @@ export default {
let mins = this.totalTime[1].toString().padStart(2, '0')
this.recipe.totalTime = 'PT' + hours + 'H' + mins + 'M'
},
selectedKeywords: {
deep: true,
handler() {
// convert keyword array to comma-separated string
this.recipe['keywords'] = this.selectedKeywords.join()
}
}
},
methods: {
/**
* Add newly created category and set as selected.
*/
addCategory (newCategory) {
this.allCategories.push(newCategory)
this.recipe['recipeCategory'] = newCategory
},
/**
* Add newly created keyword.
*/
addKeyword (newKeyword) {
this.allKeywords.push(newKeyword)
this.selectedKeywords.push(newKeyword)
},
addEntry: function(field, index, content='') {
this.recipe[field].splice(index, 0, content)
},
deleteEntry: function(field, index) {
this.recipe[field].splice(index, 1)
},
/**
* Fetch and display recipe categories
*/
fetchCategories: function() {
$.get(this.$window.baseUrl + '/categories').done((json) => {
json = json || []
this.allCategories = []
for (let i=0; i<json.length; i++) {
if (json[i].name != '*') {
this.allCategories.push(
json[i].name,
)
}
}
this.isFetchingCategories = false
})
.fail((e) => {
alert(t('cookbook', 'Failed to fetch categories'))
if (e && e instanceof Error) {
throw e
}
})
},
/**
* Fetch and display recipe keywords
*/
fetchKeywords: function() {
$.ajax(this.$window.baseUrl + '/keywords').done((json) => {
json = json || []
if (json) {
this.allKeywords = []
for (let i=0; i<json.length; i++) {
if (json[i].name != '*') {
this.allKeywords.push(
json[i].name,
)
}
}
}
this.isFetchingKeywords = false
})
.fail((e) => {
alert(t('cookbook', 'Failed to fetch keywords'))
if (e && e instanceof Error) {
throw e
}
})
},
loadRecipeData: function() {
if (!this.$store.state.recipe) {
// Make the control row show that a recipe is loading
Expand Down Expand Up @@ -171,6 +247,8 @@ export default {
}
},
setup: function() {
this.fetchCategories()
this.fetchKeywords()
if (this.$route.params.id) {
// Load the recipe from store and make edits to a local copy first
this.recipe = { ...this.$store.state.recipe }
Expand All @@ -187,6 +265,20 @@ export default {
if (timeComps) {
this.totalTime = [timeComps[1], timeComps[2]]
}
this.selectedKeywords = this.recipe['keywords'].split(',')

// fallback if fetching all keywords fails
this.selectedKeywords.forEach(kw => {
if (!this.allKeywords.includes(kw)) {
this.allKeywords.push(kw)
}
})

// fallback if fetching all categories fails
if (!this.allCategories.includes(this.recipe['recipeCategory'])) {
this.allCategories.push(this.recipe['recipeCategory'])
}

// Always set the active page last!
this.$store.dispatch('setPage', { page: 'edit' })
} else {
Expand Down