Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accessing customField properties? #39

Open
danrwilson opened this issue Aug 17, 2021 · 1 comment
Open

Accessing customField properties? #39

danrwilson opened this issue Aug 17, 2021 · 1 comment

Comments

@danrwilson
Copy link

danrwilson commented Aug 17, 2021

Hi, i'm rather new to node & angular. I'm trying to create custom fields for a Product, but having trouble accessing the custom fields i've created.

Custom fields are defined in my vendure-config.ts below.

customFields: {
        Product: [
          { 
              name: 'metaTitle', 
              type: 'string', 
              label: [ { languageCode: LanguageCode.en, value: 'Meta Title' } ] 
          },
          { 
              name: 'metaDescription', 
              type: 'string', 
              label: [ { languageCode: LanguageCode.en, value: 'Meta Description' } ] 
          },
          { 
              name: 'metaKeywords', 
              type: 'string', 
              label: [ { languageCode: LanguageCode.en, value: 'Meta Keywords' } ] 
          }
        ]
    },

product-detail.component.ts


import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { filter, map, switchMap, withLatestFrom } from 'rxjs/operators';

import { AddToCart, GetProductDetail } from '../../../common/generated-types';
import { notNullOrUndefined } from '../../../common/utils/not-null-or-undefined';
import { DataService } from '../../providers/data/data.service';
import { NotificationService } from '../../providers/notification/notification.service';
import { StateService } from '../../providers/state/state.service';

import { ADD_TO_CART, GET_PRODUCT_DETAIL } from './product-detail.graphql';
import { Title, Meta } from '@angular/platform-browser';

@Component({
    selector: 'vsf-product-detail',
    templateUrl: './product-detail.component.html',
    styleUrls: ['./product-detail.component.scss'],
})
export class ProductDetailComponent implements OnInit, OnDestroy {

    product: GetProductDetail.Product;

    selectedAsset: { id: string; preview: string; };
    selectedVariant: GetProductDetail.Variants;
    qty = 1;
    breadcrumbs: GetProductDetail.Breadcrumbs[] | null = null;

    @ViewChild('addedToCartTemplate', {static: true})
    private addToCartTemplate: TemplateRef<any>;
    private sub: Subscription;

    constructor(private dataService: DataService,
                private stateService: StateService,
                private notificationService: NotificationService,
                private route: ActivatedRoute,
                private titleService: Title, 
                private metaService: Meta) {
    }

    ngOnInit() {
        const lastCollectionSlug$ = this.stateService.select(state => state.lastCollectionSlug);
        const productSlug$ = this.route.paramMap.pipe(
            map(paramMap => paramMap.get('slug')),
            filter(notNullOrUndefined),
        );


        this.sub = productSlug$.pipe(
            switchMap(slug => {
                return this.dataService.query<GetProductDetail.Query, GetProductDetail.Variables>(GET_PRODUCT_DETAIL, {
                        slug,
                    },
                );
            }),
            map(data => data.product),
            filter(notNullOrUndefined),
            withLatestFrom(lastCollectionSlug$),
        ).subscribe(([product, lastCollectionSlug]) => {

            this.product = product;
            // trying to assign customFields here throws error
            let customFieldsData = this.product.customFields;

            if (this.product.featuredAsset) {
                this.selectedAsset = this.product.featuredAsset;
            }
            
            // return this.product.customFields.meta_title/; 
            // if(this.product.customFields){
            //     console.log('product', this.product.customFields.meta_title);
            // }
            // this.titleService.setTitle(custom.metaTitle);

            // this.metaService.addTags([
            //   {name: 'keywords', content: custom.metaKeywords},
            //   {name: 'description', content: custom.metaDescription},
            //   {name: 'robots', content: 'index, follow'}
            // ]);

            this.selectedVariant = product.variants[0];
            const collection = this.getMostRelevantCollection(product.collections, lastCollectionSlug);
            this.breadcrumbs = collection ? collection.breadcrumbs : [];
        });
    }

    ngOnDestroy() {
        if (this.sub) {
            this.sub.unsubscribe();
        }
    }

    addToCart(variant: GetProductDetail.Variants, qty: number) {
        this.dataService.mutate<AddToCart.Mutation, AddToCart.Variables>(ADD_TO_CART, {
            variantId: variant.id,
            qty,
        }).subscribe(({addItemToOrder}) => {
            switch (addItemToOrder.__typename) {
                case 'Order':
                    this.stateService.setState('activeOrderId', addItemToOrder ? addItemToOrder.id : null);
                    if (variant) {
                        this.notificationService.notify({
                            title: 'Added to cart',
                            type: 'info',
                            duration: 3000,
                            templateRef: this.addToCartTemplate,
                            templateContext: {
                                variant,
                                quantity: qty,
                            },
                        }).subscribe();
                    }
                    break;
                case 'OrderModificationError':
                case 'OrderLimitError':
                case 'NegativeQuantityError':
                case 'InsufficientStockError':
                    this.notificationService.error(addItemToOrder.message).subscribe();
                    break;
            }

        });
    }

    viewCartFromNotification(closeFn: () => void) {
        this.stateService.setState('cartDrawerOpen', true);
        closeFn();
    }

    /**
     * If there is a collection matching the `lastCollectionId`, return that. Otherwise return the collection
     * with the longest `breadcrumbs` array, which corresponds to the most specific collection.
     */
    private getMostRelevantCollection(collections: GetProductDetail.Collections[], lastCollectionSlug: string | null) {
        const lastCollection = collections.find(c => c.slug === lastCollectionSlug);
        if (lastCollection) {
            return lastCollection;
        }
        return collections.slice().sort((a, b) => {
            if (a.breadcrumbs.length < b.breadcrumbs.length) {
                return 1;
            }
            if (a.breadcrumbs.length > b.breadcrumbs.length) {
                return -1;
            }
            return 0;
        })[0];
    }

}

`
I have also added the customFields to the graphql query in product-detail.graphql.ts


export const GET_PRODUCT_DETAIL = gql`
    query GetProductDetail($slug: String!) {
        product(slug: $slug) {
            id
            name
            description
            variants {
                id
                name
                options {
                    code
                    name
                }
                price
                priceWithTax
                sku
            }
            featuredAsset {
                ...Asset
            }
            assets {
                ...Asset
            }
            collections {
                id
                slug
                breadcrumbs {
                    id
                    name
                    slug
                }
            }
            customFields {
                metaTitle
                metaDescription
                metaKeywords
            }
        }
    }
    ${ASSET_FRAGMENT}
`;

However it keeps throwing the error:
error TS2339: Property 'customFields' does not exist on type '{ __typename?: "Product" | undefined; } & Pick<Product, "id" | "name" | "description"> & { variants: ({ __typename?: "ProductVariant" | undefined; } & Pick<ProductVariant, "id" | ... 3 more ... | "sku"> & { ...; })[]; featuredAsset?: ({ ...; } & ... 2 more ... & { ...; }) | undefined; assets: ({ ...; } & ... 2 more ...'.

I have referred to the docs on the customFields, i have tried adding

declare module '@vendure/core' {
  interface CustomProductFields {
    metaTitle: string;
    metaDescription: string;
    metaKeywords: string;
  }
}

to vendure-config.ts file before the export. Maybe this is incorrect, i'm not to sure as i said i'm rather new and trying to get my head around this. It feels like i need to add something else to my angular frontend config to get it to work, or define something, but i'm not sure.

The full vendure config, only thing different from the original github version is the custom fields,

import {
    dummyPaymentHandler,
    DefaultJobQueuePlugin,
    DefaultSearchPlugin,
    VendureConfig,
    LanguageCode
} from '@vendure/core'; 


import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
import { AssetServerPlugin } from '@vendure/asset-server-plugin';
import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
import path from 'path';

export const config: VendureConfig = {
    apiOptions: {
        port: 3000,
        adminApiPath: 'admin-api',
        adminApiPlayground: {
            settings: {
                'request.credentials': 'include',
            } as any,
        },// turn this off for production
        adminApiDebug: true, // turn this off for production
        shopApiPath: 'shop-api',
        shopApiPlayground: {
            settings: {
                'request.credentials': 'include',
            } as any,
        },// turn this off for production
        shopApiDebug: true,// turn this off for production
    },
    authOptions: {
        superadminCredentials: {
            identifier: 'superadmin',
            password: 'superadmin',
        },
    },
    dbConnectionOptions: {
        type: 'mysql',
        synchronize: true, // turn this off for production
        logging: false,
        database: 'vendure',
        host: 'localhost',
        port: 3306,
        username: 'root',
        password: '',
        migrations: [path.join(__dirname, '../migrations/*.ts')],
    },
    paymentOptions: {
        paymentMethodHandlers: [dummyPaymentHandler],
    },
    customFields: {
        Product: [
          { 
              name: 'metaTitle', 
              type: 'string', 
              label: [ { languageCode: LanguageCode.en, value: 'Meta Title' } ] 
          },
          { 
              name: 'metaDescription', 
              type: 'string', 
              label: [ { languageCode: LanguageCode.en, value: 'Meta Description' } ] 
          },
          { 
              name: 'metaKeywords', 
              type: 'string', 
              label: [ { languageCode: LanguageCode.en, value: 'Meta Keywords' } ] 
          }
        ]
    },
    plugins: [
        AssetServerPlugin.init({
            route: 'assets',
            assetUploadDir: path.join(__dirname, '../static/assets'),
        }),
        DefaultJobQueuePlugin,
        DefaultSearchPlugin,
        EmailPlugin.init({
            devMode: true,
            outputPath: path.join(__dirname, '../static/email/test-emails'),
            route: 'mailbox',
            handlers: defaultEmailHandlers,
            templatePath: path.join(__dirname, '../static/email/templates'),
            globalTemplateVars: {
                // The following variables will change depending on your storefront implementation
                fromAddress: '"example" <noreply@example.com>',
                verifyEmailAddressUrl: 'http://localhost:8080/verify',
                passwordResetUrl: 'http://localhost:8080/password-reset',
                changeEmailAddressUrl: 'http://localhost:8080/verify-email-address-change'
            },
        }),
        AdminUiPlugin.init({
            route: 'admin',
            port: 3002,
        }),
    ],
};

any help would be appreciated, using mac Mojave, node v14.17.0 if that makes any difference.

@michaelbromley
Copy link
Member

Hi,

There are 2 aspects to the typings of the custom fields:

  1. The typings of the server-side entities. This is covered by the declare module '@vendure/core' { ... code above. This is only needed when you are developing plugins for your Vendure server and need to access custom fields in a type-safe way.
  2. The typings for the storefront. In this repo, these typings are generated by graphql-code-generator. There is some documentation about it here: https://github.com/vendure-ecommerce/storefront#code-generation

So based on what you wrote above, you are interested in number 2 above. Since you have already added the custom field field selection to your graphql document, you should be able to npm run generate-types and then have the generated typings automatically updated with your custom fields.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants