Skip to content

Commit

Permalink
Merge pull request #263 from Pinelab-studio/feat/primary-collection
Browse files Browse the repository at this point in the history
Select Primary Collection via Admin UI
  • Loading branch information
martijnvdbrug authored Sep 29, 2023
2 parents 32ccac7 + 6a6c3fe commit 5bc4d0c
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 59 deletions.
4 changes: 4 additions & 0 deletions packages/vendure-plugin-primary-collection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
# 1.0.1 (2023-09-22)

- Added index barrel file to export plugin ([#261](https://github.com/Pinelab-studio/pinelab-vendure-plugins/pull/261))

# 1.0.2 (2023-09-28)

- Products will have a `primaryCollection` as a custom field which can be selected via Admin UI.
14 changes: 12 additions & 2 deletions packages/vendure-plugin-primary-collection/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@

![Vendure version](https://img.shields.io/badge/dynamic/json.svg?url=https%3A%2F%2Fraw.githubusercontent.com%2FPinelab-studio%2Fpinelab-vendure-plugins%2Fmain%2Fpackage.json&query=$.devDependencies[%27@vendure/core%27]&colorB=blue&label=Built%20on%20Vendure)

To construct breadcrumbs and URL's it's useful to have a primary collection for each product, in case a product is part of multiple collections. This plugin extends Vendure's `Product` graphql type adding a `primaryCollection` field which points to the primary collection of a product, which is the the highest placed collection in Vendure (Collection's are sortable in Vendure, and it's a good practice to sort by importance).
To construct breadcrumbs and URL's it's useful to have a primary collection for each product, in case a product is part of multiple collections. This plugin extends Vendure's `Product` graphql type adding a `primaryCollection` field which points to the primary collection of a product, which can be selected in the product detail view, from a list of collections to which the product belongs.

## Getting started

Add the plugin to your `vendure-config.ts`:

```ts
plugins: [PrimaryCollectionPlugin];
plugins: [
PrimaryCollectionPlugin,
AdminUiPlugin.init({
port: 3002,
route: 'admin',
app: compileUiExtensions({
outputPath: path.join(__dirname, '__admin-ui'),
extensions: [PrimaryCollectionPlugin.ui],
}),
}),
];
```

And your good to go with just that.
2 changes: 1 addition & 1 deletion packages/vendure-plugin-primary-collection/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pinelab/vendure-plugin-primary-collection",
"version": "1.0.1",
"version": "1.1.0",
"description": "Adds a primary collection to all Products by extending vendure's graphql api",
"author": "Surafel Melese Tariku <[email protected]>",
"homepage": "https://pinelab-plugins.com/",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { Collection, Ctx, RequestContext, Product } from '@vendure/core';

@Resolver('Product')
export class PrimaryCollectionResolver {
@ResolveField()
async primaryCollection(
@Ctx() ctx: RequestContext,
@Parent() product: Product
): Promise<Collection | null> {
return (product.customFields as any).primaryCollection;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { PluginCommonModule, Type, VendurePlugin } from '@vendure/core';
import {
Collection,
LanguageCode,
PluginCommonModule,
RuntimeVendureConfig,
VendurePlugin,
} from '@vendure/core';
import { gql } from 'graphql-tag';
import { PrimaryCollectionResolver } from './primary-collection.resolver';
import { PrimaryCollectionResolver } from './api/primary-collection.resolver';
import { AdminUiExtension } from '@vendure/ui-devkit/compiler';
import path from 'path';
import '../types.ts';

@VendurePlugin({
imports: [PluginCommonModule],
Expand All @@ -13,5 +22,31 @@ import { PrimaryCollectionResolver } from './primary-collection.resolver';
resolvers: [PrimaryCollectionResolver],
},
compatibility: '^2.0.0',
configuration: (config: RuntimeVendureConfig) => {
config.customFields.Product.push({
name: 'primaryCollection',
type: 'relation',
entity: Collection,
graphQLType: 'Collection',
eager: true,
nullable: true,
ui: {
component: 'select-primary-collection',
},
label: [{ languageCode: LanguageCode.en, value: 'Primary Collection' }],
});
return config;
},
})
export class PrimaryCollectionPlugin {}
export class PrimaryCollectionPlugin {
static ui: AdminUiExtension = {
extensionPath: path.join(__dirname, 'ui'),
ngModules: [
{
type: 'shared',
ngModuleFileName: 'shared.module.ts',
ngModuleName: 'SharedExtensionModule',
},
],
};
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Component, ChangeDetectorRef } from '@angular/core';
import { FormControl } from '@angular/forms';
import { OnInit } from '@angular/core';
import {
IntCustomFieldConfig,
FormInputComponent,
CollectionFragment,
COLLECTION_FRAGMENT,
} from '@vendure/admin-ui/core';
import { DataService } from '@vendure/admin-ui/core';
import { gql } from 'graphql-tag';
import { ActivatedRoute, Params } from '@angular/router';
@Component({
template: `
<select
[formControl]="formControl"
[vdrDisabled]="readonly"
[compareWith]="compareFn"
>
<option *ngFor="let option of productsCollections" [ngValue]="option">
{{ option.name }}
</option>
</select>
`,
})
export class SelectPrimaryCollectionComponent
implements FormInputComponent<IntCustomFieldConfig>, OnInit
{
readonly!: boolean;
config!: IntCustomFieldConfig;
formControl!: FormControl;
productsCollections!: CollectionFragment[];
productsCollectionsAreLoading = true;
id!: string;
constructor(
private dataService: DataService,
private cdr: ChangeDetectorRef,
private activatedRoute: ActivatedRoute
) {}
ngOnInit(): void {
console.log(this.formControl.value, 'value');
this.formControl.parent?.parent?.statusChanges.subscribe((s) => {
if (
this.formControl.pristine &&
!this.formControl.value &&
(this.productsCollections.length || this.productsCollectionsAreLoading)
) {
this.formControl.parent?.parent?.markAsPristine();
}
});
this.activatedRoute.params.subscribe((params: Params) => {
this.id = params.id;
this.dataService.collection.getCollectionContents(params.id);
this.dataService
.query(
gql`
query ProductsCollection($id: ID) {
product(id: $id) {
collections {
...Collection
}
}
}
${COLLECTION_FRAGMENT}
`,
{ id: params.id }
)
.single$.subscribe((d: any) => {
this.productsCollections = d.product.collections;
this.productsCollectionsAreLoading = false;
this.cdr.markForCheck();
});
});
}

compareFn(a: any, b: any) {
return a?.id === b?.id;
}
}
18 changes: 18 additions & 0 deletions packages/vendure-plugin-primary-collection/src/ui/shared.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import {
SharedModule,
registerFormInputComponent,
} from '@vendure/admin-ui/core';
import { SelectPrimaryCollectionComponent } from './select-primary-collection.component';

@NgModule({
imports: [SharedModule],
declarations: [SelectPrimaryCollectionComponent],
providers: [
registerFormInputComponent(
'select-primary-collection',
SelectPrimaryCollectionComponent
),
],
})
export class SharedExtensionModule {}
7 changes: 7 additions & 0 deletions packages/vendure-plugin-primary-collection/test/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
} from '@vendure/testing';
import { initialTestData } from './initial-test-data';
import { PrimaryCollectionPlugin } from '../src/primary-collection-plugin';
import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
import path from 'path';

require('dotenv').config();

Expand All @@ -26,6 +28,11 @@ require('dotenv').config();
AdminUiPlugin.init({
port: 3002,
route: 'admin',
app: compileUiExtensions({
outputPath: path.join(__dirname, '__admin-ui'),
extensions: [PrimaryCollectionPlugin.ui],
devMode: true,
}),
}),
],
apiOptions: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('Product Primary Collection', function () {
customerCount: 2,
});
serverStarted = true;
await adminClient.asSuperAdmin();
}, 60000);

it('Should start successfully', async () => {
Expand All @@ -65,30 +66,47 @@ describe('Product Primary Collection', function () {
}
`;

it("Should return 'Computers' as a primary collection for 'Laptop'", async () => {
const { product } = await shopClient.query(primaryCollectionQuery, {
productId: 'T_1',
});
const updatePrimaryCollectionMutation = gql`
mutation UpdateProuductPrimaryCollection(
$productId: ID!
$productPrimaryCollectionId: ID
) {
updateProduct(
input: {
id: $productId
customFields: { primaryCollectionId: $productPrimaryCollectionId }
}
) {
name
id
customFields {
primaryCollection {
name
id
}
}
}
}
`;

it("Should successfully update 'Laptop's primaryCollection as 'Electronics'", async () => {
const { updateProduct: product } = await adminClient.query(
updatePrimaryCollectionMutation,
{ productId: 'T_1', productPrimaryCollectionId: 'T_3' }
);
expect(product.name).toBe('Laptop');
expect(product.primaryCollection.name).toBe('Computers');
expect(product.id).toBe('T_1');
expect(product.customFields.primaryCollection.name).toBe('Electronics');
});

it("Should return 'Electronics' as a primary collection for 'Cars'", async () => {
it("Should return 'Electronics' as a primary collection for 'Laptop' in Shop API", async () => {
const { product } = await shopClient.query(primaryCollectionQuery, {
productId: 'T_2',
productId: 'T_1',
});
expect(product.name).toBe('Cars');
expect(product.name).toBe('Laptop');
expect(product.primaryCollection.name).toBe('Electronics');
});

it("Should return 'Others' as a primary collection for 'Motors'", async () => {
const { product } = await shopClient.query(primaryCollectionQuery, {
productId: 'T_3',
});
expect(product.name).toBe('Motors');
expect(product.primaryCollection.name).toBe('Others');
});

afterAll(async () => {
return server.destroy();
});
Expand Down
7 changes: 7 additions & 0 deletions packages/vendure-plugin-primary-collection/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CustomProductFields } from '@vendure/core/dist/entity/custom-entity-fields';
import { Collection } from '@vendure/core';
declare module '@vendure/core/dist/entity/custom-entity-fields' {
interface CustomProductFields {
primaryCollection: Collection;
}
}

0 comments on commit 5bc4d0c

Please sign in to comment.