Nested sort in GraphQL - reactjs

I'm using Contentful as a CMS, wherein I have the content models "Category" and "Subcategory". Each subcategory has a category as its parent. Both models have a property called order which I set within the CMS to decide the order in which they will appear on the navigation.
Here's my GraphQL query:
query {
allContentfulCategory(sort: {fields: [order, subcategory___order]}) {
edges {
node {
order
title
subcategory {
order
title
}
}
}
}
}
The first sort works correctly and the query outputs my categories by the order field, rather than in reverse-creation order which seems to be the default output from Contentful (the newest categories show first).
However, my second sort of subcategory___order, which was inspired by this blog post, does not evaluate. In other words, the subcategories (per Category) show up in reverse order so that the top item has an order of 8, and it decreases to 7, and 6, and so on.
For example, say I had category "Animals" with order 2, and "Trees" with order 1. My content in reverse-creation order: "Cat" (order 3), "Dog" (order 1), "Chicken" (order 2), "Birch" (order 2), "Oak" (order 3), and "Willow" (order 1).
These will come through the query like so:
Trees (1):
Birch (2)
Oak (3)
Willow (1)
Animals (2):
Cat (3)
Dog (1)
Chicken (2)
I can't find a way to make the GraphQL query perform both sort commands simultaneously so that my data is ordered according to the order I set in the CMS.
So many thanks in advance!
Edit: Here's a component solution:
Within each category node that comes from my query, I can access the subcategories and their order in an array.
const categories = data.allContentfulCategory.edges
{categories.map((category) => {
return (
<div>
{category.node.subcategory && category.node.subcategory.sort((a, b) => {return a.order - b.order}).map((subcategory) => {
return (
<div>
<p>{subcategory.title}</p>
</div>
)
})}
</div>
)
})}
The key element here is how I sort the array by subcategory order within the component:
category.node.subcategory.sort((a, b) => {return a.order - b.order})
To be clear, this is not a satisfying solution, nor does it answer my question about how I might write multiple GraphQL sorts. But this code does do what I wanted with regards to organising the subcategories.

You are querying category and not subcategory:
query {
allContentfulCategory(sort: {fields: [order, subcategory___order]}) {
edges {
node {
order
title
subcategory {
order
title
}
}
}
}
}
In the query above, only the returned array data.allContentfulCategory.edges is sorted. Gatsby won't sort the data nested in category.subcategory.
Further more, the second field you specified (subcategory___order) will only be considered if the first field (order) is the same. Since each category has a unique order, subcategory will never be used.
If it's possible for you to query for something like allContentfulSubcategory instead, it will work.
Otherwise, you can move the sort logic to build time instead by using createSchemaCustomization. I don't know what your schema look like so this is just an example:
// gatsby-node.js
exports.createSchemaCustomization = ({ actions, schema }) => {
const { createTypes } = actions
const typeDefs = [
schema.buildObjectType({
name: "ContentfulCategory",
fields: {
subcategory: {
type: "[ContentfulSubcategory]",
resolve: (src) => {
const subcategories = src.subcategory
return subcategories.sort()
}
},
},
interfaces: ["Node"],
extensions: { infer: true },
}),
]
createTypes(typeDefs)
}
If you need to use createSchemaCustomization, see the Gatsby docs on the topic here.
The docs' info could be overwhelming, I also wrote a bit about this API here. It is more about field extension, but the resolver function is basically the same.

Related

Get count and specify range in a supabase js query

Im making a recipe website with nextjs and supabase. I have an occasions table which has a foreign key to many recipes. I can easily query the occasions for all recipe in that occasion however I want to paginate so I need a count and a range. How would I go about doing this or would I have to make an RPC instead?
Here is my current query that returns all recipes in an occasion - comments in the query is what I want to put a count/range on
.from("occasions")
.select(
`*,
recipes_occasions(
recipes(
// I WANT TO GET EXACT COUNT OF ALL RECIPES IN AN OCCASION HERE
recipeId, title, slug, totalTime, yield, ingredients,
recipes_categories (
categories(title, slug)
),
recipes_diets (
diets(title, slug)
),
recipes_cuisines (
cuisines(title, slug, code)
)
),
)`
)
.eq("slug", slug )
.range(0,4)
// I WANT TO TAKE SPECIFIC RANGE HERE (e.g. range(12, 24) or range(200, 212)
There are multiple ways you could solve this problem. Splitting it up to two separate queries is certainly one option.
Another option might be to flip the query and get the recipes along with it's associated occasions.
const { data, error } = supabase
.from("recipes")
-- Edit above fixed recipe typo
.select(
`*,
recipes_categories (
categories(title, slug)
),
recipes_diets (
diets(title, slug)
),
recipes_cuisines (
cuisines(title, slug, code)
),
occasions!inner(*)`,
{ count: 'exact' }
)
.eq("occasions.slug", slug )
.range(0,4)
count: 'exact' will get the number of recipes so that you can use it to paginate.
The !inner keyword used after occasions allows you to filter the results of recipes selected based on values of occasions.
-- Edit above fixed recipe typo
I managed to get the range working by first getting the occasionID from the slug and then another query to get the recipes with that occasion ID like so. Added count to destructure also.
const { data, error } = await supabase
.from("occasions")
.select(`occasionId`)
.eq("slug", context.query.slug )
const occasionId = await data[0].occasionId;
const { data: recipes, count, recipeError } = await supabase
.from('recipes_occasions')
.select(`*, recipeDetails: recipes(title)`, {count: 'exact'})
.eq('occasion_id', occasionId)
.range(0,1)
Result in my terminal
[
{
id: 134,
created_at: '2022-12-13T18:17:35.433285+00:00',
recipe_id: 'd7c493ff-6aae-4929-bf74-c6d44e2eb8f7',
occasion_id: '1c907e40-4717-4c22-aeb6-c51044fb276a',
recipeDetails: { title: 'Easter egg cheesecake' }
},
{
id: 135,
created_at: '2022-12-13T18:18:39.011853+00:00',
recipe_id: '30254e94-6692-4a57-a173-c5ccee758d59',
occasion_id: '1c907e40-4717-4c22-aeb6-c51044fb276a',
recipeDetails: { title: 'Rhubarb & custard tart' }
}
]
13

SelectInput react admin to filter duplicate values in choices and display only unique values

I am using react-admin SelectInput and uses name as options but there are in the list that has the same name so the option shows duplicate values. I have tried doing research about filtering duplicate values but I can't seem to make it work.
My data looks like this:
data: [{name: "car1",id: 1,}, {name: "car1",id: 2,}, {name: "car2",id: 3,}]
Select options will display: car1, car 1, car 2
I want to display no duplicate: car1, car2
Here is my code:
<SelectInput optionText={<Choice/>}/>
const Choice = ({...record}) => {
return(
<span>{`${record && record.record.carName}`}</span>
)
}
It allows me to play with the select options below like this and it display "test" so I think it can be done here but I cant find a good logic to implement the filter. Anyone has any idea? I'd greatly appreciate it!
`${record && record.record.carName}` !== 'car1' ? <span>test</span> : null
You may filter with Set. Set stores only unique values. We filter the data based on whether the name already exists in the Set or not. I hope you don't care about the id. As the first item will remain and the last items will be removed as duplicates. Hope the idea is clear.
let s = new Set();
let newData = data.filter(d => {
if (!s.has(d.name)) {
s.add(d.name);
return d;
}
});
If all you want is that there are no duplicates in the selection, I would suggest that you filter them before sending them to SelectInput.
You can do this by the following:
const data = [{name: "car1",id: 1,}, {name: "car1",id: 2,}, {name: "car2",id: 3,}];
const uniqueChoices = data.reduce((acc, choice) => {
if (acc.some(item => item.name === choice.name) {
return acc;
}
return [...acc, item]
}, []);
<SelectInput choices={uniqueChoices} />

Is it possible to filter Firestore data on multiple, user selected values in Angular

I'm essentially trying to build a product filter where the user can select (or not select e.g. they aren't required) from any of the filter options.
My firestore is setup as:
product
{
colour: "white"
brand: "a brand"
}
Currently I can filter on say 'colour' with this code:
filterProducts(value: string){
this.filteredProducts = this.db.collection('products', ref => ref.where('colour', '==', value)).valueChanges();
}
How can I adapt the above to be able to filter on either colour, brand or both? Is this even possible?
If you're asking whether you can query on multiple fields, the answer is "yes". For example to filter on brand and color:
ref.where('colour', '==', 'white').where('brand', '==', 'a brand')
For more on this see the Firebase documentation on filtering data and compound queries.
You'll of course need to pass both values into your filterProducts method in order to be able to use them.
If you only want to add a condition when the user has given a value for that filter, you'd do something like this:
ref = firebase.firestore.collection(...); // whatever you already have
if (colourValue) {
ref = ref.where('colour', '==', colourValue);
}
if (brandValue) {
ref = ref.where('brand', '==', brandValue);
}

Is it possible to select data in the reducer and put those data into one of states?

I have an order state.
In order state, there are states of followings.
const initialState = {
channel: null,
order: {},
fetching:true,
menu: [],
categories: [],
subcategories: [],
currentCategoryId: 1,
currentSubcategoryId: 5,
currentMenu: [],
};
So menu is all lists of food item in the database. categories are list of menu categories and subcategories are list of all subcategories.
Now when a user clicks category selector, it changes value of currentCategoryId and it shows items with selected category_id. For currentSubcategoryId works same with currentCategoryId.
So now I am trying to make pagination for the menu. In order to make pagination for menu, I need to calculate how many menu are there based on category_id or subcategory_id, for example, when currentCategoryId is 1, how many menus with category_id = 1are there. So I made currentMenu. What I am trying to do with currentMenu is that when a user clicks category selector to change currentCategoryId, it changes value in reducer and gets menu of which category_id equals to changed currentCategoryId.
case Constants.CHANGE_CATEGORY:
return {...state, currentCategoryId: action.categoryId, currentMenu: state.menu.map((menu) => {
if (currentCategoryId === category_id) {
// Put results of if statement to currentMenu array..
}
})};
So I made reducer case like above, but I do not know how can I insert results of if statement values to currentMenu array... How can I achieve this? Any suggestions?
Thanks in advance.
If i get you right so you can use a really great package called 'react-addons-update'.
So you can, for example, do something like:
import update from 'react-addons-update'
...
...
case Constants.CHANGE_CATEGORY:
...
if (currentCategoryId === category_id) {
...
return update(state, {
$merge: { [currentMenu]: value }
});
...
}
})};
Another useful commands like $merge is $set.
You can read more here

How to populate AngularJS nested category dropdowns?

On my backend, I have a nested categories structure like this:
Category
--------
id
parent_id
name
That I load completely into $scope.categories as a single dimension array.
On screen, I want to show one dropdown per level, showing the correspondent path. For example, if the path is cars/sedan/ford/taurus, I want to show four dropdowns:
Cars v Sedans v Ford v Taurus v
Whenever a dropdown is changed, the ones to the right should be deleted, and the immediate one populated with all child categories whose parent is the one I just selected. Regular stuff.
I also created a category_path array that contains each category_id in the path to the product category: [null, category_id, category_id, category_id].
And then I did this:
<select
ng-repeat="item in category_path"
ng-model="selected_category[$index]"
ng-options="category.name for category in (categories | filter : { parent_id: item } : true ) track by category.id "
ng-change="select_category(selected_category[$index], $index)"
></select>
On my controller:
$scope.select_category = function ( selected_category, index ) {
if ( $filter('filter') ($scope.categories, { parent_id: selected_category.id }, true).length ) {
if ( $scope.category_path[index+1] != undefined ) {
$scope.category_path = $scope.category_path.slice(0, index+1);
$scope.category_path[index+1] = selected_category.id;
} else {
$scope.category_path.push( selected_category.id );
}
} else {
$scope.product.category_id = selected_category.id;
}
}
This works like charm, except for the fact that I can't populate the default values for each dropdown, based on the current product category_path array.
I tried:
ng-init="select_category[$index] = category_path[$index]"
without success. Ideas?
Your category_path array holds different values than the model, i.e. the selected_category array. The latter seems to hold JSON strings while the former holds a single value (for the id key).
To make the default values work, insert the full JSON objects into the model array.

Resources