I want to add content to a repeatable component in my beforeUpdate hook. (adding a changed slug to a “previous slugs” list)
in v3, I could just push new data on the component array and it would save.
in v4, it doesn’t work like that. Component data now holds __pivot: and such. I do not know how to add new data to this. I’ve tried adding a component with the entityService first, and adding that result to the array. It seemed to work, but it has strange behavior that the next saves puts in two entries. I feel like there should be an easier way to go about this.
It seems like the way to go about this is to create the pivot manually:
// create an entry for the component
const newRedirect = await strapi.entityService.create('redirects.redirect', {
data: {
from: oldData.slug,
},
});
// add the component to this model entry
data.redirects = [...data.redirects, {
id: newRedirect.id,
__pivot: { field: 'redirects', component_type: 'redirects.redirect' },
}];
But this feels pretty hacky. If I change the components name or the field key, this will break. I'd rather have a Strapi core way of doing this
the way strapi currently handles components is by providing full components array, so in case you want to inject something, you have to read components first and then apply full update, if it makes it clear.
Update
So after few hours of searching, had to do few hours of trail and error, however here is the solution, using knex:
module.exports = {
async beforeUpdate(event) {
// get previous slug
const { slug: previousSlug } = await strapi.db
.query("api::test.test")
.findOne({ where: event.params.where });
// create component
const [component] = await strapi.db
// this name of components table in database
.connection("components_components_previous_slugs")
.insert({ slug: previousSlug })
.returning("id");
// append component to event
event.params.data.previousSlugs = [
...event.params.data.previousSlugs,
{
id: component.id,
// the pivot, you have to copy manually
// 'field' is the name of the components property
// 'component_type' is internal name of component
__pivot: {
field: "previousSlugs",
component_type: "components.previous-slugs",
},
},
];
},
};
So, seems there is no service, or something exposed in strapi to create component for you.
The stuff that also required to be noted, on my first attempt i try to create relation manually in tests_components table, made for me after i added a repeatable component, to content-type, but after an hour more i found out that is WRONG and should not be done, seems strapi does that under the hood and modifying that table actually breaks logic...
so if there is more explanation needed, ping me here...
result:
You can update, create and delete component data that is attached to a record with Query Engine API, which is provided by Strapi.
To modify component data you just need the ID.
const { data } = event.params;
const newData = {
field1: value1,
etc...
};
await strapi.query('componentGroup.component').update({
where: { id: data.myField.id },
data: newData
})
When you have a component field that equals null you need to create that component and point to it.
const tempdata = await strapi.query('componentGroup.component').create(
{ data: newData }
);
data.myField = {
id: tempdata.id,
__pivot: {
field: 'myField',
component_type: 'componentGroup.component'
}
}
Se the Strapi forum for more information.
Related
I have this query that works
async find(ctx) {
let { _start, _limit } = ctx.request.query;
console.log(ctx.request.query)
_limit ? 0 : (_limit = 10);
const entities = await strapi.services["course-series"].find({});
return entities.map((entity) => {
// Do I sort them here or in the url query (and how)
entity.courses = entity.courses.slice(_start, _limit);
return sanitizeEntity(entity, { model: strapi.models["course-series"] });
});
}
The idea is that I can load 10 courses from each series at first and then get the next 10...
I just realized that the first 10 I am getting are not the recent ones.
As I commented // Do I sort them here or in the url query (and how)
What version of Strapi do you use?
What does this line do strapi.services["course-series"].find({})? How did you build this find method in the service? What does it do? Does it accept params?
Personally I'd do something like that (assuming you're working with Strapi version > 4:
const entities = await strapi.entityService.findMany('api::course-series.course-series', {
fields: [/* list the course-series fields you want to populate */],
populate: {
courses: {
fields: [/* list the course fields you want to populate */],
sort: 'createdAt:desc', // You can use id, publishedAt or updatedAt here, depends on your sorting prefrences
offset: _start,
limit: _limit // I must admit I haven't tested `offset` and `limit` on the populated related field
}
}
})
// ...the rest of your code, if needed
Read more about Entity Service API here.
Doing it the way you did it, you will always first retrieve the full list of courses for each course-series, and then run costly operations like mapping (the lesser of 2 evils) and above all sorting.
I have a gatsby site with the contentful plugin and graphql queries (setup is working).
[EDIT]
My gatsby setup pulls data dynamically using the pageCreate feature. And populates my template component, the root graphql query of which I've shared below. I can create multiple pages using the setup if the pages on contentful follow the structure given in the below query.
[/EDIT]
My question is about a limitation I seemed to have come across or just don't know enough grpahql to understand this yet.
My high level content model 'BasicPageLayout' consists of references to other content types through the field 'Section'. So, it's flexible in terms of which content types are contained in the 'BasicPageLayout' and the order in which they are added.
Root page query
export const pageQuery = graphql`
query basicPageQuery {
contentfulBasicPageLayout(pageName: {eq: "Home"}) {
heroSection {
parent {
id
}
...HeroFields
}
section1 {
parent {
id
}
...ContentText
}
section2 {
parent {
id
}
...ContentTextOverMedia
}
section3 {
parent {
id
}
...ContentTextAndImage
}
section4 {
parent {
id
}
...ContentText
}
}
}
The content type fragments all live in the respecitve UI components.
The above query and setup are working.
Now, I have "Home" Hard coded because I'm having trouble creating a flexible reusable query. I'm taking advantage of contentful's flexible nature when creating the models, but haven't found a way to create that flexibility in the graphql query for it.
What I do know:
Graphql query is resolved at run time, so everything that needs to be fetched should be in that query. It can't be 'dynamic'.
Issue: The 'Section' fields in the basicPageLayout can link to any content type. So we can mix and match the granular level content types. How do I add the content type fragment (like ContentTextAndImage vs ContentText) so it is appropriate for that section instance ('Section' field in the query)?
In other words
I'd like the root query to get 'Home' data which might have 4 sections, all of type - ContentTextOverMedia
as well as 'About ' data that might have also have 4 sections but with alternating types - ContentText and ContentTextAndImage
This is the goal because I want to create content (Pages) by mix-matching content types on contentful, without needing to update the code each time a new Page is created. Which is why Contentful is useful and was picked in the first place.
My ideas so far:
A. Run two queries, in series. One fetches the parent.id on each section and that holds the content type info. Second fetches the data using the appropriate fragment.
B. Fetch the JSON file of the basicPageLayouts content instance (such as 'Home') separately through Contentful API, and using that JSON file create the graphql string to be used in each instance (So, different layout for Home, About, and so on)
This needs more experimentation, not sure if it's viable, could also be more complex then it needs to be.
So, please share thoughts on the above paths that I'm exploring or another solution that I haven't considered using graphql or gatsby's features.
This is my first question on SO btw, I've spent some time on refining it and trying to follow the guidelines but please do give me feedback in comments so I can improve even if you don't have an answer to my question.
Thanks in advance.
If I understood correctly you want to create pages dynamically from the data coming from Contentful.
You can achieve this using the Gatsbyjs Node API specifically createPage.
In your gatsby-node.js file you can have something like this
const fs = require('fs-extra')
const path = require('path')
exports.createPages = ({graphql, boundActionCreators}) => {
const {createPage} = boundActionCreators
return new Promise((resolve, reject) => {
const landingPageTemplate = path.resolve('src/templates/landing-page.js')
resolve(
graphql(`
{
allContentfulBesicPageLayout {
edges {
node {
pageName
}
}
}
}
`).then((result) => {
if (result.errors) {
reject(result.errors)
}
result.data.allContentfulBesicPageLayout.edges.forEach((edge) => {
createPage ({
path: `${edge.node.pageName}`,
component: landingPageTemplate,
context: {
slug: edge.node.pageName // this will passed to each page gatsby create
}
})
})
return
})
)
})
}
Now in your src/templates/landing-page.js
import React, { Component } from 'react'
const LandingPage = ({data}) => {
return (<div>Add you html here</div>)
}
export const pageQuery = graphql`
query basicPageQuery($pageName: String!) {
contentfulBasicPageLayout(pageName: {eq: $pageName}) {
heroSection {
parent {
id
}
...HeroFields
}
section1 {
parent {
id
}
...ContentText
}
section2 {
parent {
id
}
...ContentTextOverMedia
}
section3 {
parent {
id
}
...ContentTextAndImage
}
section4 {
parent {
id
}
...ContentText
}
}
}
note the $pageName param that's what was passed to the component context when creating a page.
This way you will end up creating as many pages as you want.
Please note: the react part of the code was not tested but I hope you get the idea.
Update:
To have a flexible query you instead of having your content Types as single ref field, you can have one field called sections and you can add the section you want there in the order you desire.
Your query will look like this
export const pageQuery = graphql`
query basicPageQuery($pageName: String!) {
contentfulBasicPageLayout(pageName: {eq: $pageName}) {
sections {
... on ContentfulHeroFields {
internal {
type
}
}
}
}
Khaled
Do you know how can I get the ordered list of field names from given form? Instance API has a property called "fieldList" and it's an array but it's not in correct order. (ordered list = [firstFieldName, secondFieldName, ...] so what I need is a list of field names in order they appear in my form - top to bottom)
Also the redux-form' action '##redux-form/REGISTER_FIELD' is dispatching out of correct form order so I guess it's not what I need here...
(My redux-form version: 7.3.0)
I have experience with redux-form and also have checked its API, but didn't find a documented way for getting the fields in the way they appear in the form.
However, here's how I would do it:
I'll create a Reducer, that will keep track of the fields in the order,
they are registered (appear in the form).
We have very detailed action. As you already mentioned - ##redux-form/REGISTER_FIELD action is dispatching out all the fields in process of being registered in the correct order. This action has the following payload:
{
type: '##redux-form/REGISTER_FIELD',
meta: {
form: 'user'
},
payload: {
name: 'firstName',
type: 'Field'
}
}
Create a reducer. So I'll just create a Reducer, that will listen for all ##redux-form/REGISTER_FIELD actions. Something like that:
// The reducer will keep track of all fields in the order they are registered by a form.
// For example: `user` form has two registered fields `firstName, lastName`:
// { user: ['firstName', 'lastName'] }
const formEnhancedReducer = (state = {}, action) {
switch (action.type) {
case '##redux-form/REGISTER_FIELD':
const form = action.meta.form
const field = action.payload.name
return { ...state, [form]: [...state[form], field] }
default:
return state
}
}
Usage. In order to get ordered fields by a form, you just have access the Store (state) formEnhancer property with the form name: state.formEnhanced.user.
Keep in mind that you have to consider some cases as ##redux-form/DESTROY, but I think it's a pretty straightforward implementation.
I would prefer to keep things simple and just subscribed to ##redux-form/REGISTER_FIELD and just change the reducer implementation a little bit, in order to prevent form fields duplication. So I will just validate if the form field is already registered and won't care for supporting ##redux-form/DESTROY.
Hope it helps.
One way that I have been able to retrieve an ordered list of form field names from a given form is via the registered fields stored in the redux form state using the connect HOC (Higher Order Component) from 'react-redux':
import React, { Component } from 'react';
import { connect } from 'react-redux';
import _ from 'lodash';
class Foo extends Component {
render() {
const {
registeredFields,
} = this.props;
...
...
...
}
}
const mapStateToProps = (state, props) => {
// retrieve the registered fields from the form that is stored in redux state; using lodash 'get' function
const registeredFields = _.get(state, 'form.nameOfYourForm.registeredFields');
// creating an object with the field name as the key and the position as the value
const registeredFieldPositions = _.chain(registeredFields).keys().reduce((registeredFieldPositions, key, index) => {
registeredFieldPositions[key] = index;
return registeredFieldPositions;
}, {}).value();
return({
registeredFieldPositions,
});
};
// registeredFieldPositions will now be passed as a prop to Foo
export default connect(mapStateToProps)(Foo);
Well the title says it all, details following.
I have two related models, User & Role.
User has roles defined as:
Ext.define('App.model.security.User', {
extend: 'App.model.Base',
entityName: 'User',
fields: [
{ name: 'id' },
{ name: 'email'},
{ name: 'name'},
{ name: 'enabled', type: 'bool'}
],
manyToMany: 'Role'
});
Then I have a grid of users and a form to edit user's data including his roles.
The thing is, when I try to add or delete a role from the user a later call to session.getSaveBatch() returns undefined and then I cannot start the batch to send the modifications to the server.
How can I solve this?
Well after reading a lot I found that Ext won't save the changed relationships between two models at least on 5.1.1.
I've had to workaround this by placing an aditional field on the left model (I named it isDirty) with a default value of false and set it true to force the session to send the update to the server with getSaveBatch.
Later I'll dig into the code to write an override to BatchVisitor or a custom BatchVisitor class that allow to save just associations automatically.
Note that this only occurs when you want to save just the association between the two models and if you also modify one of the involved entities then the association will be sent on the save batch.
Well this was interesting, I've learned a lot about Ext by solving this simple problem.
The solution I came across is to override the BatchVisitor class to make use of an event handler for the event onCleanRecord raised from the private method visitData of the Session class.
So for each record I look for left side entities in the matrix and if there is a change then I call the handler for onDirtyRecord which is defined on the BatchVisitor original class.
The code:
Ext.define('Ext.overrides.data.session.BatchVisitor', {
override: 'Ext.data.session.BatchVisitor',
onCleanRecord: function (record) {
var matrices = record.session.matrices
bucket = null,
ops = [],
recordId = record.id,
className = record.$className;
// Before anything I check that the record does not exists in the bucket
// If it exists then any change on matrices will be considered (so leave)
try {
bucket = this.map[record.$className];
ops.concat(bucket.create || [], bucket.destroy || [], bucket.update || []);
var found = ops.findIndex(function (element, index, array) {
if (element.id === recordId) {
return true;
}
});
if (found != -1) {
return;
}
}
catch (e) {
// Do nothing
}
// Now I look for changes on matrices
for (name in matrices) {
matrix = matrices[name].left;
if (className === matrix.role.cls.$className) {
slices = matrix.slices;
for (id in slices) {
slice = slices[id];
members = slice.members;
for (id2 in members) {
id1 = members[id2][0]; // This is left side id, right side is index 1
state = members[id2][2];
if (id1 !== recordId) { // Not left side => leave
break;
}
if (state) { // Association changed
this.onDirtyRecord(record);
// Same case as above now it exists in the bucket (so leave)
return;
}
}
}
}
}
}
});
It works very well for my needs, probably it wont be the best solution for others but can be a starting point anyways.
Finally, if it's not clear yet, what this does is give the method getSaveBatch the ability to detect changes on relationships.
I am working with Angular Meteor and am having an issue with my objects/arrays. I have this code:
angular.module("learn").controller("CurriculumDetailController", ['$scope', '$stateParams', '$meteor',
function($scope, $stateParams, $meteor){
$scope.curriculum = $meteor.object(CurriculumList, $stateParams.curriculumId);
$scope.resources = _.map($scope.curriculum.resources, function(obj) {
return ResourceList.findOne({_id:obj._id})
});
console.log($scope.resources)
}]);
I am attempting to iterate over 'resources', which is a nested array in the curriculum object, look up each value in the 'ResourceList' collection, and return the new array in the scope.
Problem is, sometimes it works, sometimes it doesnt. When I load up the page and access it through a UI-router link. I get the array as expected. But if the page is refreshed, $scope.resources is an empty array.
My thought is there is something going on with asynchronous calls but have not been able for find a solution. I still have the autopublish package installed. Any help would be appreciated.
What you're going to do is return a cursor containing all the information you want, then you can work with $meteor.object on the client side if you like. Normally, publishComposite would look something like this: (I don't know what your curriculum.resources looks like)
Use this method if the curriculum.resources has only ONE id:
// this takes the place of the publish method
Meteor.publishComposite('curriculum', function(id) {
return {
find: function() {
// Here you are getting the CurriculumList based on the id, or whatever you want
return CurriculumList.find({_id: id});
},
children: [
{
find: function(curr) {
// (curr) will be each of the CurriculumList's found from the parent query
// Normally you would do something like this:
return ResourceList.find(_id: curr.resources[0]._id);
}
}
]
}
})
This method if you have multiple resources:
However, since it looks like your curriculum is going to have a resources list with one or many objects with id's then we need to build the query before returning anything. Try something like:
// well use a function so we can send in an _id
Meteor.publishComposite('curriculum', function(id){
// we'll build our query before returning it.
var query = {
find: function() {
return CurriculumList.find({_id: id});
}
};
// now we'll fetch the curriculum so we can access the resources list
var curr = CurriculumList.find({_id: id}).fetch();
// this will pluck the ids from the resources and place them into an array
var rList = _.pluck(curr.resources, '_id');
// here we'll iterate over the resource ids and place a "find" object into the query.children array.
query.children = [];
_.each(rList, function(id) {
var childObj = {
find: function() {
return ResourceList.find({_id: id});
}
};
query.children.push(childObj)
})
return query;
});
So what should happen here (I didn't test) is with one publish function you will be getting the Curriculum you want, plus all of it's resourceslist children.
Now you will have access to these on the client side.
$scope.curriculum = $meteor.object(CurriculumList, $stateParams.curriculumId);
// collection if more than one, object if only one.
$scope.resources = $meteor.collection(ResoursesList, false);
This was thrown together somewhat quickly so I apologize if it doesn't work straight off, any trouble I'll help you fix.