@payloadcms/storage-r2 0.0.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.prettierignore +12 -0
  2. package/.swcrc +24 -0
  3. package/LICENSE.md +22 -0
  4. package/README.md +3 -0
  5. package/eslint.config.js +18 -0
  6. package/package.json +4 -0
  7. package/src/addresses/addressesCollection.ts +76 -0
  8. package/src/addresses/defaultAddressFields.ts +83 -0
  9. package/src/addresses/defaultCountries.ts +50 -0
  10. package/src/carts/beforeChange.ts +51 -0
  11. package/src/carts/cartsCollection.ts +146 -0
  12. package/src/currencies/index.ts +29 -0
  13. package/src/endpoints/confirmOrder.ts +312 -0
  14. package/src/endpoints/initiatePayment.ts +322 -0
  15. package/src/exports/addresses.ts +2 -0
  16. package/src/exports/currencies.ts +1 -0
  17. package/src/exports/fields.ts +5 -0
  18. package/src/exports/orders.ts +1 -0
  19. package/src/exports/payments/stripe.ts +1 -0
  20. package/src/exports/plugin.ts +1 -0
  21. package/src/exports/products.ts +1 -0
  22. package/src/exports/react.ts +8 -0
  23. package/src/exports/transactions.ts +1 -0
  24. package/src/exports/translations.ts +1 -0
  25. package/src/exports/types.ts +7 -0
  26. package/src/exports/ui.ts +3 -0
  27. package/src/exports/variants.ts +5 -0
  28. package/src/fields/amountField.ts +43 -0
  29. package/src/fields/cartItemsField.ts +84 -0
  30. package/src/fields/currencyField.ts +39 -0
  31. package/src/fields/inventoryField.ts +22 -0
  32. package/src/fields/pricesField.ts +65 -0
  33. package/src/fields/statusField.ts +57 -0
  34. package/src/fields/variantsFields.ts +56 -0
  35. package/src/index.ts +275 -0
  36. package/src/orders/ordersCollection.ts +157 -0
  37. package/src/payments/adapters/stripe/confirmOrder.ts +123 -0
  38. package/src/payments/adapters/stripe/endpoints/webhooks.ts +69 -0
  39. package/src/payments/adapters/stripe/index.ts +135 -0
  40. package/src/payments/adapters/stripe/initiatePayment.ts +131 -0
  41. package/src/products/productsCollection.ts +78 -0
  42. package/src/react/provider/index.tsx +893 -0
  43. package/src/react/provider/types.ts +184 -0
  44. package/src/react/provider/utilities.ts +22 -0
  45. package/src/transactions/transactionsCollection.ts +166 -0
  46. package/src/translations/en.ts +64 -0
  47. package/src/translations/index.ts +11 -0
  48. package/src/translations/translation-schema.json +35 -0
  49. package/src/types.ts +403 -0
  50. package/src/ui/PriceInput/FormattedInput.tsx +134 -0
  51. package/src/ui/PriceInput/index.scss +28 -0
  52. package/src/ui/PriceInput/index.tsx +43 -0
  53. package/src/ui/PriceInput/utilities.ts +46 -0
  54. package/src/ui/PriceRowLabel/index.css +13 -0
  55. package/src/ui/PriceRowLabel/index.tsx +56 -0
  56. package/src/ui/VariantOptionsSelector/ErrorBox.tsx +27 -0
  57. package/src/ui/VariantOptionsSelector/OptionsSelect.tsx +78 -0
  58. package/src/ui/VariantOptionsSelector/index.css +37 -0
  59. package/src/ui/VariantOptionsSelector/index.tsx +83 -0
  60. package/src/utilities/defaultProductsValidation.ts +42 -0
  61. package/src/utilities/errorCodes.ts +14 -0
  62. package/src/utilities/getCollectionSlugMap.ts +84 -0
  63. package/src/utilities/sanitizePluginConfig.ts +80 -0
  64. package/src/variants/variantOptionsCollection.ts +59 -0
  65. package/src/variants/variantTypesCollection.ts +55 -0
  66. package/src/variants/variantsCollection/hooks/beforeChange.ts +47 -0
  67. package/src/variants/variantsCollection/hooks/validateOptions.ts +72 -0
  68. package/src/variants/variantsCollection/index.ts +119 -0
  69. package/tsconfig.json +7 -0
  70. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,56 @@
1
+ 'use client'
2
+
3
+ import { useRowLabel } from '@payloadcms/ui'
4
+ import { useMemo } from 'react'
5
+
6
+ import type { CurrenciesConfig } from '../../types.js'
7
+
8
+ import './index.css'
9
+ import { convertFromBaseValue } from '../PriceInput/utilities.js'
10
+
11
+ type Props = {
12
+ currenciesConfig: CurrenciesConfig
13
+ }
14
+
15
+ export const PriceRowLabel: React.FC<Props> = (props) => {
16
+ const { currenciesConfig } = props
17
+ const { defaultCurrency, supportedCurrencies } = currenciesConfig
18
+
19
+ const { data } = useRowLabel<{ amount: number; currency: string }>()
20
+
21
+ const currency = useMemo(() => {
22
+ if (data.currency) {
23
+ return supportedCurrencies.find((c) => c.code === data.currency) ?? supportedCurrencies[0]
24
+ }
25
+
26
+ const fallbackCurrency = supportedCurrencies.find((c) => c.code === defaultCurrency)
27
+
28
+ if (fallbackCurrency) {
29
+ return fallbackCurrency
30
+ }
31
+
32
+ return supportedCurrencies[0]
33
+ }, [data.currency, supportedCurrencies, defaultCurrency])
34
+
35
+ const amount = useMemo(() => {
36
+ if (data.amount) {
37
+ return convertFromBaseValue({ baseValue: data.amount, currency: currency! })
38
+ }
39
+
40
+ return '0'
41
+ }, [currency, data.amount])
42
+
43
+ return (
44
+ <div className="priceRowLabel">
45
+ <div className="priceLabel">Price:</div>
46
+
47
+ <div className="priceValue">
48
+ <span>
49
+ {currency?.symbol}
50
+ {amount}
51
+ </span>
52
+ <span>({data.currency})</span>
53
+ </div>
54
+ </div>
55
+ )
56
+ }
@@ -0,0 +1,27 @@
1
+ 'use client'
2
+
3
+ import { FieldError, useField, useTranslation } from '@payloadcms/ui'
4
+
5
+ type Props = {
6
+ children?: React.ReactNode
7
+ existingOptions: string[]
8
+ path: string
9
+ }
10
+
11
+ export const ErrorBox: React.FC<Props> = (props) => {
12
+ const { children, path } = props
13
+ const { errorMessage, showError } = useField<(number | string)[]>({ path })
14
+
15
+ return (
16
+ <div className="variantOptionsSelectorError">
17
+ <FieldError message={errorMessage} path={path} showError={showError} />
18
+ <div
19
+ className={['variantOptionsSelectorErrorWrapper', showError && 'showError']
20
+ .filter(Boolean)
21
+ .join(' ')}
22
+ >
23
+ {children}
24
+ </div>
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,78 @@
1
+ 'use client'
2
+
3
+ import type { SelectFieldClient } from 'payload'
4
+
5
+ import { FieldLabel, ReactSelect, useField, useForm } from '@payloadcms/ui'
6
+ import { useCallback, useId, useMemo } from 'react'
7
+
8
+ type Props = {
9
+ field: Omit<SelectFieldClient, 'type'>
10
+ label: string
11
+ options: { label: string; value: number | string }[]
12
+ path: string
13
+ }
14
+
15
+ export const OptionsSelect: React.FC<Props> = (props) => {
16
+ const {
17
+ field: { required },
18
+ label,
19
+ options: optionsFromProps,
20
+ path,
21
+ } = props
22
+
23
+ const { setValue, value } = useField<(number | string)[]>({ path })
24
+ const id = useId()
25
+
26
+ const selectedValue = useMemo(() => {
27
+ if (!value || !Array.isArray(value) || value.length === 0) {
28
+ return undefined
29
+ }
30
+ const foundOption = optionsFromProps.find((option) => {
31
+ return value.find((item) => item === option.value)
32
+ })
33
+
34
+ return foundOption
35
+ }, [optionsFromProps, value])
36
+
37
+ const handleChange = useCallback(
38
+ // @ts-expect-error - TODO: Fix types
39
+ (option) => {
40
+ if (selectedValue) {
41
+ let selectedValueIndex = -1
42
+
43
+ const valuesWithoutSelected = [...value].filter((o, index) => {
44
+ if (o === selectedValue.value) {
45
+ selectedValueIndex = index
46
+ return false
47
+ }
48
+
49
+ return true
50
+ })
51
+
52
+ const newValues = [...valuesWithoutSelected]
53
+
54
+ newValues.splice(selectedValueIndex, 0, option.value)
55
+
56
+ setValue(newValues)
57
+ } else {
58
+ const values = [...(value || []), option.value]
59
+
60
+ setValue(values)
61
+ }
62
+ },
63
+ [selectedValue, setValue, value],
64
+ )
65
+
66
+ return (
67
+ <div className="variantOptionsSelectorItem">
68
+ <FieldLabel htmlFor={id} label={label} required={required} />
69
+
70
+ <ReactSelect
71
+ inputId={id}
72
+ onChange={handleChange}
73
+ options={optionsFromProps}
74
+ value={selectedValue}
75
+ />
76
+ </div>
77
+ )
78
+ }
@@ -0,0 +1,37 @@
1
+ @layer payload-default {
2
+ .variantOptionsSelector {
3
+ margin-top: calc(var(--spacing-field) * 2);
4
+ margin-bottom: calc(var(--spacing-field) * 2);
5
+ }
6
+
7
+ .variantOptionsSelectorHeading {
8
+ font-size: calc(var(--base) * 1);
9
+ font-weight: 500;
10
+ color: var(--color-text);
11
+ margin-bottom: calc(var(--base) * 0.5);
12
+ }
13
+
14
+ .variantOptionsSelectorItem {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: 0;
18
+ }
19
+
20
+ .variantOptionsSelectorList {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: calc(var(--base) * 0.75);
24
+ }
25
+
26
+ .variantOptionsSelectorError {
27
+ position: relative;
28
+ }
29
+
30
+ .variantOptionsSelectorErrorWrapper {
31
+ &.showError {
32
+ border-radius: 2px;
33
+ outline: 1px solid var(--theme-error-400);
34
+ outline-offset: 2px;
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,83 @@
1
+ import type { SelectFieldServerProps } from 'payload'
2
+
3
+ import { FieldLabel } from '@payloadcms/ui'
4
+
5
+ import { ErrorBox } from './ErrorBox.js'
6
+ import './index.css'
7
+ import { OptionsSelect } from './OptionsSelect.js'
8
+ type Props = {} & SelectFieldServerProps
9
+
10
+ export const VariantOptionsSelector: React.FC<Props> = async (props) => {
11
+ const { clientField, data, path, req, user } = props
12
+ const { label } = clientField
13
+
14
+ const product = await req.payload.findByID({
15
+ id: data.product,
16
+ collection: 'products',
17
+ depth: 0,
18
+ select: {
19
+ variants: true,
20
+ variantTypes: true,
21
+ },
22
+ user,
23
+ })
24
+
25
+ // @ts-expect-error - TODO: Fix types
26
+ const existingVariantOptions = product.variants?.docs?.map((variant) => variant.options) ?? []
27
+
28
+ const variantTypeIDs = product.variantTypes
29
+
30
+ const variantTypes = []
31
+
32
+ // Need to get the variant types separately so that the options are populated
33
+ // @ts-expect-error - TODO: Fix types
34
+ if (variantTypeIDs?.length && variantTypeIDs.length > 0) {
35
+ // @ts-expect-error - TODO: Fix types
36
+ for (const variantTypeID of variantTypeIDs) {
37
+ const variantType = await req.payload.findByID({
38
+ id: variantTypeID,
39
+ collection: 'variantTypes',
40
+ depth: 1,
41
+ joins: {
42
+ options: {
43
+ sort: 'value',
44
+ },
45
+ },
46
+ })
47
+
48
+ if (variantType) {
49
+ variantTypes.push(variantType)
50
+ }
51
+ }
52
+ }
53
+
54
+ return (
55
+ <div className="variantOptionsSelector">
56
+ <div className="variantOptionsSelectorHeading">
57
+ <FieldLabel as="span" label={label} />
58
+ </div>
59
+
60
+ <ErrorBox existingOptions={existingVariantOptions} path={path}>
61
+ <div className="variantOptionsSelectorList">
62
+ {variantTypes.map((type) => {
63
+ // @ts-expect-error - TODO: Fix types
64
+ const options = type.options.docs.map((option) => ({
65
+ label: option.label,
66
+ value: option.id,
67
+ }))
68
+
69
+ return (
70
+ <OptionsSelect
71
+ field={clientField}
72
+ key={type.name}
73
+ label={type.label || type.name}
74
+ options={options}
75
+ path={path}
76
+ />
77
+ )
78
+ })}
79
+ </div>
80
+ </ErrorBox>
81
+ </div>
82
+ )
83
+ }
@@ -0,0 +1,42 @@
1
+ import type { ProductsValidation } from '../types.js'
2
+
3
+ import { MissingPrice, OutOfStock } from './errorCodes.js'
4
+
5
+ export const defaultProductsValidation: ProductsValidation = ({
6
+ currenciesConfig,
7
+ currency,
8
+ product,
9
+ quantity = 1,
10
+ variant,
11
+ }) => {
12
+ if (!currency) {
13
+ throw new Error('Currency must be provided for product validation.')
14
+ }
15
+
16
+ const priceField = `priceIn${currency.toUpperCase()}`
17
+
18
+ if (variant) {
19
+ if (!variant[priceField]) {
20
+ throw new Error(`Variant with ID ${variant.id} does not have a price in ${currency}.`)
21
+ }
22
+
23
+ if (variant.inventory === 0 || (variant.inventory && variant.inventory < quantity)) {
24
+ throw new Error(
25
+ `Variant with ID ${variant.id} is out of stock or does not have enough inventory.`,
26
+ )
27
+ }
28
+ } else if (product) {
29
+ // Validate the product's details only if the variant is not provided as it can have its own inventory and price
30
+ if (!product[priceField]) {
31
+ throw new Error(`Product does not have a price in.`, {
32
+ cause: { code: MissingPrice, codes: [product.id, currency] },
33
+ })
34
+ }
35
+
36
+ if (product.inventory === 0 || (product.inventory && product.inventory < quantity)) {
37
+ throw new Error(`Product is out of stock or does not have enough inventory.`, {
38
+ cause: { code: OutOfStock, codes: [product.id] },
39
+ })
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,14 @@
1
+ export const AddressNotFound = 'AddressNotFound'
2
+ export const AddressValidationError = 'AddressValidationError'
3
+ export const InvalidCurrency = 'InvalidCurrency'
4
+ export const InvalidPaymentMethod = 'InvalidPaymentMethod'
5
+ export const InvalidQuantity = 'InvalidQuantity'
6
+ export const MissingCurrency = 'MissingCurrency'
7
+ export const MissingInventory = 'MissingInventory'
8
+ export const MissingPrice = 'MissingPrice'
9
+ export const MissingProduct = 'MissingProduct'
10
+ export const MissingVariant = 'MissingVariant'
11
+ export const NotEnoughInventory = 'NotEnoughInventory'
12
+ export const OutOfStock = 'OutOfStock'
13
+ export const PaymentFailed = 'PaymentFailed'
14
+ export const UserNotAuthenticated = 'UserNotAuthenticated'
@@ -0,0 +1,84 @@
1
+ import type { CollectionSlugMap, SanitizedEcommercePluginConfig } from '../types.js'
2
+
3
+ type Props = {
4
+ sanitizedPluginConfig: SanitizedEcommercePluginConfig
5
+ }
6
+
7
+ /**
8
+ * Generates a map of collection slugs based on the sanitized plugin configuration.
9
+ * Takes into consideration any collection overrides provided in the plugin.
10
+ */
11
+ export const getCollectionSlugMap = ({ sanitizedPluginConfig }: Props): CollectionSlugMap => {
12
+ const defaultSlugMap: CollectionSlugMap = {
13
+ addresses: 'addresses',
14
+ carts: 'carts',
15
+ customers: 'users',
16
+ orders: 'orders',
17
+ products: 'products',
18
+ transactions: 'transactions',
19
+ variantOptions: 'variantOptions',
20
+ variants: 'variants',
21
+ variantTypes: 'variantTypes',
22
+ }
23
+
24
+ const collectionSlugsMap: CollectionSlugMap = {
25
+ ...defaultSlugMap,
26
+ }
27
+
28
+ if (typeof sanitizedPluginConfig.customers === 'object' && sanitizedPluginConfig.customers.slug) {
29
+ collectionSlugsMap.customers = sanitizedPluginConfig.customers.slug
30
+
31
+ if (
32
+ typeof sanitizedPluginConfig.addresses === 'object' &&
33
+ sanitizedPluginConfig.addresses.collectionOverride?.slug
34
+ ) {
35
+ collectionSlugsMap.addresses = sanitizedPluginConfig.addresses.collectionOverride.slug
36
+ }
37
+ }
38
+
39
+ if (
40
+ typeof sanitizedPluginConfig.orders === 'object' &&
41
+ sanitizedPluginConfig.orders.ordersCollection?.slug
42
+ ) {
43
+ collectionSlugsMap.orders = sanitizedPluginConfig.orders.ordersCollection.slug
44
+ }
45
+
46
+ if (typeof sanitizedPluginConfig.products === 'object') {
47
+ if (sanitizedPluginConfig.products.productsCollection?.slug) {
48
+ collectionSlugsMap.products = sanitizedPluginConfig.products.productsCollection.slug
49
+ }
50
+
51
+ if (typeof sanitizedPluginConfig.products.variants === 'object') {
52
+ if (sanitizedPluginConfig.products.variants.variantsCollection?.slug) {
53
+ collectionSlugsMap.variants =
54
+ sanitizedPluginConfig.products.variants.variantsCollection.slug
55
+ }
56
+
57
+ if (sanitizedPluginConfig.products.variants.variantOptionsCollection?.slug) {
58
+ collectionSlugsMap.variantOptions =
59
+ sanitizedPluginConfig.products.variants.variantOptionsCollection.slug
60
+ }
61
+
62
+ if (sanitizedPluginConfig.products.variants.variantTypesCollection?.slug) {
63
+ collectionSlugsMap.variantTypes =
64
+ sanitizedPluginConfig.products.variants.variantTypesCollection.slug
65
+ }
66
+ }
67
+ }
68
+
69
+ if (
70
+ typeof sanitizedPluginConfig.transactions === 'object' &&
71
+ sanitizedPluginConfig.transactions.transactionsCollection?.slug
72
+ ) {
73
+ collectionSlugsMap.transactions = sanitizedPluginConfig.transactions.transactionsCollection.slug
74
+ }
75
+
76
+ if (
77
+ typeof sanitizedPluginConfig.carts === 'object' &&
78
+ sanitizedPluginConfig.carts.cartsCollection?.slug
79
+ ) {
80
+ collectionSlugsMap.carts = sanitizedPluginConfig.carts.cartsCollection.slug
81
+ }
82
+
83
+ return collectionSlugsMap
84
+ }
@@ -0,0 +1,80 @@
1
+ import type { EcommercePluginConfig, SanitizedEcommercePluginConfig } from '../types.js'
2
+
3
+ import { defaultAddressFields } from '../addresses/defaultAddressFields.js'
4
+ import { USD } from '../currencies/index.js'
5
+
6
+ type Props = {
7
+ pluginConfig: EcommercePluginConfig
8
+ }
9
+
10
+ export const sanitizePluginConfig = ({ pluginConfig }: Props): SanitizedEcommercePluginConfig => {
11
+ const config = {
12
+ ...pluginConfig,
13
+ } as Partial<SanitizedEcommercePluginConfig>
14
+
15
+ if (typeof config.customers === 'undefined') {
16
+ config.customers = {
17
+ slug: 'users',
18
+ }
19
+ }
20
+
21
+ if (
22
+ typeof config.addresses === 'undefined' ||
23
+ (typeof config.addresses === 'boolean' && config.addresses === true)
24
+ ) {
25
+ config.addresses = {
26
+ addressFields: defaultAddressFields(),
27
+ }
28
+ } else {
29
+ const addressFields =
30
+ (typeof pluginConfig.addresses === 'object' &&
31
+ typeof pluginConfig.addresses.addressFields === 'function' &&
32
+ pluginConfig.addresses.addressFields({
33
+ defaultFields: defaultAddressFields(),
34
+ })) ||
35
+ defaultAddressFields()
36
+
37
+ config.addresses = {
38
+ ...config.addresses,
39
+ addressFields,
40
+ }
41
+ }
42
+
43
+ if (!config.currencies) {
44
+ config.currencies = {
45
+ defaultCurrency: 'USD',
46
+ supportedCurrencies: [USD],
47
+ }
48
+ }
49
+
50
+ if (
51
+ typeof config.inventory === 'undefined' ||
52
+ (typeof config.inventory === 'boolean' && config.inventory === true)
53
+ ) {
54
+ config.inventory = {
55
+ fieldName: 'inventory',
56
+ }
57
+ }
58
+
59
+ if (typeof config.carts === 'undefined') {
60
+ config.carts = true
61
+ }
62
+
63
+ if (typeof config.orders === 'undefined') {
64
+ config.orders = true
65
+ }
66
+
67
+ if (typeof config.transactions === 'undefined') {
68
+ config.transactions = true
69
+ }
70
+
71
+ if (typeof config.payments === 'undefined') {
72
+ config.payments = {
73
+ paymentMethods: [],
74
+ }
75
+ } else if (!config.payments.paymentMethods) {
76
+ config.payments.paymentMethods = []
77
+ }
78
+
79
+ return config as SanitizedEcommercePluginConfig
80
+ }
@@ -0,0 +1,59 @@
1
+ import type { CollectionConfig, Field } from 'payload'
2
+
3
+ import type { FieldsOverride } from '../types.js'
4
+
5
+ type Props = {
6
+ overrides?: { fields?: FieldsOverride } & Partial<Omit<CollectionConfig, 'fields'>>
7
+ /**
8
+ * Slug of the variant types collection, defaults to 'variantTypes'.
9
+ */
10
+ variantTypesSlug?: string
11
+ }
12
+
13
+ export const variantOptionsCollection: (props?: Props) => CollectionConfig = (props) => {
14
+ const { overrides, variantTypesSlug = 'variantTypes' } = props || {}
15
+ const fieldsOverride = overrides?.fields
16
+
17
+ const variantOptionsDefaultFields: Field[] = [
18
+ {
19
+ name: 'variantType',
20
+ type: 'relationship',
21
+ admin: {
22
+ readOnly: true,
23
+ },
24
+ relationTo: variantTypesSlug,
25
+ required: true,
26
+ },
27
+ {
28
+ name: 'label',
29
+ type: 'text',
30
+ required: true,
31
+ },
32
+ {
33
+ name: 'value',
34
+ type: 'text',
35
+ admin: {
36
+ description: 'should be defaulted or dynamic based on label',
37
+ },
38
+ required: true,
39
+ },
40
+ ]
41
+
42
+ const fields =
43
+ fieldsOverride && typeof fieldsOverride === 'function'
44
+ ? fieldsOverride({ defaultFields: variantOptionsDefaultFields })
45
+ : variantOptionsDefaultFields
46
+
47
+ const baseConfig: CollectionConfig = {
48
+ slug: 'variantOptions',
49
+ ...overrides,
50
+ admin: {
51
+ group: false,
52
+ useAsTitle: 'label',
53
+ ...overrides?.admin,
54
+ },
55
+ fields,
56
+ }
57
+
58
+ return { ...baseConfig }
59
+ }
@@ -0,0 +1,55 @@
1
+ import type { CollectionConfig, Field } from 'payload'
2
+
3
+ import type { FieldsOverride } from '../types.js'
4
+
5
+ type Props = {
6
+ overrides?: { fields?: FieldsOverride } & Partial<Omit<CollectionConfig, 'fields'>>
7
+ /**
8
+ * Slug of the variant options collection, defaults to 'variantOptions'.
9
+ */
10
+ variantOptionsSlug?: string
11
+ }
12
+
13
+ export const variantTypesCollection: (props?: Props) => CollectionConfig = (props) => {
14
+ const { overrides, variantOptionsSlug = 'variantOptions' } = props || {}
15
+ const fieldsOverride = overrides?.fields
16
+
17
+ const variantTypesDefaultFields: Field[] = [
18
+ {
19
+ name: 'label',
20
+ type: 'text',
21
+ required: true,
22
+ },
23
+ {
24
+ name: 'name',
25
+ type: 'text',
26
+ required: true,
27
+ },
28
+ {
29
+ name: 'options',
30
+ type: 'join',
31
+ collection: variantOptionsSlug,
32
+ maxDepth: 2,
33
+ on: 'variantType',
34
+ orderable: true,
35
+ },
36
+ ]
37
+
38
+ const fields =
39
+ fieldsOverride && typeof fieldsOverride === 'function'
40
+ ? fieldsOverride({ defaultFields: variantTypesDefaultFields })
41
+ : variantTypesDefaultFields
42
+
43
+ const baseConfig: CollectionConfig = {
44
+ slug: 'variantTypes',
45
+ ...overrides,
46
+ admin: {
47
+ group: false,
48
+ useAsTitle: 'label',
49
+ ...overrides?.admin,
50
+ },
51
+ fields,
52
+ }
53
+
54
+ return { ...baseConfig }
55
+ }
@@ -0,0 +1,47 @@
1
+ import type { CollectionBeforeChangeHook } from 'payload'
2
+
3
+ type Props = {
4
+ productsSlug: string
5
+ variantOptionsSlug: string
6
+ }
7
+
8
+ export const variantsCollectionBeforeChange: (args: Props) => CollectionBeforeChangeHook =
9
+ ({ productsSlug, variantOptionsSlug }) =>
10
+ async ({ data, req }) => {
11
+ if (data?.options?.length && data.options.length > 0) {
12
+ const titleArray = []
13
+ const productID = data.product
14
+ const product = await req.payload.findByID({
15
+ id: productID,
16
+ collection: productsSlug,
17
+ depth: 0,
18
+ select: {
19
+ title: true,
20
+ variantTypes: true,
21
+ },
22
+ })
23
+
24
+ titleArray.push(product.title)
25
+
26
+ for (const option of data.options) {
27
+ const variantOption = await req.payload.findByID({
28
+ id: option,
29
+ collection: variantOptionsSlug,
30
+ depth: 0,
31
+ select: {
32
+ label: true,
33
+ },
34
+ })
35
+
36
+ if (!variantOption) {
37
+ continue
38
+ }
39
+
40
+ titleArray.push(variantOption.label)
41
+ }
42
+
43
+ data.title = titleArray.join(' — ')
44
+ }
45
+
46
+ return data
47
+ }