I am working in Vue 3 using the Options API.
My data is initially setting properly. However, when I requery my data after axios fetch, and set it to the data property analytics, I see it updating in the console log and in vue extension, but not in the template. Could someone point me in the right direction?
<template>
{{analytics.analytics.daily_impression_data}}
</template>
export default {
data() {
return {
analytics: {
analytics: {
average: 0,
total: 0,
daily_impression_data: {},
},
},
}
},
async mounted() {
await this.getAnalyticsData();
},
methods:
async onSelectDate() {
await this.getAnalyticsData();
},
async getAnalyticsData() {
Object.assign(this.options, this.parseQstring(this.$route.params.truck));
const qstring = this.generateQstring();
await Analytics.fetchAll(qstring).then((data) => {
this.analytics = data;
});
},
}
}
Related
I have stitched two schemas together and run on localhost to query it. But the query returns null for the data in the second schema and I am not sure why.
I have the following code to stitch to remote schemas together and run a localhost graphql server to serve it. It should add the linked data from the second schema under cmsMetaData in the main Product data. But cmsMetaData is null.
import { ApolloServer } from 'apollo-server-micro';
import { ApolloServerPluginInlineTraceDisabled, ApolloServerPluginLandingPageLocalDefault } from "apollo-server-core";
import { stitchSchemas } from '#graphql-tools/stitch';
import { delegateToSchema } from '#graphql-tools/delegate';
import { RenameTypes, RenameRootFields } from '#graphql-tools/wrap';
import createRemoteSchema from '../../utils/createRemoteExecutor';
// Configuration for Next.js API Routes
export const config = {
api: {
bodyParser: false,
},
};
// Export as a Next.js API Route
export default async (req, res) => {
// Setup subschema configurations
const productsSubschema = await createRemoteSchema({
url: 'https://schema1.com/graphql/'
});
const cmsSubschema = await createRemoteSchema({
url: 'https://schema2.com/graphql/',
transforms: [
new RenameRootFields(
(operationName, fieldName, fieldConfig) => `strapi_${fieldName}`,
),
new RenameTypes((name) => `Strapi_${name}`),
],
});
// Build the combined schema and set up the extended schema and resolver
const schema = stitchSchemas({
subschemas: [productsSubschema, cmsSubschema],
typeDefs: `
extend type Product {
cmsMetaData: Strapi_Product
}
`,
resolvers: {
Product: {
cmsMetaData: {
selectionSet: `{ id }`,
resolve(product, args, context, info) {
// Get the data for the extended type from the subschema for Strapi
return delegateToSchema({
schema: cmsSubschema,
operation: 'query',
fieldName: 'strapi_product',
args: { where: { SaleorID: product.id } },
context,
info,
});
},
},
},
},
});
// Set up the GraphQL server
const apolloServer = new ApolloServer({
schema,
plugins: [
ApolloServerPluginInlineTraceDisabled(),
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
],
});
await apolloServer.start();
const apolloServerHandler = apolloServer.createHandler({
path: '/api/graphql',
});
// Return the GraphQL endpoint
return apolloServerHandler(req, res);
};
utils/createRemoteExecutor.js is:
import { introspectSchema, wrapSchema } from '#graphql-tools/wrap';
import { print } from 'graphql';
// Builds a remote schema executor function,
// customize any way that you need (auth, headers, etc).
// Expects to recieve an object with "document" and "variable" params,
// and asynchronously returns a JSON response from the remote.
export default async function createRemoteSchema({ url, ...filters }) {
const executor = async ({ document, variables }) => {
const query = print(document);
const fetchResult = await fetch(url, {
method: 'POST',
headers: {
// We can also do Authentication here
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
});
return fetchResult.json();
};
return wrapSchema({
schema: await introspectSchema(executor),
executor,
...filters,
});
}
The query is:
products(first: 100, channel: "default-channel")
{
edges
{
node
{
id
name
cmsMetaData
{
Title
SaleorID
}
}
}
}
In my api.tsx, which I generate using codegen.yaml, Product contains cmsMetaData as follows, which is of type Strapi_Product:
export type Product = Node & ObjectWithMetadata & {
__typename?: 'Product';
...
cmsMetaData?: Maybe<Array<Maybe<Strapi_Product>>>;
...
}
Strapi_Product is as follows which contains Title, SaleorID etc.:
export type Strapi_Product = {
__typename?: 'Strapi_Product';
SaleorID?: Maybe<Scalars['String']>;
Title?: Maybe<Scalars['String']>;
createdAt?: Maybe<Scalars['Strapi_DateTime']>;
publishedAt?: Maybe<Scalars['Strapi_DateTime']>;
updatedAt?: Maybe<Scalars['Strapi_DateTime']>;
};
But the date in GraphQL shows null for cmsMetaData as null:
{
"data": {
"products": {
"edges": [
{
"node": {
"id": "UHJvZHVjdDoxMjc=",
"name": "52-00 Base Plate",
"cmsMetaData": null
}
},
{
"node": {
"id": "UHJvZHVjdDoxMjg=",
"name": "52-01HD Weigh Module",
"cmsMetaData": null
}
}
]
}
}
}
Your problem seems related to this github issue. Most of your code looks totally fine, so I guess it fetches the schema correctly. The cmsMetaData field is null because it did not find anything matching objects using the selection criteria. This unwanted behavior is in the transformation and/or the resolver.
A good starting point for debugging would be to remove the RenameRootFields mutation. Furthermore, this example looks like your use case, it is an excellent step-by-step guide.
I also rebuild your example from an example I found on the internet. You most probably have a typo in one of the field names, that caused a null value for me. Make sure A equals B in the code below. I am guessing the initial fieldName is "Product", so after the transformation, this does not equal "strapi_product" and returns a null value.
const cmsSubschema = await createRemoteSchema({
url: 'https://schema2.com/graphql/',
transforms: [
new RenameRootFields(
(_, fieldName) => `strapi_${fieldName}`,), // A
new RenameTypes((name) => `Strapi_${name}`),
],
});
// Build the combined schema and set up the extended schema and resolver
const schema = stitchSchemas({
subschemas: [productsSubschema, cmsSubschema],
typeDefs: `
extend type Product {
cmsMetaData: Strapi_Product
}
`,
resolvers: {
Product: {
cmsMetaData: {
selectionSet: `{ id }`,
resolve(product, args, context, info) {
// Get the data for the extended type from the subschema for Strapi
return delegateToSchema({
schema: cmsSubschema,
operation: 'query',
fieldName: 'strapi_product', // B
args: { where: { SaleorID: product.id } },
context,
info,
});
},
},
},
},
});
I'm using Next.js with Typescript, and I'm having some troubles to correctly type my props that getServerSideProps would return me. On getServerSideProps, or as I call it, getServerSidePropsImpl, I check user authentication and decide if I give it a redirect or the data for it to initialize. The problem with this is that Typescript doesn't correctly type my props, or give me some errors.
So, i had the idea to :
// /pages/sheet/1.tsx
import React from 'react';
import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
import database from '../../utils/database';
import { sessionSSR } from '../../utils/session';
export default function Sheet1(props: InferGetServerSidePropsType<typeof getServerSidePropsImpl>): JSX.Element {
//...Do some stuff...
return <></>;
}
async function getServerSidePropsImpl(ctx: GetServerSidePropsContext) {
const player = ctx.req.session.player;
if (!player) {
return {
redirect: {
destination: '/',
permanent: false
},
props: {
playerID: 0,
playerInfo: []
}
};
}
const playerID = player.id;
const results = await Promise.all([
database.playerInfo.findMany({
where: {
player_id: playerID
},
select: {
info: true,
value: true
},
})
]);
return {
props: {
playerID,
playerInfo: results[0]
}
};
}
export const getServerSideProps = sessionSSR(getServerSidePropsImpl);
To make sure Typescript would correctly type my props object, I had to put generic values when returning a redirect:
return {
redirect: {
destination: '/',
permanent: false
},
props: {
playerID: 0,
playerInfo: []
}
};
Is there any issues to this approach, giving the amount of props will increase as I make this component?
I am experimenting with Vue3's Composition API in a Laravel/VueJS/InertiaJS stack.
A practice that I have used a lot in Vue2 with this stack is to have 1 route that returns the Vue page component (eg. Invoices.vue) and then in the created() callback, I would trigger an axios call to an additional endpoint to fetch the actual data.
I am now trying to replicate a similar approach in Vue3 with composition API like so
export default {
components: {Loader, PageBase},
props: {
fetch_url: {
required: true,
type: String,
}
},
setup(props) {
const loading = ref(false)
const state = reactive({
invoices: getInvoices(),
selectedInvoices: [],
});
async function getInvoices() {
loading.value = true;
return await axios.get(props.fetch_url).then(response => {
return response.data.data;
}).finally(() => {
loading.value = false;
})
}
function handleSelectionChange(selection) {
state.selectedInvoices = selection;
}
return {
loading,
state,
handleSelectionChange,
}
}
}
This however keeps on giving me the propise, rather than the actual data that is returned.
Changing it like so does work:
export default {
components: {Loader, PageBase},
props: {
fetch_url: {
required: true,
type: String,
}
},
setup(props) {
const loading = ref(false)
const state = reactive({
invoices: [],
selectedInvoices: [],
});
axios.get(props.fetch_url).then(response => {
state.invoices = response.data.data;
}).finally(() => {
loading.value = false;
})
function handleSelectionChange(selection) {
state.selectedInvoices = selection;
}
return {
loading,
state,
handleSelectionChange,
}
}
}
I want to use function though, so I can re-use it for filtering etc.
Very curious to read how others are doing this.
I have been googling about it a bit, but cant seem to find relevant docu.
All feedback is highly welcomed.
I tried this now with async setup() and await getInvoices() and <Suspense> but it never displayed any content.
So this is how I'd do it, except I wouldn't and I'd use vuex and vuex-orm to store the invoices and fetch the state from the store.
<template>
<div>loading:{{ loading }}</div>
<div>state:{{ state }}</div>
</template>
<script>
import {defineComponent, ref, reactive} from "vue";
import axios from "axios";
export default defineComponent({
name: 'HelloWorld',
props: {
fetch_url: {
required: true,
type: String,
}
},
setup(props) {
const loading = ref(false)
const state = reactive({
invoices: []
})
async function getInvoices() {
loading.value = true;
await axios.get(props.fetch_url).then(response => {
state.invoices = response.data;
}).finally(() => {
loading.value = false;
})
}
return {
getInvoices,
loading,
state,
}
},
async created() {
await this.getInvoices()
}
})
</script>
<style scoped>
</style>
This is of course similar to what you're doing in option 2.
What I want
I want to change pages without next thinking I am trying to open another page.
The Problem
I have this weird routing problem.
First, my folder structure
pages
[app]
[object]
index.js
index.js
manager.js
feed.js
I am at this path /[app] and navigate to /[app]/manager and then I want to navigate to /[app]/feed and I get this Unhandled Runtime Error.
TypeError: Cannot read property "title" of undefined
This error comes from [object] index.js. Stacktrace is below. Of course, it makes sense it cannot read title because I am trying to open another page. And yet it thinks I am trying to open [object].
This error happens from time to time, but it doesn't matter in what order I try to open the pages, it can be manager to feed or feed to manager, or whatever else I have there.
My getStaticPaths and getStaticProps are the same on all these pages, I will share the one for manager.js.
export const getStaticPaths = async () => {
const paths = appRoutes.map((appRoute) => {
const slug = appRoute.slug;
return {
params: {
app: slug,
manager: 'manager',
},
};
});
return {
fallback: false,
paths,
};
};
export const getStaticProps = async ({ locale }) => {
return {
props: {
...(await serverSideTranslations(locale, ['manager', 'common'])),
},
};
};
And the same again, but for [object]:
export const getStaticPaths = async () => {
const allObjects = await loadObjectData({ id: 'all' });
const paths = allObjects.flatMap((object) => {
return appRoutes.map((appRoute) => {
return {
params: {
object: object.type,
app: appRoute.slug,
},
};
});
});
return {
fallback: false,
paths,
};
};
export const getStaticProps = async ({ params, locale }) => {
const object = await loadObjectData({ type: params.object });
const app = appRoutes.find((appRoute) => appRoute?.slug === params.app);
if (!object) {
throw new Error(
`${object} is not a valid Object. Try checking out your parameters: ${params.object}`
);
}
if (!app) {
throw new Error(`${app} is not a valid App.`);
}
return {
props: {
...(await serverSideTranslation(locale, ['common'])),
object,
app,
},
};
};
This error is hard to reproduce because it happens only from time to time.
New Edits
This is the full file of [object]/index.js
import appRoutes from '../../../routes/appRoutes';
import loadObjectData from '../../../utils/loadObjects';
import { serverSideTranslation } from 'next-i18next/serverSideTranslations';
export default function ObjectPage({ object }) {
return <h1> {object.title} </h1>;
}
export const getStaticPaths = async () => {
const allObjects = await loadObjectData({ id: 'all' });
const paths = allObjects.flatMap((object) => {
return appRoutes.map((appRoute) => {
return {
params: {
object: object.type,
app: appRoute.slug,
},
};
});
});
return {
fallback: false,
paths,
};
};
export const getStaticProps = async ({ params, locale }) => {
const object = await loadObjectData({ type: params.object });
const app = appRoutes.find((appRoute) => appRoute?.slug === params.app);
if (!object) {
throw new Error(
`${object} is not a valid Object. Try checking out your parameters: ${params.object}`
);
}
if (!app) {
throw new Error(`${app} is not a valid App.`);
}
return {
props: {
...(await serverSideTranslation(locale, ['common'])),
object,
app,
},
};
};
Stacktrace:
ObjectPage: index.js:6 Uncaught TypeError: Cannot read property 'title' of undefined
at ObjectPage (http://localhost:3000/_next/static/chunks/pages/%5Bapp%5D/%5Bobject%5D.js:3733:21)
at div
at Grid (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:13654:35)
at WithStyles (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:179881:31)
at div
at StyledComponent (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:179652:28)
at div
at ProjectSelectionStore (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:234820:77)
at Layout (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:278:23)
at TaskStore (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:235454:77)
at UserDocumentStore (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:235663:77)
at StoneStore (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:235119:77)
at StoreMall (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:409:23)
at ThemeProvider (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:178584:24)
at App (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:234333:24)
at I18nextProvider (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:224427:19)
at AppWithTranslation
at ErrorBoundary (http://localhost:3000/_next/static/chunks/main.js?ts=1624290251377:146:47)
at ReactDevOverlay (http://localhost:3000/_next/static/chunks/main.js?ts=1624290251377:250:23)
at Container (http://localhost:3000/_next/static/chunks/main.js?ts=1624290251377:8662:5)
at AppContainer (http://localhost:3000/_next/static/chunks/main.js?ts=1624290251377:9151:24)
at Root (http://localhost:3000/_next/static/chunks/main.js?ts=1624290251377:9282:24)
25.06.2021
So I consoled logged the router from the ObjectPage and for each NavigationItem. I noticed something strange.
This is the href I am passing to teh <Link>:
{
pathname: "/[app]/[menuItem]"
query: {
app: "content"
menuItem: "files"
}
}
And this is the full router I am getting back on ObjectPage.
{
asPath: "/content/editor" // this the path i want to open
back: ƒ ()
basePath: ""
beforePopState: ƒ ()
components: {
"/[app]/[object]": {styleSheets: Array(0), __N_SSG: true, __N_SSP: undefined, props: {…}, Component: ƒ}
"/[app]/editor": {initial: true, props: {…}, err: undefined, __N_SSG: true, Component: ƒ, …}
"/_app": {styleSheets: Array(0), Component: ƒ}
}
defaultLocale: "de"
events: {on: ƒ, off: ƒ, emit: ƒ}
isFallback: false
isLocaleDomain: false
isPreview: false
isReady: true
locale: "de"
locales: ["de"]
pathname: "/[app]/[object]" // [object] is being loaded
prefetch: ƒ ()
push: ƒ ()
query: {app: "content", menuItem: "editor", object: "editor"} // this is interesting
reload: ƒ ()
replace: ƒ ()
route: "/[app]/[object]" // same as pathname
}
In the query you can see object was injected. But I cannot tell from where and why.
I had this code:
{
pathname: "/[app]/[menuItem]"
query: {
app: "content"
menuItem: "files"
}
}
This was incorrect because there is no dynamic path to [menuItem]. So instead I wrote:
{
pathname: "/[app]/files"
query: {
app: "content"
}
}
Which fixed the issue I had.
I have misunderstood the docs for parameters.
I want to test that when i type a value in an input(inputA), anoter input(inputB) gets updated with a value.
inputA accepts a postal code e.g: "10999", after inputB shows a location: "Berlin"
This works on the actual app, i type in inputA, and inputB gets updated.
When ome types on inputA, an action is dispatched and then inputB gets a new value from the redux state.
This is my test code, any ideas why it doesnt updates the input with placeholder of "Ort" on the test, but it does on the actual app?
import { render, withIntl, withStore, configureStore, withState } from "test-utils-react-testing-library";
import { screen, fireEvent, withHistory, withRoute, within } from "#testing-library/react";
import configureMockStore from 'redux-mock-store';
import ProfileForm from "./ProfileForm";
import PersonalDetails from "../PersonalDetails/PersonalDetails";
const STATE = {
locations: { locations: {} },
streets: { streets: {} },
password: {}
};
const mockStore = configureMockStore();
const STORE = mockStore({
streets: {
isFetching: false,
},
locations: {
locations: {
isFetching: false,
},
},
user: {
session: {
impersonated_access_token: "",
},
updateError: "error",
},
});
const props = {
id: "user1",
user: { email: "max#muster.de" },
locations: {},
onSubmit: jest.fn(),
};
beforeEach(jest.resetAllMocks);
describe("ProfileForm", () => {
describe("on personal details change", () => {
it("auto selects only location when postalcode becomes selected", () => {
const locations = { electricity: { [PLZ_1]: [LOCATION_OBJ_1] } };
const user = { postalcode: null };
render(<ProfileForm {...props} user={user} locations={locations} />, [...decorators, withStore(STORE)]);
const input = screen.getByPlaceholderText("PLZ");
fireEvent.change(input, { target: { value: "10999" } })
screen.debug(screen.getByPlaceholderText("PLZ"))
screen.debug(screen.getByPlaceholderText("Ort"))
expect(screen.getByPlaceholderText("Ort")).toHaveValue("Berlin");
});
});
I guess your input hasn't been updated yet.
Try to use waitfor:
https://testing-library.com/docs/dom-testing-library/api-async#waitfor
import { waitFor } from "#testing-library/react";
const inputNode = screen. getByPlaceholderText("Ort");
// keep in mind that you need to make your test async like this
// it("auto selects only location when postalcode becomes selected", async () => {
await waitFor(() => expect(inputNode).toHaveValue("Berlin"));
If it won't work, try to add timeout:
await waitFor(() => expect(inputNode).toHaveValue("Berlin"), { timeout: 4000 });
I've encountered a similar proplem and found that changes in the microtask queue aren't always flushed, so the changes are not applied/rendered until the test is finished running. What worked for me, was to call jest.useFakeTimers() at the beginning of your testcase, and then await act(async () => { jest.runOnlyPendingTimers() }); after the call to fireEvent.<some-event>(...)
In your case:
it("auto selects only location when postalcode becomes selected", async () => {
jest.useFakeTimers();
const locations = { electricity: { [PLZ_1]: [LOCATION_OBJ_1] } };
const user = { postalcode: null };
render(<ProfileForm {...props} user={user} locations={locations} />, [...decorators, withStore(STORE)]);
const input = screen.getByPlaceholderText("PLZ");
fireEvent.change(input, { target: { value: "10999" } })
await act(async () => {
jest.runOnlyPendingTimers();
});
screen.debug(screen.getByPlaceholderText("PLZ"))
screen.debug(screen.getByPlaceholderText("Ort"))
expect(screen.getByPlaceholderText("Ort")).toHaveValue("Berlin");
});
Tried, but get this error: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. No idea where that comes from :(
Try to use findBy instead of getBy.
https://testing-library.com/docs/dom-testing-library/api-queries#findby
import { screen, waitFor } from "#testing-library/react";
const inputNode = await screen.findByPlaceholderText("Ort");
// or with timeout: await screen.findByPlaceholderText("Ort", { timeout: 4000 });
await waitFor(() => expect(inputNode).toHaveValue("Berlin"));