Skip to content

Commit 7f02208

Browse files
Merge pull request #375 from seyfeb/feature/showTagCloudInSearchResults
Show tag cloud in search results
2 parents 0dda437 + 4ca22cf commit 7f02208

File tree

5 files changed

+240
-42
lines changed

5 files changed

+240
-42
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
[#373](https://github.com./nextcloud/cookbook/pull/373/) @seyfeb
2222
- Pasted content with newlines creates new input fields automatically for tools and ingredients in recipe editor
2323
[#379](https://github.com./nextcloud/cookbook/pull/379/) @seyfeb
24+
- Selectable keywords for filtering in recipe lists
25+
[#375](https://github.com./nextcloud/cookbook/pull/375/) @seyfeb
2426

2527
### Changed
2628
- Switch of project ownership to neextcloud organization in GitHub

lib/Db/RecipeDb.php

+51-17
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,24 @@ public function deleteRecipeById(int $id) {
7272

7373
public function findAllRecipes(string $user_id) {
7474
$qb = $this->db->getQueryBuilder();
75-
76-
$qb->select('*')
75+
76+
$qb->select(['r.recipe_id', 'r.name', 'k.name AS keywords'])
7777
->from(self::DB_TABLE_RECIPES, 'r')
78-
->where('user_id = :user')
78+
->where('r.user_id = :user')
7979
->orderBy('r.name');
8080
$qb->setParameter('user', $user_id, TYPE::STRING);
81+
$qb->leftJoin('r', self::DB_TABLE_KEYWORDS, 'k', 'r.recipe_id = k.recipe_id');
8182

8283
$cursor = $qb->execute();
8384
$result = $cursor->fetchAll();
8485
$cursor->closeCursor();
8586

8687
$result = $this->sortRecipes($result);
87-
88-
return $this->unique($result);
88+
89+
// group recipes, convert keywords to comma-separated list
90+
$recipesGroupedTags = $this->groupKeywordInResult($result);
91+
92+
return $this->unique($recipesGroupedTags);
8993
}
9094

9195
public function unique(array $result) {
@@ -196,23 +200,28 @@ public function getRecipesByCategory(string $category, string $user_id) {
196200

197201
if ($category != '_')
198202
{
199-
$qb->select(['r.recipe_id', 'r.name'])
200-
->from(self::DB_TABLE_CATEGORIES, 'k')
201-
->where('k.name = :category')
202-
->andWhere('k.user_id = :user')
203+
// One would probably want to use GROUP_CONCAT to create the list of keywords
204+
// for the recipe, but those don't seem to work:
205+
// $qb->select(['r.recipe_id', 'r.name', 'GROUP_CONCAT(k.name) AS keywords']) // not working
206+
// $qb->select(['r.recipe_id', 'r.name', DB::raw('GROUP_CONCAT(k.name) AS keywords')]) // not working
207+
$qb->select(['r.recipe_id', 'r.name', 'k.name AS keywords'])
208+
->from(self::DB_TABLE_CATEGORIES, 'c')
209+
->where('c.name = :category')
210+
->andWhere('c.user_id = :user')
203211
->setParameter('category', $category, TYPE::STRING)
204212
->setParameter('user', $user_id, TYPE::STRING);
205213

206-
$qb->join('k', self::DB_TABLE_RECIPES, 'r', 'k.recipe_id = r.recipe_id');
214+
$qb->join('c', self::DB_TABLE_RECIPES, 'r', 'c.recipe_id = r.recipe_id');
215+
$qb->leftJoin('c', self::DB_TABLE_KEYWORDS, 'k', 'c.recipe_id = k.recipe_id');
207216

208-
$qb->groupBy(['r.name', 'r.recipe_id']);
209-
$qb->orderBy('r.name');
217+
$qb->groupBy(['r.name', 'r.recipe_id', 'k.name']);
218+
$qb->orderBy('r.name');
210219
}
211220
else
212221
{
213-
214-
$qb->select(['r.recipe_id', 'r.name'])
222+
$qb->select(['r.recipe_id', 'r.name', 'k.name AS keywords'])
215223
->from(self::DB_TABLE_RECIPES, 'r')
224+
->leftJoin('r', self::DB_TABLE_KEYWORDS, 'k', 'r.recipe_id = k.recipe_id')
216225
->leftJoin(
217226
'r',
218227
self::DB_TABLE_CATEGORIES,
@@ -231,8 +240,11 @@ public function getRecipesByCategory(string $category, string $user_id) {
231240
$cursor = $qb->execute();
232241
$result = $cursor->fetchAll();
233242
$cursor->closeCursor();
243+
244+
// group recipes, convert keywords to comma-separated list
245+
$recipesGroupedTags = $this->groupKeywordInResult($result);
234246

235-
return $this->unique($result);
247+
return $this->unique($recipesGroupedTags);
236248
}
237249

238250
/**
@@ -267,13 +279,14 @@ public function getRecipesByKeywords(string $keywords, string $user_id) {
267279
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
268280
*/
269281
public function findRecipes(array $keywords, string $user_id) {
282+
270283
$has_keywords = $keywords && is_array($keywords) && sizeof($keywords) > 0 && $keywords[0];
271284

272285
if(!$has_keywords) { return $this->findAllRecipes($user_id); }
273286

274287
$qb = $this->db->getQueryBuilder();
275288

276-
$qb->select(['r.recipe_id', 'r.name'])
289+
$qb->select(['r.recipe_id', 'r.name', 'k.name AS keywords'])
277290
->from(self::DB_TABLE_RECIPES, 'r');
278291

279292
$qb->leftJoin('r', self::DB_TABLE_KEYWORDS, 'k', 'k.recipe_id = r.recipe_id');
@@ -309,7 +322,28 @@ public function findRecipes(array $keywords, string $user_id) {
309322
$result = $cursor->fetchAll();
310323
$cursor->closeCursor();
311324

312-
return $this->unique($result);
325+
// group recipes, convert keywords to comma-separated list
326+
$recipesGroupedTags = $this->groupKeywordInResult($result);
327+
328+
return $this->unique($recipesGroupedTags);
329+
}
330+
331+
/**
332+
* @param array $results Array of recipes with double entries for different keywords
333+
* Group recipes by id and convert keywords to comma-separated list
334+
*/
335+
public function groupKeywordInResult(array $result) {
336+
$recipesGroupedTags = array();
337+
foreach ($result as $recipe) {
338+
if(!array_key_exists($recipe['recipe_id'], $recipesGroupedTags)){
339+
$recipesGroupedTags[$recipe['recipe_id']] = $recipe;
340+
} else {
341+
if (!is_null($recipe['keywords'])) {
342+
$recipesGroupedTags[$recipe['recipe_id']]['keywords'] .= ','.$recipe['keywords'];
343+
}
344+
}
345+
}
346+
return $recipesGroupedTags;
313347
}
314348

315349
/**

src/components/AppIndex.vue

+91-14
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
11
<template>
2-
<ul>
3-
<li v-for="recipe in filteredRecipes" :key="recipe.recipe_id">
4-
<router-link :to="'/recipe/'+recipe.recipe_id">
5-
<img v-if="recipe.imageUrl" :src="recipe.imageUrl">
6-
<span>{{ recipe.name }}</span>
7-
</router-link>
8-
</li>
9-
</ul>
2+
<div>
3+
<ul v-if="keywords.length" class="keywords">
4+
<RecipeKeyword v-for="(keyword,idx) in keywords" :key="'kw'+idx" :keyword="keyword" v-on:keyword-clicked="keywordClicked(keyword)" v-bind:class="{active : keywordFilter.includes(keyword), disabled : !keywordContainedInVisibleRecipes(keyword)}" />
5+
</ul>
6+
<ul class="recipes">
7+
<li v-for="(recipe, index) in filteredRecipes" :key="recipe.recipe_id" v-show="recipeVisible(index)">
8+
<router-link :to="'/recipe/'+recipe.recipe_id">
9+
<img v-if="recipe.imageUrl" :src="recipe.imageUrl">
10+
<span>{{ recipe.name }}</span>
11+
</router-link>
12+
</li>
13+
</ul>
14+
</div>
1015
</template>
1116

1217
<script>
18+
import RecipeKeyword from './RecipeKeyword'
19+
1320
export default {
1421
name: 'Index',
22+
components: {
23+
RecipeKeyword,
24+
},
1525
data () {
1626
return {
1727
filters: "",
1828
recipes: [],
29+
keywords: [],
30+
keywordFilter: [],
1931
}
2032
},
2133
computed: {
@@ -34,6 +46,30 @@ export default {
3446
},
3547
},
3648
methods: {
49+
/**
50+
* Callback for click on keyword
51+
*/
52+
keywordClicked: function(keyword) {
53+
const index = this.keywordFilter.indexOf(keyword)
54+
if (index > -1) {
55+
this.keywordFilter.splice(index, 1)
56+
} else {
57+
this.keywordFilter.push(keyword)
58+
}
59+
},
60+
/**
61+
* Check if a keyword exists in the currently visible recipes.
62+
*/
63+
keywordContainedInVisibleRecipes: function(keyword) {
64+
for (let i=0; i<this.recipes.length; ++i) {
65+
if (this.recipeVisible(i)
66+
&& this.recipes[i].keywords
67+
&& this.recipes[i].keywords.split(',').includes(keyword)) {
68+
return true
69+
}
70+
}
71+
return false
72+
},
3773
/**
3874
* Load all recipes from the database
3975
*/
@@ -42,6 +78,7 @@ export default {
4278
var $this = this
4379
$.get(this.$window.baseUrl + '/api/recipes').done(function (recipes) {
4480
$this.recipes = recipes
81+
$this.setKeywords(recipes)
4582
deferred.resolve()
4683
// Always set page name last
4784
$this.$store.dispatch('setPage', { page: 'index' })
@@ -52,6 +89,38 @@ export default {
5289
})
5390
return deferred.promise()
5491
},
92+
/**
93+
* Check if recipe should be displayed, depending on selected keyword filter.
94+
* Returns true if recipe contains all selected keywords.
95+
*/
96+
recipeVisible: function(index) {
97+
if (this.keywordFilter.length == 0) {
98+
return true
99+
} else {
100+
if (!this.recipes[index].keywords) {
101+
return false
102+
}
103+
let kw_array = this.recipes[index].keywords.split(',')
104+
return this.keywordFilter.every(kw => kw_array.includes(kw))
105+
}
106+
},
107+
/**
108+
* Extract and set list of keywords from the returned recipes
109+
*/
110+
setKeywords: function(recipes) {
111+
this.keywords = []
112+
if ((recipes.length) > 0) {
113+
recipes.forEach(recipe => {
114+
if(recipe['keywords']) {
115+
recipe['keywords'].split(',').forEach(kw => {
116+
if(!this.keywords.includes(kw)) {
117+
this.keywords.push(kw)
118+
}
119+
})
120+
}
121+
})
122+
}
123+
},
55124
},
56125
mounted () {
57126
this.$root.$off('applyRecipeFilter')
@@ -65,35 +134,43 @@ export default {
65134

66135
<style scoped>
67136
68-
ul {
137+
ul.keywords {
138+
display: flex;
139+
flex-wrap: wrap;
140+
flex-direction: row;
141+
width: 100%;
142+
margin: .5rem 1rem .5rem;
143+
}
144+
145+
ul.recipes {
69146
display: flex;
70147
flex-wrap: wrap;
71148
flex-direction: row;
72149
width: 100%;
73150
}
74151
75-
ul li {
152+
ul.recipes li {
76153
width: 300px;
77154
max-width: 100%;
78155
margin: 0.5rem 1rem 1rem;
79156
}
80-
ul li a {
157+
ul.recipes li a {
81158
display: block;
82159
height: 105px;
83160
box-shadow: 0 0 3px #AAA;
84161
border-radius: 3px;
85162
}
86-
ul li a:hover {
163+
ul.recipes li a:hover {
87164
box-shadow: 0 0 5px #888;
88165
}
89166
90-
ul li img {
167+
ul.recipes li img {
91168
float: left;
92169
height: 105px;
93170
border-radius: 3px 0 0 3px;
94171
}
95172
96-
ul li span {
173+
ul.recipes li span {
97174
display: block;
98175
padding: 0.5rem 0.5em 0.5rem calc(105px + 0.5rem);
99176
}

src/components/RecipeKeyword.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default {
88
props: ['keyword'],
99
data () {
1010
return {
11-
};
11+
}
1212
},
1313
computed: {
1414
},

0 commit comments

Comments
 (0)