How to create Strapi entries in Nextjs with a Form (Apollo and GraphQL) - forms

I am trying to create new Strapi entries in Nextjs by submitting a form using Apollo client and GraphQL.
I tried a lot of diffrent things with my limited knowledge and was not able to make it work. While researching the topic I realized that most people are using the "useMutation" hook. It never worked though (also when using "useQuery" for queries). So I used a similar approach as for queries because they work.
queries: client.query | mutations: client.mutate
What I have tried:
// GraphQL that works in the Strapi playground
const TEST1 = gql`
mutation CreateMitgliedanmeldung($name: String!) {
createMitgliedanmeldung(data: { name: $name }) {
data {
attributes {
name
}
}
}
}`;
export default function MitgliedWerden() {
const formState = {
name: ''
};
function updateFormState(key, value) {
formState[key] = value;
}
function submit2() {
console.log(formState.name);
client.mutate({
variables: { name: formState.name },
mutation: TEST1,
})
}
return (
<div>
<form onSubmit={submit2()}>
<input onChange={e => updateFormState('name', e.target.value)} id="name"></input>
<button type="submit">Mitglied werden 1</button>
</form>
</div>
)
}
The code above creates new entries but there are multiple problems:
Variable "name" is not present in the new entry but can be console logged inside "updateFormState" function
(same mutation works fine inside graphql playground)
Form is submitted when page is reloaded/loaded
When submitting the page reloads (this is fine if the other problems are gone)
To fix the reload problem I added the following and called it onSubmit.
// calling this onSubmit instead of submit2 function
const newsubmit = (e) => {
e.preventDefault();
submit2()
};
Now submitting or reloading the page does not create a new entry but I get the console.log with the correct value. It seems like the "client.mutate" is broken or can't work in those conditions.
I was not able to find a lot about the ".mutate" function from apollo and the more often used "useMutation" hook did not work at all for me. Using "client.query" works fine.
Apollo Client:
import { ApolloClient, InMemoryCache } from "#apollo/client"
const defaultOptions = {
query: {
fetchPolicy: "no-cache",
},
}
const client = new ApolloClient({
uri: process.env.STRAPI_GRAPHQL_URL,
headers: { "Authorization": process.env.STRAPI_TOKEN },
cache: new InMemoryCache(),
defaultOptions,
});
export default client
Dependencies:
"#apollo/client": "^3.6.9",
"#apollo/react-hooks": "^4.0.0",
"graphql": "^16.5.0",
"graphql-request": "^4.3.0",
"next": "12.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",

Related

Strapi email designer plugin reference template to record

I'm currently developing a multi-tenant API with Strapi and for one of the parts I use the Strapi email designer plugin because I want to send some emails but I want them to be custom designed for each tenant, the problem is that the plugin's table is not accessible in the content manager of Strapi so I can only hard code the template to a specific endpoint, is there a way to have the plugin table in the content manager or for it to be referenced to a content manager table something like:
(table)tenant->(field)templateId => (ref-table)plugin-email-designer->(ref-field)templateId
you know so I can switch and set dynamically from the Strapi panel and not with hard-coded endpoints
I've checked your issue briefly, and there is option you are going to like, but it involves using patch-package...
So, let's assume that you have strapi project created and you have added strapi-plugin-email-designer and you are using yarn v1.xx.xx:
yarn add patch-package postinstall-postinstall
Go to node_modules/strapi-plugin-email-designer/server/content-types/email-template/schema.json
change following fileds:
{
...
"pluginOptions": {
"content-manager": {
"visible": true
},
"content-type-builder": {
"visible": true
}
},
...
}
now run
yarn patch-package strapi-plugin-email-designer
now open your projects package.json and add to scripts:
{
"scripts": {
...
"postinstall": "patch-package"
}
}
run
yarn build
yarn develop
head to admin ui, you should see new Collection:
so now you can do that:
Sending Email
Let's assume you added a relation has one called email_template to your model.
Next we need to add custom route, so in /src/api/tenant/routes/ create file called routes.js
/src/api/tenant/routes/routes.js
module.exports = {
routes: [
{
method: 'POST',
path: `/tenants/:id/send`,
handler: `tenant.send`
}
]
}
now, we need to add handler to controller:
/src/api/tenant/controllers/tenant.js
"use strict";
/**
* tenant controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::tenant.tenant", ({ strapi }) => ({
async send(ctx) {
const { id } = ctx.params;
const { data } = ctx.request.body;
// notice, if you need extra validation you add it here
// if (!data) return ctx.badRequest("no data was provided");
const { to, subject } = data;
const { email_template, ...tenant } = await strapi.db
.query("api::tenant.tenant")
// if you have extra relations it's better to populate them directly here
.findOne({ where: { id }, populate: ["email_template"] });
console.log(email_template);
try {
await strapi
.plugin("email-designer")
.service("email")
.sendTemplatedEmail(
{
to,
//from, < should be set in /config/plugins.js email.settings.defaultFrom
//replayTo < should be set in /config/plugins.js email.settings.defaultReplyTo
},
{
templateReferenceId: email_template.templateReferenceId,
subject,
},
{
...tenant,
// this equals to apply all the data you have in tenant
// this may need to be aligned between your tenant and template
}
);
return { success: `Message sent to ${to}` };
} catch (e) {
strapi.log.debug("📺: ", e);
return ctx.badRequest(null, e);
}
},
}));
don't forget to enable access to /api/tenants/:id/send in admin panel, Settings - Roles
POST http://localhost:1337/api/tenants/1/send
{
"data": {
"to" : "email#example.com",
"subject": "Hello World"
}
}
response:
{
"success": "Message sent to email#example.com"
}
pls note, there is no template validation, e.g. if you give it a wrong template it would not be happy

Next js Strapi integration not displaying data

I am trying to build a simple task website to get familiar with full stack development. I am using Next js and Strapi. I have tried all I can think of, but the data from the server just will not display on the frontend. It seems to me that the page loads too soon, before the data has been loaded in. However, I am not a full stack dev and am therefore not sure.
import axios from 'axios';
const Tasks = ({ tasks }) => {
return (
<ul>
{tasks && tasks.map(task => (
<li key={task.id}>{task.name}</li>
))}
</ul>
);
};
export async function getStaticProps() {
const res = await axios.get('http://localhost:1337/tasks');
const data = await res.data;
if (!data) {
return {
notFound: true,
}
} else {
console.log(data)
}
return {
props: { tasks: data },
};
};
export default Tasks;
I had the same issue. You need to call the api from the pages files in the pages folder. I don't know why this is but that's how it works.

busboy-bodyparser changes my request so that GridFsStorage doesn't register the request-data in mongodb

I am a frontend developer trying to broaden my horizons, and making what will become a MERN application. I'm struggling with image uploads to mongodb.
First I used the express bodyparser:
app.use(express.urlencoded({ extended: true }));
and app.use(express.json());
when used like this I managed to upload the file fine, and the uploaded file showed up in MongoDB Compass.
I found out that this doesn't support multipart/form-data, so I've changed the bodyparser to busboy-bodyparser so that I can access both form-data and the file that is being uploaded. So I changed the bodyparser to:
app.use(busboyBodyParser());
and now it won't upload the request-data to mongodb.
My upload control looks like this:
const upload = require("../middleware/upload");
const uploadFile = async (req, res) => {
try {
req.file = req.files.file;
await upload(req, res);
if (req.file == undefined) {
return res.send(`You must select a file.`);
}
return res.send(`File has been uploaded.`);
} catch (error) {
console.log(error);
return res.send(`Error when trying upload image: ${error}`);
}
};
module.exports = {
uploadFile: uploadFile
};
the reason I've set req.file equals to req.files.file is because busboy-bodyparser sends the file from req.files.file and not req.file, and I thought that this change would make the request function properly, it did not.
My upload-middleware looks like this:
const promise = mongoose.connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true });
const conn = mongoose.connection;
let gfs;
conn.once('open', () => {
gfs = Grid(conn, mongoose.mongo);
gfs.collection('uploads');
});
//create storage object
const storage = new GridFsStorage({
db: promise,
file: (req, file) => {
return new Promise((resolve, reject) => {
crypto.randomBytes(16, (err, buf) => {
if (err) {
return reject(err);
}
const filename = buf.toString('hex') + path.extname(file.originalname);
const fileInfo = {
filename: filename,
bucketName: 'uploads',
metadata: {
title: req.body.title,
orientation: req.body.orientation
}
};
resolve(fileInfo);
});
});
}
});
const uploadFile = multer({ storage }).single("file");
var uploadFilesMiddleware = util.promisify(uploadFile);
module.exports = uploadFilesMiddleware;
I believe this is the code that logs (node:15124) DeprecationWarning: Listening to events on the Db class has been deprecated and will be removed in the next major version.
(Use node --trace-deprecation ... to show where the warning was created)
which is another problem I'm unsure how to solve, but that's another problem for another day.
My end goal is to be able to send the file to mongodb, with the attached metadata (title and orientation).
with this code I'm able to get the "File has been uploaded" message from the upload-control, but in mongoDB compass no file/chunks has been uploaded. The uploads worked great on file-uploads (without the metadata) with the express-bodyparser, so when I changed that to the busboy-bodyparser I get both the file and the metadata as intended but it is not loaded into the db, which leads me to believe that the new bodyparser changes the request so that GridFsStorage no longer recognizes it and doesn't put the data into the db. But frankly I'm just speculating here, and I generally have very limited knowledge of backend.
I use the correct enctype on the form I believe:
<form
action="/upload"
method="POST"
enctype="multipart/form-data">
any tips or explanations is very much appreciated!
I am a complete beginner in backend, so don't be afraid to spell it our for me :)
I managed to fix it!
I'm unsure what caused it, but I believe that the req.body-fields hadn't been populated yet or something of that nature. I therefore switched out
metadata: {
title: req.body.title,
orientation: req.body.orientation
}
with: metadata: req.body and it just works.
For any other backend-newbie who might stumble upon this, also remember to name your inputs in html like this: <input name="title" type="text" /> it is the name-attribute that gets submitted with the html-form and gives the key to req.body, so that you can access for example req.body.title (which didn't work here, but still worth knowing)

Searching for ObjectID after implementing routing in Algolia

I have feature whereby am constructing a url like :
http://localhost/listings?q=&idx=content_index&p=0&dFR[objectID][0]=97&dFR[objectID][1]=96
It creates a facetFilters: [["objectID:97","objectID:96"]]"}. I have a clear All feature also which clear all the filters:
search.addWidget(
instantsearch.widgets.clearAll({
container: '#clearAll',
templates: {
link: '<i class="icon icon-undo2"></i>'
},
autoHideContainer: false,
clearsQuery: true
})
);
This works perfectly fine and clears the above filter also. But the issue came when started routing. With routing,
http://localhost/listings?q=&idx=content_index&p=0&dFR%5Bgenres.name%5D%5B0%5D=Comedy
changed to :
http://localhost/listings?genres=Comedy
Have done the below changes for the above:
routing: {
stateMapping: {
stateToRoute(uiState) {
return {
query: uiState.query,
// we use the character ~ as it is one that is rarely present in data and renders well in urls
genres:
uiState.refinementList &&
uiState.refinementList['genres.name'] &&
uiState.refinementList['genres.name'].join('~'),
page: uiState.page
};
},
routeToState(routeState) {
return {
query: routeState.query,
refinementList: {
'genres.name': routeState.genres && routeState.genres.split('~'),
},
page: routeState.page
};
}
}
},
Have to implement the same functionality for objectID. How to do that?

Testing Angular $resource with external service

I'm trying to make some basic tests on REST requests I'm doing using Angular $resource.
The service code works just fine.
'use strict';
angular.module('lelylan.services', ['ngResource']).
factory('Device', ['Settings', '$resource', '$http', function(Settings, $resource, $http) {
var token = 'df39d56eaa83cf94ef546cebdfb31241327e62f8712ddc4fad0297e8de746f62';
$http.defaults.headers.common["Authorization"] = 'Bearer ' + token;
var resource = $resource(
'http://localhost:port/devices/:id',
{ port: ':3001', id: '#id' },
{ update: { method: 'PUT' } }
);
return resource;
}]);
I'm using the Device resource inside a directive and it works. The problems comes out
when I start making some tests on the services. Here is a sample test where I mock the
HTTP request using $httpBackend and I make a request to the mocked URL.
Unluckily it does not return anything, although the request is made. I'm sure about this
because if a request to another URL is made, the test suite automatically raises an error.
I've been spending lot of time, but no solutions. Here the test code.
'use strict';
var $httpBackend;
describe('Services', function() {
beforeEach(module('lelylan'));
beforeEach(inject(function($injector) {
var uri = 'http://localhost:3001/devices/50c61ff1d033a9b610000001';
var device = { name: 'Light', updated_at: '2012-12-20T18:40:19Z' };
$httpBackend = $injector.get('$httpBackend');
$httpBackend.whenGET(uri).respond(device)
}));
describe('Device#get', function() {
it('returns a JSON', inject(function(Device) {
device = Device.get({ id: '50c61ff1d033a9b610000001' });
expect(device.name).toEqual('Light');
}));
});
});
As the device is not loaded this is the error.
Expected undefined to equal 'Light'.
Error: Expected undefined to equal 'Light'.
I've tried also using the following solution, but it doesn't get into the function
to check the expectation.
it('returns a JSON', inject(function(Device) {
device = Device.get({ id: '50c61ff1d033a9b610000001' }, function() {
expect(device.name).toEqual('Light');
});
}));
Any suggestion or link to solve this problem is really appreciated.
Thanks a lot.
You were very close, the only thing missing was a call to the $httpBackend.flush();. The working test looks like follows:
it('returns a JSON', inject(function(Device) {
var device = Device.get({ id: '50c61ff1d033a9b610000001' });
$httpBackend.flush();
expect(device.name).toEqual('Light');
}));
and a live test in plunker: http://plnkr.co/edit/Pp0LbLHs0Qxlgqkl948l?p=preview
You might also want to check docs for the $httpBackend mock.
In later versions of angular, I'm using 1.2.0rc1 you also need to call this within a $apply or call $digest on a scope. The resource call isn't made unless you do something like this:
var o, back, scope;
beforeEach(inject(function( $httpBackend, TestAPI,$rootScope) {
o = TestAPI;
back = $httpBackend;
scope = $rootScope.$new();
}));
it('should call the test api service', function() {
back.whenGET('/api/test').respond({});
back.expectGET('/api/test');
scope.$apply( o.test());
back.flush();
});