My testing setup includes three components at the moment:
- ContentPostBuilder
- ContentPostBuilderContent
- ContentPostBuilderContentValue
The component tree is scoped to a specific GraphQL type: ContentPost. Besides of root-level properties, it has a property called “content” which is the relevant property for this issue. The value is an array that can have multiple types of elements as values in any order, hence the query is augmented with __typename before being sent. Think of the element values of the content array as a tagged union, where __typename is the discriminator.
ContentPostBuilder is responsible for storing the GraphQL query and to provide the ContentPostContext with the retrieved data and the introspection model. The addGraphQlProperty function on the context is supposed to be used from child components to register a GraphQL-Path that should trigger an update in ContentPostBuilder to include that property path in the GraphQL-Query. This is the feature that I cannot seem to get working.
ContentPostBuilderContent uses the __typename from the actual data to decide if it renders or not. For debugging purposes it outputs “__typename mismatch” if the user-selected __typename does not match the data typename. In the expected end result, this just returns null so it does not render. Furthermore, the ContentPostBuilderContext passes the current schema, gql schema path and data slice from the GQL query to its children.
Finally, the ContentPostBuilderContentValue consumes both contexts to allow the user to select properties from the currently selected __typename in the content. Whenever the user changes selection, an update should be made to the GraphQL query in the ContentPostBuilder. Thats why it calls builderContext.addGraphQlPropertyPath(gqlPath). This does not work however, because I cannot manipulate properties in ContentPostBuilder with setControlContextData. This makes sense and documentation states that only choice types will trigger once the function is called.
Some example content value:
[
{"__typename":"header", "title":"some title", "subtitle": "a minor subtitle"},
{"__typename":"TextBlock", "textcontent":"some longer textcontent"
]
This means that the user can use a ContentPostBuilderContent element and choose between “header” and “textblock” with the dropdown. If the user selected “header” and if the user adds a ContentPostBuilderContentValue as a child to ContentPostBuilderContent, the user can select “title” or “subtitle” and the GraphQL-Query in the ContentPostBuilder should update with the path given via the callback in the context.
All ContentPostBuilder children are single instances of a conditional renderer for a specific __typename of the tagged union, thats why we include all children for each content element in ContentPostBuilder.
I also tried to iterate all children and collect the used GraphQL-properties via studioops, but that did not work as expected either. Any guidance would be appreciated.
export type ContentPostBuilderProps = {
children: ReactElement;
introspectionModel?: any;
startIndex: number;
page: number;
pageSize: number;
query: undefined | { query: string };
setControlContextData?: (ctxData: { query: string }) => void;
};
export type ContentPostContext = {
data: Record<string, any> | null;
introspectionModel: GraphQLSchema | null;
addGraphQlPropertyPath: (valuePath: string) => void;
};
export const ContentPostContext = React.createContext<ContentPostContext>({
data: null,
introspectionModel: null,
addGraphQlPropertyPath: _ => _,
});
export default function ContentPostBuilder({
children,
page,
query,
pageSize,
introspectionModel,
setControlContextData,
}: ContentPostBuilderProps) {
const { client, stage } = useGraphQlContext();
const inEditor = !!usePlasmicCanvasContext();
if (!query) {
return <p>Configure a GraphQl query.</p>;
}
if (!introspectionModel) {
return <>Please use introspection once.</>;
}
const augmentedQuery = augmentWithTypename(query.query);
const { data } = usePlasmicQueryData(augmentedQuery, async () => {
const data = await client.request(augmentedQuery, {
skip: pageSize * (page - 1),
first: pageSize,
});
const keys = Object.keys(data);
if (keys.length > 1) {
throw new Error('Expected only one result type in GraphQL Query.');
}
return data;
});
if (!data) {
return <>no data</>;
}
let contentPost = data.contentPost as any;
const schema = buildClientSchema(introspectionModel);
return (
<div>
{contentPost.content.map(contentEntry => {
return (
<ContentPostContext.Provider
value={{
data: contentEntry,
introspectionModel: schema,
addGraphQlPropertyPath: newValue => {
console.log('updateGraphQLQuery', newValue);
setControlContextData!({
query: 'test',
});
},
}}
>
{children}
</ContentPostContext.Provider>
);
})}
{query.query}
</div>
);
}
export const contentPostBuilderComponentMeta: CodeComponentMeta<ContentPostBuilderProps> =
{
name: 'company-content-post-builder',
displayName: 'company Content Post Builder',
importPath: '@company/plasmic-components',
importName: 'companyContentPostBuilder',
props: {
children: {
type: 'slot',
defaultValue: [],
},
query: {
// this seems to not be updateable by setControlContextData calls
type: 'code',
lang: 'graphql',
readOnly: false,
endpoint: (props, ctx) =>
'...',
headers: (props, ctx) => ({
//...
}),
},
page: {
type: 'number',
defaultValue: 1,
},
pageSize: {
type: 'number',
defaultValue: 10,
},
introspectionModel: {
type: 'object',
description:
'Stores the introspection model for Hygraph content elements.',
readOnly: true,
},
graphQlDataPaths: {
type: 'array',
defaultValue: [],
},
},
actions: [
{
type: 'button-action',
label: 'Introspect',
onClick: async ({ studioOps }) => {
studioOps.updateProps({
introspectionModel: await getFullIntrospection(),
});
},
},
{
type: 'button-action',
label: 'Reset Query',
onClick: async ({ studioOps, componentProps, contextData }) => {
// This was an attempt to iterate all children and extract used graphql-paths from them
const queue = [];
let gqlPaths = [];
Children.forEach(componentProps.children, c => queue.push(c));
let current: any = null;
while ((current = queue.shift())) {
if (current.props.children) {
if (current.props.children) {
Children.forEach(current.props.children, c => queue.push(c));
}
if (current.props.gqlPathValue) {
gqlPaths.push(current.props.gqlPathValue);
}
}
}
console.log(
gqlPaths,
componentProps,
contextData,
componentProps.children
);
},
},
],
};
export const registerContentPostBuilder = buildRegisterComponentFn(
ContentPostBuilder,
contentPostBuilderComponentMeta
);
export const ContentPostBuilderContentContex = React.createContext<{
schema: GraphQLNamedType | null;
selectedComponent: string;
dataSlice: null;
currentGqlPath: string;
}>({
schema: null,
selectedComponent: '',
dataSlice: null,
currentGqlPath: '',
});
export type ContentBuilderElementProps = {
children: ReactElement;
component?: string;
renderElement: boolean;
setControlContextData?: (ctxData: { allComponents: string[] }) => void;
};
export default function ContentPostBuilderContent({
children,
component,
setControlContextData,
}: ContentBuilderElementProps) {
const inEditor = !!usePlasmicCanvasContext();
const context = ensure(useContext(ContentPostContext));
const gqlTypeDefinition = context.introspectionModel?.getType(
'ContentPostcontentUnion'
);
if (isUnionType(gqlTypeDefinition)) {
setControlContextData?.({
allComponents: gqlTypeDefinition.getTypes().map(t => t.name),
});
}
if (!component) {
if (inEditor) {
return (
<p>
Please select a the Content Type to render for the Content Builder
Element.
</p>
);
}
}
if (!gqlTypeDefinition) {
return <p>Could not resolve ContentPostcontentUnion</p>;
}
if (context.data!.__typename !== component) {
return <p>Typename mismatch</p>;
}
return (
<ContentPostBuilderContentContex.Provider
value={{
schema: gqlTypeDefinition,
dataSlice: context.data,
selectedComponent: component || '',
currentGqlPath: 'content.#' + component,
}}
>
<h1>{children}</h1>
</ContentPostBuilderContentContex.Provider>
);
}
export const contentBuilderElementComponentMeta: CodeComponentMeta<ContentBuilderElementProps> =
{
name: 'company-content-post-element',
displayName: 'company Content Builder Content',
importPath: '@company/plasmic-components',
importName: 'ContentBuilderElement',
props: {
children: 'slot',
component: {
type: 'choice',
options: (props, ctx) => {
return ctx?.allComponents || [];
},
},
},
};
export const registerContentPostBuilderContent = buildRegisterComponentFn(
ContentPostBuilderContent,
contentBuilderElementComponentMeta
);
export type ContentBuilderElementProps = {
children: ReactElement;
value: string;
setControlContextData?: (ctxData: {
value: string[];
gqlPathValue: string;
}) => void;
};
export default function ContentPostBuilderContentValue({
value,
setControlContextData,
}: ContentBuilderElementProps) {
const inEditor = !!usePlasmicCanvasContext();
const context = ensure(useContext(ContentPostBuilderContentContex));
const builderContext = ensure(useContext(ContentPostContext));
const currentGqlType = builderContext.introspectionModel?.getType(
context.selectedComponent
);
if (isObjectType(currentGqlType)) {
setControlContextData?.({ value: Object.keys(currentGqlType.getFields()) });
}
if (value !== '' && inEditor) {
const gqlPath = context.currentGqlPath + '.' + value;
builderContext.addGraphQlPropertyPath(gqlPath);
setControlContextData?.({
gqlPathValue: gqlPath,
value: currentGqlType ? Object.keys(currentGqlType.getFields()) : [],
});
}
return <>{context.dataSlice[value] || value + ' does not exist on data.'}</>;
}
export const contentBuilderElementComponentMeta: CodeComponentMeta<ContentBuilderElementProps> =
{
name: 'company-content-post-element-value',
displayName: 'company Content Builder Content Value',
importPath: '@company/plasmic-components',
importName: 'ContentBuilderElementValue',
props: {
children: 'slot',
renderElement: {
type: 'boolean',
description:
'Renders this element even it is not used in the selected preview content post.',
defaultValue: false,
},
value: {
type: 'choice',
options: (props, ctx) => {
console.log('SET');
return ctx?.value || [];
},
},
// this prop can also not be set via code
gqlPathValue: {
type: 'string',
},
},
};
export const registerContentPostBuilderContentValue = buildRegisterComponentFn(
ContentPostBuilderContentValue,
contentBuilderElementComponentMeta
);