-
Notifications
You must be signed in to change notification settings - Fork 316
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
07999ea
commit d868f2d
Showing
17 changed files
with
697 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"saleor-app-products-feed": minor | ||
--- | ||
|
||
Added new feature: Attributes required by the Google Merchant can now be mapped to product attributes. |
5 changes: 5 additions & 0 deletions
5
apps/products-feed/graphql/fragments/AttributeWithMappingFragment.graphql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
fragment AttributeWithMappingFragment on Attribute { | ||
id | ||
name | ||
slug | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
apps/products-feed/graphql/queries/FetchAttributesWithMapping.graphql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
query FetchAttributesWithMapping($cursor: String){ | ||
attributes(first: 100, after: $cursor){ | ||
pageInfo{ | ||
hasNextPage | ||
endCursor | ||
} | ||
edges{ | ||
node{ | ||
...AttributeWithMappingFragment | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
apps/products-feed/src/modules/app-configuration/attribute-fetcher.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { Client } from "urql"; | ||
import { | ||
AttributeWithMappingFragmentFragment, | ||
FetchAttributesWithMappingDocument, | ||
} from "../../../generated/graphql"; | ||
|
||
export class AttributeFetcher { | ||
constructor(private apiClient: Pick<Client, "query">) {} | ||
|
||
private async fetchRecursivePage( | ||
accumulator: AttributeWithMappingFragmentFragment[], | ||
cursor?: string | ||
): Promise<AttributeWithMappingFragmentFragment[]> { | ||
const result = await this.apiClient | ||
.query(FetchAttributesWithMappingDocument, { | ||
cursor, | ||
}) | ||
.toPromise(); | ||
|
||
if (result.error) { | ||
throw new Error(result.error.message); | ||
} | ||
|
||
if (!result.data) { | ||
// todo sentry | ||
throw new Error("Empty attributes data"); | ||
} | ||
|
||
accumulator = [...accumulator, ...(result.data.attributes?.edges.map((c) => c.node) ?? [])]; | ||
|
||
const hasNextPage = result.data.attributes?.pageInfo.hasNextPage; | ||
const endCursor = result.data.attributes?.pageInfo.endCursor; | ||
|
||
if (hasNextPage && endCursor) { | ||
return this.fetchRecursivePage(accumulator, endCursor); | ||
} else { | ||
return accumulator; | ||
} | ||
} | ||
|
||
/** | ||
* Fetches all attribute pages - standard page is max 100 items | ||
*/ | ||
async fetchAllAttributes(): Promise<AttributeWithMappingFragmentFragment[]> { | ||
let attributes: AttributeWithMappingFragmentFragment[] = []; | ||
|
||
return this.fetchRecursivePage(attributes, undefined); | ||
} | ||
} |
131 changes: 131 additions & 0 deletions
131
apps/products-feed/src/modules/app-configuration/attribute-mapping-form.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import { AppConfigSchema, RootConfig } from "./app-config"; | ||
import { useForm } from "react-hook-form"; | ||
|
||
import { Box, Button, Text } from "@saleor/macaw-ui/next"; | ||
|
||
import React, { useCallback, useMemo } from "react"; | ||
import { Multiselect } from "@saleor/react-hook-form-macaw"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { trpcClient } from "../trpc/trpc-client"; | ||
import { useDashboardNotification } from "@saleor/apps-shared"; | ||
import { AttributeWithMappingFragmentFragment } from "../../../generated/graphql"; | ||
|
||
type AttributeMappingConfiguration = Exclude<RootConfig["attributeMapping"], null>; | ||
|
||
type Props = { | ||
initialData: AttributeMappingConfiguration; | ||
attributes: AttributeWithMappingFragmentFragment[]; | ||
onSubmit(data: AttributeMappingConfiguration): Promise<void>; | ||
}; | ||
|
||
export const AttributeMappingConfigurationForm = (props: Props) => { | ||
const { handleSubmit, control } = useForm<AttributeMappingConfiguration>({ | ||
defaultValues: props.initialData, | ||
resolver: zodResolver(AppConfigSchema.attributeMapping), | ||
}); | ||
|
||
const options = props.attributes.map((a) => ({ value: a.id, label: a.name || a.id })) || []; | ||
|
||
return ( | ||
<Box | ||
as={"form"} | ||
display={"flex"} | ||
gap={5} | ||
flexDirection={"column"} | ||
onSubmit={handleSubmit((data) => { | ||
props.onSubmit(data); | ||
})} | ||
> | ||
<Multiselect | ||
control={control} | ||
name="brandAttributeIds" | ||
label="Brand attributes" | ||
options={options} | ||
/> | ||
<Multiselect | ||
control={control} | ||
name="sizeAttributeIds" | ||
label="Size attributes" | ||
options={options} | ||
/> | ||
<Multiselect | ||
control={control} | ||
name="materialAttributeIds" | ||
label="Material attributes" | ||
options={options} | ||
/> | ||
<Multiselect | ||
control={control} | ||
name="colorAttributeIds" | ||
label="Color attributes" | ||
options={options} | ||
/> | ||
|
||
<Box display={"flex"} flexDirection={"row"} gap={4} justifyContent={"flex-end"}> | ||
<Button type="submit" variant="primary"> | ||
Save mapping | ||
</Button> | ||
</Box> | ||
</Box> | ||
); | ||
}; | ||
|
||
export const ConnectedAttributeMappingForm = () => { | ||
const { notifyError, notifySuccess } = useDashboardNotification(); | ||
|
||
const { data: attributes, isLoading: isAttributesLoading } = | ||
trpcClient.appConfiguration.getAttributes.useQuery(); | ||
|
||
const { data, isLoading: isConfigurationLoading } = trpcClient.appConfiguration.fetch.useQuery(); | ||
|
||
const isLoading = isAttributesLoading || isConfigurationLoading; | ||
|
||
const { mutate } = trpcClient.appConfiguration.setAttributeMapping.useMutation({ | ||
onSuccess() { | ||
notifySuccess("Success", "Updated attribute mapping"); | ||
}, | ||
onError() { | ||
notifyError("Error", "Failed to update, please refresh and try again"); | ||
}, | ||
}); | ||
|
||
const handleSubmit = useCallback( | ||
async (data: AttributeMappingConfiguration) => { | ||
mutate(data); | ||
}, | ||
[mutate] | ||
); | ||
|
||
const formData: AttributeMappingConfiguration = useMemo(() => { | ||
if (data?.attributeMapping) { | ||
return data.attributeMapping; | ||
} | ||
|
||
return { | ||
colorAttributeIds: [], | ||
sizeAttributeIds: [], | ||
brandAttributeIds: [], | ||
materialAttributeIds: [], | ||
}; | ||
}, [data]); | ||
|
||
if (isLoading) { | ||
return <Text>Loading...</Text>; | ||
} | ||
|
||
const showForm = !isLoading && attributes?.length; | ||
|
||
return ( | ||
<> | ||
{showForm ? ( | ||
<AttributeMappingConfigurationForm | ||
onSubmit={handleSubmit} | ||
initialData={formData} | ||
attributes={attributes} | ||
/> | ||
) : ( | ||
<Box>Loading</Box> | ||
)} | ||
</> | ||
); | ||
}; |
Oops, something went wrong.