diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..e72141f2b3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,22 @@ +# Description + +Please include a summary of the changes and the related issue. + +# Breaking changes + +Does this PR include any breaking changes we should be aware of? + +# Screenshots + +You can add screenshots here if applicable. + +# Checklist + +:pushpin: Always: +- [ ] I have set a clear title +- [ ] My PR is small and contains a single feature +- [ ] I have [checked my own PR](## "Fix typo's and remove unused or commented out code") + +:zap: Most of the time: +- [ ] I have added or updated test cases +- [ ] I have updated the README if needed \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 91acedbcf5..b869b886cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## 2.1.1 (2023-10-18) + + +#### Fixes + +* **admin-ui** Add missing RTL compatibility to some admin-ui components (#2451) ([ec61b58](https://github.com/vendure-ecommerce/vendure/commit/ec61b58)), closes [#2451](https://github.com/vendure-ecommerce/vendure/issues/2451) +* **admin-ui** Add unique location id for prod detail variants table ([ce2b251](https://github.com/vendure-ecommerce/vendure/commit/ce2b251)) +* **admin-ui** Do not load pending search index updates if permissions are insufficient (#2460) ([08ad982](https://github.com/vendure-ecommerce/vendure/commit/08ad982)), closes [#2460](https://github.com/vendure-ecommerce/vendure/issues/2460) [#2456](https://github.com/vendure-ecommerce/vendure/issues/2456) +* **admin-ui** Fix customer group select input ([02fe6ae](https://github.com/vendure-ecommerce/vendure/commit/02fe6ae)), closes [#2441](https://github.com/vendure-ecommerce/vendure/issues/2441) +* **admin-ui** Fix initial render of code editor input marking dirty ([9dda349](https://github.com/vendure-ecommerce/vendure/commit/9dda349)) +* **admin-ui** Fix setting facet values on new product ([9d88db2](https://github.com/vendure-ecommerce/vendure/commit/9d88db2)), closes [#2355](https://github.com/vendure-ecommerce/vendure/issues/2355) +* **admin-ui** Improve Italian translations (#2445) ([3fd93c7](https://github.com/vendure-ecommerce/vendure/commit/3fd93c7)), closes [#2445](https://github.com/vendure-ecommerce/vendure/issues/2445) +* **admin-ui** Improvements to Nepali translation (#2463) ([4035fda](https://github.com/vendure-ecommerce/vendure/commit/4035fda)), closes [#2463](https://github.com/vendure-ecommerce/vendure/issues/2463) +* **admin-ui** Make ExtensionHostComponent work with new extension APIs ([b917e62](https://github.com/vendure-ecommerce/vendure/commit/b917e62)) +* **admin-ui** Make utility margin/padding classes RTL-compatible ([74c6634](https://github.com/vendure-ecommerce/vendure/commit/74c6634)) +* **common** Remove trademark symbols from normalized strings (#2447) ([9aac191](https://github.com/vendure-ecommerce/vendure/commit/9aac191)), closes [#2447](https://github.com/vendure-ecommerce/vendure/issues/2447) +* **core** Fix custom field resolver for eager translatable relation (#2457) ([09dd7df](https://github.com/vendure-ecommerce/vendure/commit/09dd7df)), closes [#2453](https://github.com/vendure-ecommerce/vendure/issues/2453) +* **core** Fix regression in ProductService.findOne not using relations ([92cad43](https://github.com/vendure-ecommerce/vendure/commit/92cad43)), closes [#2443](https://github.com/vendure-ecommerce/vendure/issues/2443) +* **core** Normalize email address on updating Customer ([957d0ad](https://github.com/vendure-ecommerce/vendure/commit/957d0ad)), closes [#2449](https://github.com/vendure-ecommerce/vendure/issues/2449) +* **payments-plugin** Fix Mollie klarna AutoCapture (#2446) ([8db459a](https://github.com/vendure-ecommerce/vendure/commit/8db459a)), closes [#2446](https://github.com/vendure-ecommerce/vendure/issues/2446) +* **payments-plugin** Fix Stripe controller crashing server instance (#2454) ([b0ece21](https://github.com/vendure-ecommerce/vendure/commit/b0ece21)), closes [#2454](https://github.com/vendure-ecommerce/vendure/issues/2454) [#2450](https://github.com/vendure-ecommerce/vendure/issues/2450) +* **payments-plugin** Idempotent 'paid' Mollie webhooks (#2462) ([2f7a8d5](https://github.com/vendure-ecommerce/vendure/commit/2f7a8d5)), closes [#2462](https://github.com/vendure-ecommerce/vendure/issues/2462) + +#### Features + +* **admin-ui** Add Croatian translation (#2442) ([b594c55](https://github.com/vendure-ecommerce/vendure/commit/b594c55)), closes [#2442](https://github.com/vendure-ecommerce/vendure/issues/2442) +* **admin-ui** Add product slug in product multi selector dialog component (#2461) ([b7f3452](https://github.com/vendure-ecommerce/vendure/commit/b7f3452)), closes [#2461](https://github.com/vendure-ecommerce/vendure/issues/2461) + ## 2.1.0 (2023-10-11) @@ -82,6 +110,8 @@ https://www.apollographql.com/docs/apollo-server/migration/ * The ForbiddenError now defaults to a "warning" rather than "error" log level. Previously this was causing too much noise in logging services and the new level better reflects the severity of the error. +* If after update you are running into the error `[GraphQL error]: Message: POST body missing, invalid Content-Type, or JSON object has no keys.`, this may be due to having the [body-parser](https://www.npmjs.com/package/body-parser) `json` middleware configured in your app. You should be able to safely remove this middleware in order to resolve the issue. + ## 2.0.10 (2023-10-11) diff --git a/docs/docs/guides/core-concepts/orders/index.md b/docs/docs/guides/core-concepts/orders/index.md index b999b47b5a..68d9222a18 100644 --- a/docs/docs/guides/core-concepts/orders/index.md +++ b/docs/docs/guides/core-concepts/orders/index.md @@ -117,6 +117,20 @@ const myCustomOrderProcess = configureDefaultOrderProcess({ // Orders to have a shipping method assigned // before payment. arrangingPaymentRequiresShipping: false, + + // Other constraints which can be disabled. See the + // DefaultOrderProcessOptions interface docs for full + // explanations. + // + // checkModificationPayments: false, + // checkAdditionalPaymentsAmount: false, + // checkAllVariantsExist: false, + // arrangingPaymentRequiresContents: false, + // arrangingPaymentRequiresCustomer: false, + // arrangingPaymentRequiresStock: false, + // checkPaymentsCoverTotal: false, + // checkAllItemsBeforeCancel: false, + // checkFulfillmentStates: false, }); export const config: VendureConfig = { @@ -241,6 +255,81 @@ const customerValidationProcess: OrderProcess<'ValidatingCustomer'> = { For an explanation of the `init()` method and `injector` argument, see the guide on [injecting dependencies in configurable operations](/guides/developer-guide/strategies-configurable-operations/#injecting-dependencies). ::: +### Responding to a state transition + +Once an order has successfully transitioned to a new state, the [`onTransitionEnd` state transition hook](/reference/typescript-api/state-machine/state-machine-config#ontransitionend) is called. This can be used to perform some action +upon successful state transition. + +In this example, we have a referral service which creates a new referral for a customer when they complete an order. We want to create the referral only if the customer has a referral code associated with their account. + +```ts +import { OrderProcess, OrderState } from '@vendure/core'; + +import { ReferralService } from '../service/referral.service'; + +let referralService: ReferralService; + +export const referralOrderProcess: OrderProcess = { + init: (injector) => { + referralService = injector.get(ReferralService); + }, + onTransitionEnd: async (fromState, toState, data) => { + const { order, ctx } = data; + if (toState === 'PaymentSettled') { + if (order.customFields.referralCode) { + await referralService.createReferralForOrder(ctx, order); + } + } + }, +}; +``` + +:::caution +Use caution when modifying an order inside the `onTransitionEnd` function. The `order` object that gets passed in to this function +will later be persisted to the database. Therefore any changes must be made to that `order` object, otherwise the changes might be lost. + +As an example, let's say we want to add a Surcharge to the order. The following code **will not work as expected**: + +```ts +export const myOrderProcess: OrderProcess = { + async onTransitionEnd(fromState, toState, data) { + if (fromState === 'AddingItems' && toState === 'ArrangingPayment') { + // highlight-start + // WARNING: This will not work! + await orderService.addSurchargeToOrder(ctx, order.id, { + description: 'Test', + listPrice: 42, + listPriceIncludesTax: false, + }); + // highlight-end + } + } +}; +``` + +Instead, you need to ensure you **mutate the `order` object**: + +```ts +export const myOrderProcess: OrderProcess = { + async onTransitionEnd(fromState, toState, data) { + if (fromState === 'AddingItems' && toState === 'ArrangingPayment') { + // highlight-start + const {surcharges} = await orderService.addSurchargeToOrder(ctx, order.id, { + description: 'Test', + listPrice: 42, + listPriceIncludesTax: false, + }); + // Important: mutate the order object + order.surcharges = surcharges; + // highlight-end + } + }, +} +``` + + +::: + ## TypeScript Typings To make your custom states compatible with standard services you should declare your new states in the following way: diff --git a/docs/docs/guides/developer-guide/custom-fields/index.md b/docs/docs/guides/developer-guide/custom-fields/index.md index 9241b16f69..8d69678353 100644 --- a/docs/docs/guides/developer-guide/custom-fields/index.md +++ b/docs/docs/guides/developer-guide/custom-fields/index.md @@ -5,6 +5,7 @@ sidebar_position: 3 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import CustomFieldProperty from '@site/src/components/CustomFieldProperty'; Custom fields allow you to add your own custom data properties almost every Vendure entity. The entities which may have custom fields defined are listed in the [CustomFields interface documentation](/reference/typescript-api/custom-fields/). @@ -192,6 +193,68 @@ mutation { } ``` +## Accessing custom fields in TypeScript + +As well as exposing custom fields via the GraphQL APIs, you can also access them directly in your TypeScript code. This is useful for plugins which need to access custom field data. + +Given the following custom field configuration: + +```ts title="src/vendure-config.ts" +import { VendureConfig } from '@vendure/core'; + +const config: VendureConfig = { + // ... + customFields: { + Customer: [ + { name: 'externalId', type: 'string' }, + { name: 'avatar', type: 'relation', entity: Asset }, + ], + }, +}; +``` + +the `externalId` will be available whenever you access a `Customer` entity: + +```ts +const customer = await this.connection.getRepository(ctx, Customer).findOne({ + where: { id: 1 }, +}); +console.log(customer.externalId); +``` + +The `avatar` relation will require an explicit join to be performed in order to access the data, since it is not +eagerly loaded by default: + +```ts +const customer = await this.connection.getRepository(ctx, Customer).findOne({ + where: { id: 1 }, + relations: { + customFields: { + avatar: true, + } + } +}); +console.log(customer.avatar); +``` + +or if using the QueryBuilder API: + +```ts +const customer = await this.connection.getRepository(ctx, Customer).createQueryBuilder('customer') + .leftJoinAndSelect('customer.customFields.avatar', 'avatar') + .where('customer.id = :id', { id: 1 }) + .getOne(); +console.log(customer.avatar); +``` + +or using the EntityHydrator: + +```ts +const customer = await this.customerService.findOne(ctx, 1); +await this.entityHydrator.hydrate(ctx, customer, { relations: ['customFields.avatar'] }); +console.log(customer.avatar); +``` + ## Custom field config properties ### Common properties @@ -200,9 +263,7 @@ All custom fields share some common properties: #### name -Required - -`string` + The name of the field. This is used as the column name in the database, and as the GraphQL field name. The name should not contain spaces and by convention should be camelCased. @@ -223,17 +284,13 @@ const config = { #### type -Required - -[`CustomFieldType`](/reference/typescript-api/custom-fields/custom-field-type) + The type of data that will be stored in the field. #### list -Optional - -`boolean` + If set to `true`, then the field will be an array of the specified type. Defaults to `false`. @@ -261,9 +318,7 @@ Setting a custom field to be a list has the following effects: #### label -Optional - -[`LocalizedStringArray`](/reference/typescript-api/configurable-operation-def/localized-string-array) + An array of localized labels for the field. These are used in the Admin UI to label the field. @@ -292,9 +347,7 @@ const config = { #### description -Optional - -[`LocalizedStringArray`](/reference/typescript-api/configurable-operation-def/localized-string-array) + An array of localized descriptions for the field. These are used in the Admin UI to describe the field. @@ -323,9 +376,7 @@ const config = { #### public -Optional - -`boolean` + Whether the custom field is available via the Shop API. Defaults to `true`. @@ -347,9 +398,7 @@ const config = { #### readonly -Optional - -`boolean` + Whether the custom field can be updated via the GraphQL APIs. Defaults to `false`. If set to `true`, then the field can only be updated via direct manipulation via TypeScript code in a plugin. @@ -372,9 +421,7 @@ const config = { #### internal -Optional - -`boolean` + Whether the custom field is exposed at all via the GraphQL APIs. Defaults to `false`. If set to `true`, then the field will not be available via the GraphQL API, but can still be used in TypeScript code in a plugin. Internal fields are useful for storing data which is not intended @@ -398,9 +445,7 @@ const config = { #### defaultValue -Optional - -`any` + The default value when an Entity is created with this field. If not provided, then the default value will be `null`. Note that if you set `nullable: false`, then you should also provide a `defaultValue` to avoid database errors when creating new entities. @@ -423,9 +468,7 @@ const config = { #### nullable -Optional - -`boolean` + Whether the field is nullable in the database. If set to `false`, then a `defaultValue` should be provided. @@ -449,9 +492,7 @@ const config = { #### unique -Optional - -`boolean` + Whether the value of the field should be unique. When set to `true`, a UNIQUE constraint is added to the column. Defaults to `false`. @@ -474,9 +515,7 @@ const config = { #### validate -Optional - -`(value: any) => string | LocalizedString[] | void` + A custom validation function. If the value is valid, then the function should not return a value. If a string or LocalizedString array is returned, this is interpreted as an error message. @@ -544,9 +583,7 @@ In addition to the common properties, the `string` custom fields have some type- #### pattern -Optional - -`string` + A regex pattern which the field value must match. If the value does not match the pattern, then the validation will fail. @@ -568,9 +605,7 @@ const config = { #### options -Optional - -`{ value: string; label?: LocalizedString[]; }[]` + An array of pre-defined options for the field. This is useful for fields which should only have a limited set of values. The `value` property is the value which will be stored in the database, and the `label` property is an optional array of localized strings which will be displayed in the admin UI. @@ -600,9 +635,7 @@ Attempting to set the value of the field to a value which is not in the `options #### length -Optional - -`number` + The max length of the varchar created in the database. Defaults to 255. Maximum is 65,535. @@ -628,17 +661,13 @@ In addition to the common properties, the `localeString` custom fields have some #### pattern -Optional - -`string` + Same as the `pattern` property for `string` fields. #### length -Optional - -`number` + Same as the `length` property for `string` fields. @@ -648,9 +677,7 @@ In addition to the common properties, the `int` & `float` custom fields have som #### min -Optional - -`number` + The minimum permitted value. If the value is less than this, then the validation will fail. @@ -672,9 +699,7 @@ const config = { #### max -Optional - -`number` + The maximum permitted value. If the value is greater than this, then the validation will fail. @@ -696,9 +721,7 @@ const config = { #### step -Optional - -`number` + The step value. This is used in the Admin UI to determine the increment/decrement value of the input field. @@ -726,9 +749,7 @@ The min, max & step properties for datetime fields are intended to be used as de #### min -Optional - -`string` + The earliest permitted date. If the value is earlier than this, then the validation will fail. @@ -750,9 +771,7 @@ const config = { #### max -Optional - -`string` + The latest permitted date. If the value is later than this, then the validation will fail. @@ -774,9 +793,7 @@ const config = { #### step -Optional - -`string` + The step value. See [the MDN datetime-local docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local#step) to understand how this is used. @@ -786,9 +803,8 @@ In addition to the common properties, the `relation` custom fields have some typ #### entity -Required + -`VendureEntity` The entity which this custom field is referencing. This can be one of the built-in entities, or a custom entity. If the entity is a custom entity, it must extend the `VendureEntity` class. @@ -814,9 +830,7 @@ const config = { #### eager -Optional - -`boolean` + Whether to [eagerly load](https://typeorm.io/#/eager-and-lazy-relations) the relation. Defaults to false. Note that eager loading has performance implications, so should only be used when necessary. @@ -842,9 +856,7 @@ const config = { #### graphQLType -Optional - -`string` + The name of the GraphQL type that corresponds to the entity. Can be omitted if the GraphQL type name is the same as the entity name, which is the case for all of the built-in entities. @@ -870,11 +882,9 @@ const config = { In the above example, the `CmsArticle` entity is being used as a related entity. However, the GraphQL type name is `BlogPost`, so we must specify this in the `graphQLType` property, otherwise Vendure will try to extend the GraphQL schema with reference to a non-existent "CmsArticle" type. -### `inverseSide` - -Optional +#### inverseSide -`string | ((object: VendureEntity) => any);` + Allows you to specify the [inverse side of the relation](https://typeorm.io/#inverse-side-of-the-relationship). Let's say you are adding a relation from `Product` to a custom entity which refers back to the product. You can specify this inverse relation like so: diff --git a/docs/docs/guides/developer-guide/overview/index.md b/docs/docs/guides/developer-guide/overview/index.md index 2fff5d6d84..c998cffbb5 100644 --- a/docs/docs/guides/developer-guide/overview/index.md +++ b/docs/docs/guides/developer-guide/overview/index.md @@ -24,7 +24,7 @@ Vendure is built on the following open-source technologies: - **SQL Database**: Vendure requires an SQL database compatible with [TypeORM](https://typeorm.io/). Officially we support **PostgreSQL**, **MySQL/MariaDB** and **SQLite** but Vendure can also be used with API-compatible variants such [Amazon Aurora](https://aws.amazon.com/rds/aurora/), [CockroachDB](https://www.cockroachlabs.com/), or [PlanetScale](https://planetscale.com/). - **TypeScript & Node.js**: Vendure is written in [TypeScript](https://www.typescriptlang.org/) and runs on [Node.js](https://nodejs.org). -- **NestJS**: The underlying framework is [NestJS](https://nestjs.com/), which is a full-featured application development framework for Node.js. Building on NestJS means that Vendure from the well-defined structure and rich feature-set and ecosystem that NestJS provides. +- **NestJS**: The underlying framework is [NestJS](https://nestjs.com/), which is a full-featured application development framework for Node.js. Building on NestJS means that Vendure benefits from the well-defined structure and rich feature-set and ecosystem that NestJS provides. - **GraphQL**: The Shop and Admin APIs use [GraphQL](https://graphql.org/), which is a modern API technology which allows you to specify the exact data that your client application needs in a convenient and type-safe way. Internally we use [Apollo Server](https://www.apollographql.com/docs/apollo-server/) to power our GraphQL APIs. - **Angular**: The Admin UI is built with [Angular](https://angular.io/), a popular, stable application framework from Google. Note that you do not need to know Angular to use Vendure, and UI extensions can even be written in the front-end framework of your choice, such as React or Vue. diff --git a/docs/docs/guides/developer-guide/rest-endpoint/index.md b/docs/docs/guides/developer-guide/rest-endpoint/index.md index 2c8dd12e8d..32f033baba 100644 --- a/docs/docs/guides/developer-guide/rest-endpoint/index.md +++ b/docs/docs/guides/developer-guide/rest-endpoint/index.md @@ -9,7 +9,7 @@ REST-style endpoints can be defined as part of a [plugin](/guides/developer-guid REST endpoints are implemented as NestJS Controllers. For comprehensive documentation, see the [NestJS controllers documentation](https://docs.nestjs.com/controllers). ::: -In this guide we will define a plugin adds a single REST endpoint at `http://localhost:3000/products` which returns a list of all products. +In this guide we will define a plugin that adds a single REST endpoint at `http://localhost:3000/products` which returns a list of all products. ## Create a controller diff --git a/docs/docs/guides/developer-guide/testing/index.md b/docs/docs/guides/developer-guide/testing/index.md index 40c7d3b994..a07aef1b1e 100644 --- a/docs/docs/guides/developer-guide/testing/index.md +++ b/docs/docs/guides/developer-guide/testing/index.md @@ -210,3 +210,38 @@ describe('my plugin', () => { }); ``` ::: + +## Accessing internal services + +It is possible to access any internal service of the Vendure server via the `server.app` object, which is an instance of the NestJS `INestApplication`. + +For example, to access the `ProductService`: + +```ts title="src/plugins/my-plugin/e2e/my-plugin.e2e-spec.ts" +import { createTestEnvironment, testConfig } from '@vendure/testing'; +import { describe, beforeAll } from 'vitest'; +import { MyPlugin } from '../my-plugin.ts'; + +describe('my plugin', () => { + + const { server, adminClient, shopClient } = createTestEnvironment({ + ...testConfig, + plugins: [MyPlugin], + }); + + // highlight-next-line + let productService: ProductService; + + beforeAll(async () => { + await server.init({ + productsCsvPath: path.join(__dirname, 'fixtures/e2e-products.csv'), + initialData: myInitialData, + customerCount: 2, + }); + await adminClient.asSuperAdmin(); + // highlight-next-line + productService = server.app.get(ProductService); + }, 60000); + +}); +``` diff --git a/docs/docs/guides/developer-guide/the-service-layer/index.mdx b/docs/docs/guides/developer-guide/the-service-layer/index.mdx index 5c96bd3809..2166d16bbe 100644 --- a/docs/docs/guides/developer-guide/the-service-layer/index.mdx +++ b/docs/docs/guides/developer-guide/the-service-layer/index.mdx @@ -229,3 +229,83 @@ export class ItemService { Further examples can be found in the [TypeORM QueryBuilder documentation](https://typeorm.io/select-query-builder). ::: + +### Working with relations + +One limitation of TypeORM's typings is that we have no way of knowing at build-time whether a particular relation will be +joined at runtime. For instance, the following code will build without issues, but will result in a runtime error: + +```ts +const product = await this.connection.getRepository(ctx, Product).findOne({ + where: { id: productId }, +}); +if (product) { + // highlight-start + console.log(product.featuredAsset.preview); + // ^ Error: Cannot read property 'preview' of undefined + // highlight-end +} +``` + +This is because the `featuredAsset` relation is not joined by default. The simple fix for the above example is to use +the `relations` option: + +```ts +const product = await this.connection.getRepository(ctx, Product).findOne({ + where: { id: productId }, + // highlight-next-line + relations: { featuredAsset: true }, +}); +``` +or in the case of the QueryBuilder API, we can use the `leftJoinAndSelect()` method: + +```ts +const product = await this.connection.getRepository(ctx, Product).createQueryBuilder('product') + // highlight-next-line + .leftJoinAndSelect('product.featuredAsset', 'featuredAsset') + .where('product.id = :id', { id: productId }) + .getOne(); +``` + +### Using the EntityHydrator + +But what about when we do not control the code which fetches the entity from the database? For instance, we might be implementing +a function which gets an entity passed to it by Vendure. In this case, we can use the [`EntityHydrator`](/reference/typescript-api/data-access/entity-hydrator/) +to ensure that a given relation is "hydrated" (i.e. joined) before we use it: + +```ts +import { EntityHydrator, ShippingCalculator } from '@vendure/core'; + +let entityHydrator: EntityHydrator; + +const myShippingCalculator = new ShippingCalculator({ + // ... rest of config omitted for brevity + init(injector) { + entityHydrator = injector.get(EntityHydrator); + }, + calculate: (ctx, order, args) => { + // highlight-start + // ensure that the customer and customer.groups relations are joined + await entityHydrator.hydrate(ctx, order, { relations: ['customer.groups' ]}); + // highlight-end + + if (order.customer?.groups?.some(g => g.name === 'VIP')) { + // ... do something special for VIP customers + } else { + // ... do something else + } + }, +}); +``` + +### Joining relations in built-in service methods + +Many of the core services allow an optional `relations` argument in their `findOne()` and `findMany()` and related methods. +This allows you to specify which relations should be joined when the query is executed. For instance, in the [`ProductService`](/reference/typescript-api/services/product-service) +there is a `findOne()` method which allows you to specify which relations should be joined: + +```ts +const productWithAssets = await this.productService + .findOne(ctx, productId, ['featuredAsset', 'assets']); +``` + diff --git a/docs/docs/guides/extending-the-admin-ui/getting-started/index.md b/docs/docs/guides/extending-the-admin-ui/getting-started/index.md index ee58610e14..6974a33fa2 100644 --- a/docs/docs/guides/extending-the-admin-ui/getting-started/index.md +++ b/docs/docs/guides/extending-the-admin-ui/getting-started/index.md @@ -31,7 +31,7 @@ npm install --save-dev @vendure/ui-devkit ```bash -yarn add --save-dev @vendure/ui-devkit +yarn add --dev @vendure/ui-devkit ``` diff --git a/docs/docs/guides/extending-the-admin-ui/nav-menu/index.md b/docs/docs/guides/extending-the-admin-ui/nav-menu/index.md index afd11c9331..2d76e8ee78 100644 --- a/docs/docs/guides/extending-the-admin-ui/nav-menu/index.md +++ b/docs/docs/guides/extending-the-admin-ui/nav-menu/index.md @@ -71,4 +71,27 @@ Running the server will compile our new shared module into the app, and the resu It is also possible to override one of the default (built-in) nav menu sections or items. This can be useful for example if you wish to provide a completely different implementation of the product list view. -This is done by setting the `id` property to that of an existing nav menu section or item. +This is done by setting the `id` property to that of an existing nav menu section or item. The `id` can be found by inspecting the link element in your browser's dev tools for the `data-item-id` attribute: + +![Navbar menu id](./nav-menu-id.webp) + +## Removing existing nav items + +If you would like to remove an existing nav item, you can do so by overriding it and setting the `requiresPermission` property to an invalid value: + +```ts title="src/plugins/greeter/ui/providers.ts" +import { SharedModule, addNavMenuItem} from '@vendure/admin-ui/core'; + +export default [ + addNavMenuItem({ + id: 'collections', // <-- we will override the "collections" menu item + label: 'Collections', + routerLink: ['/catalog', 'collections'], + // highlight-start + // we use an invalid permission which ensures it is hidden from all users + requiresPermission: '__disable__' + // highlight-end + }, + 'catalog'), +]; +``` diff --git a/docs/docs/guides/extending-the-admin-ui/nav-menu/nav-menu-id.webp b/docs/docs/guides/extending-the-admin-ui/nav-menu/nav-menu-id.webp new file mode 100644 index 0000000000..5dafda2a2f Binary files /dev/null and b/docs/docs/guides/extending-the-admin-ui/nav-menu/nav-menu-id.webp differ diff --git a/docs/docs/guides/storefront/checkout-flow/index.mdx b/docs/docs/guides/storefront/checkout-flow/index.mdx index 0005193f04..26c8a5c554 100644 --- a/docs/docs/guides/storefront/checkout-flow/index.mdx +++ b/docs/docs/guides/storefront/checkout-flow/index.mdx @@ -308,7 +308,7 @@ while the payment is being arranged. The [`transitionOrderToState`](/reference/g ```graphql -mutation TransitionToState($state: String!) +mutation TransitionToState($state: String!) { transitionOrderToState(state: $state) { ...ActiveOrder ...on OrderStateTransitionError { diff --git a/docs/docs/guides/storefront/connect-api/index.mdx b/docs/docs/guides/storefront/connect-api/index.mdx index 35799925ae..0aad8e05ff 100644 --- a/docs/docs/guides/storefront/connect-api/index.mdx +++ b/docs/docs/guides/storefront/connect-api/index.mdx @@ -536,7 +536,7 @@ const requestMiddleware: RequestMiddleware = async (request) => { // Check all responses for a new session token const responseMiddleware: ResponseMiddleware = (response) => { - if (!(response instanceof Error) && response.errors) { + if (!(response instanceof Error) && !response.errors) { const authHeader = response.headers.get('vendure-auth-token'); if (authHeader) { // If the session token has been returned by the Vendure diff --git a/docs/docs/reference/admin-ui-api/custom-input-components/default-inputs.md b/docs/docs/reference/admin-ui-api/custom-input-components/default-inputs.md index 3f06ec5014..43d825ee45 100644 --- a/docs/docs/reference/admin-ui-api/custom-input-components/default-inputs.md +++ b/docs/docs/reference/admin-ui-api/custom-input-components/default-inputs.md @@ -289,7 +289,7 @@ class CustomerGroupFormInputComponent implements FormInputComponent, OnInit { constructor(dataService: DataService) ngOnInit() => ; selectGroup(group: ItemOf) => ; - compareWith(o1: ItemOf, o2: ItemOf) => ; + compareWith(o1: T, o2: T) => ; } ``` * Implements: FormInputComponent, OnInit @@ -340,7 +340,7 @@ class CustomerGroupFormInputComponent implements FormInputComponent, OnInit { ### compareWith - `} /> + `} /> diff --git a/docs/docs/reference/admin-ui-api/custom-table-components/custom-column-component.md b/docs/docs/reference/admin-ui-api/custom-table-components/custom-column-component.md index 99aa70b88f..13af4351cb 100644 --- a/docs/docs/reference/admin-ui-api/custom-table-components/custom-column-component.md +++ b/docs/docs/reference/admin-ui-api/custom-table-components/custom-column-component.md @@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## CustomColumnComponent - + Components which are to be used to render custom cells in a data table should implement this interface. diff --git a/docs/docs/reference/admin-ui-api/custom-table-components/data-table-component-config.md b/docs/docs/reference/admin-ui-api/custom-table-components/data-table-component-config.md index 10ad7b2fc9..2409146735 100644 --- a/docs/docs/reference/admin-ui-api/custom-table-components/data-table-component-config.md +++ b/docs/docs/reference/admin-ui-api/custom-table-components/data-table-component-config.md @@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## DataTableComponentConfig - + Configures a CustomDetailComponent to be placed in the given location. diff --git a/docs/docs/reference/typescript-api/data-access/entity-hydrator.md b/docs/docs/reference/typescript-api/data-access/entity-hydrator.md index 9ac4442c09..317736d30d 100644 --- a/docs/docs/reference/typescript-api/data-access/entity-hydrator.md +++ b/docs/docs/reference/typescript-api/data-access/entity-hydrator.md @@ -11,20 +11,42 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## EntityHydrator - + This is a helper class which is used to "hydrate" entity instances, which means to populate them -with the specified relations. This is useful when writing plugin code which receives an entity +with the specified relations. This is useful when writing plugin code which receives an entity, and you need to ensure that one or more relations are present. *Example* ```ts -const product = await this.productVariantService - .getProductForVariant(ctx, variantId); +import { Injectable } from '@nestjs/common'; +import { ID, RequestContext, EntityHydrator, ProductVariantService } from '@vendure/core'; -await this.entityHydrator - .hydrate(ctx, product, { relations: ['facetValues.facet' ]}); +@Injectable() +export class MyService { + + constructor( + // highlight-next-line + private entityHydrator: EntityHydrator, + private productVariantService: ProductVariantService, + ) {} + + myMethod(ctx: RequestContext, variantId: ID) { + const product = await this.productVariantService + .getProductForVariant(ctx, variantId); + + // at this stage, we don't know which of the Product relations + // will be joined at runtime. + + // highlight-start + await this.entityHydrator + .hydrate(ctx, product, { relations: ['facetValues.facet' ]}); + + // You can be sure now that the `facetValues` & `facetValues.facet` relations are populated + // highlight-end + } +} ``` In this above example, the `product` instance will now have the `facetValues` relation diff --git a/docs/docs/reference/typescript-api/services/customer-service.md b/docs/docs/reference/typescript-api/services/customer-service.md index 9de458b184..2e91471aa6 100644 --- a/docs/docs/reference/typescript-api/services/customer-service.md +++ b/docs/docs/reference/typescript-api/services/customer-service.md @@ -31,8 +31,8 @@ class CustomerService { refreshVerificationToken(ctx: RequestContext, emailAddress: string) => Promise; verifyCustomerEmailAddress(ctx: RequestContext, verificationToken: string, password?: string) => Promise>; requestPasswordReset(ctx: RequestContext, emailAddress: string) => Promise; - resetPassword(ctx: RequestContext, passwordResetToken: string, password: string) => Promise< - User | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError + resetPassword(ctx: RequestContext, passwordResetToken: string, password: string) => Promise< + User | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError >; requestUpdateEmailAddress(ctx: RequestContext, userId: ID, newEmailAddress: string) => Promise; updateEmailAddress(ctx: RequestContext, token: string) => Promise; @@ -69,8 +69,8 @@ class CustomerService { RequestContext, userId: ID, filterOnChannel: = true) => Promise<Customer | undefined>`} /> -Returns the Customer entity associated with the given userId, if one exists. -Setting `filterOnChannel` to `true` will limit the results to Customers which are assigned +Returns the Customer entity associated with the given userId, if one exists. +Setting `filterOnChannel` to `true` will limit the results to Customers which are assigned to the current active Channel only. ### findAddressesByCustomerId @@ -86,13 +86,13 @@ Returns a list of all RequestContext, input: CreateCustomerInput, password?: string) => Promise<ErrorResultUnion<CreateCustomerResult, Customer>>`} /> -Creates a new Customer, including creation of a new User with the special `customer` Role. - -If the `password` argument is specified, the Customer will be immediately verified. If not, -then an AccountRegistrationEvent is published, so that the customer can have their -email address verified and set their password in a later step using the `verifyCustomerEmailAddress()` -method. - +Creates a new Customer, including creation of a new User with the special `customer` Role. + +If the `password` argument is specified, the Customer will be immediately verified. If not, +then an AccountRegistrationEvent is published, so that the customer can have their +email address verified and set their password in a later step using the `verifyCustomerEmailAddress()` +method. + This method is intended to be used in admin-created Customer flows. ### update @@ -113,47 +113,47 @@ This method is intended to be used in admin-created Customer flows. RequestContext, input: RegisterCustomerInput) => Promise<RegisterCustomerAccountResult | EmailAddressConflictError | PasswordValidationError>`} /> -Registers a new Customer account with the NativeAuthenticationStrategy and starts -the email verification flow (unless AuthOptions `requireVerification` is set to `false`) -by publishing an AccountRegistrationEvent. - +Registers a new Customer account with the NativeAuthenticationStrategy and starts +the email verification flow (unless AuthOptions `requireVerification` is set to `false`) +by publishing an AccountRegistrationEvent. + This method is intended to be used in storefront Customer-creation flows. ### refreshVerificationToken RequestContext, emailAddress: string) => Promise<void>`} /> -Refreshes a stale email address verification token by generating a new one and +Refreshes a stale email address verification token by generating a new one and publishing a AccountRegistrationEvent. ### verifyCustomerEmailAddress RequestContext, verificationToken: string, password?: string) => Promise<ErrorResultUnion<VerifyCustomerAccountResult, Customer>>`} /> -Given a valid verification token which has been published in an AccountRegistrationEvent, this +Given a valid verification token which has been published in an AccountRegistrationEvent, this method is used to set the Customer as `verified` as part of the account registration flow. ### requestPasswordReset RequestContext, emailAddress: string) => Promise<void>`} /> -Publishes a new PasswordResetEvent for the given email address. This event creates +Publishes a new PasswordResetEvent for the given email address. This event creates a token which can be used in the `resetPassword()` method. ### resetPassword -RequestContext, passwordResetToken: string, password: string) => Promise< User | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError >`} /> +RequestContext, passwordResetToken: string, password: string) => Promise< User | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError >`} /> -Given a valid password reset token created by a call to the `requestPasswordReset()` method, +Given a valid password reset token created by a call to the `requestPasswordReset()` method, this method will change the Customer's password to that given as the `password` argument. ### requestUpdateEmailAddress RequestContext, userId: ID, newEmailAddress: string) => Promise<boolean | EmailAddressConflictError>`} /> -Publishes a IdentifierChangeRequestEvent for the given User. This event contains a token -which is then used in the `updateEmailAddress()` method to change the email address of the User & +Publishes a IdentifierChangeRequestEvent for the given User. This event contains a token +which is then used in the `updateEmailAddress()` method to change the email address of the User & Customer. ### updateEmailAddress RequestContext, token: string) => Promise<boolean | IdentifierChangeTokenInvalidError | IdentifierChangeTokenExpiredError>`} /> -Given a valid email update token published in a IdentifierChangeRequestEvent, this method +Given a valid email update token published in a IdentifierChangeRequestEvent, this method will update the Customer & User email address. ### createOrUpdate @@ -184,8 +184,8 @@ Creates a new Addre RequestContext, order: Order) => `} /> -If the Customer associated with the given Order does not yet have any Addresses, -this method will create new Address(es) based on the Order's shipping & billing +If the Customer associated with the given Order does not yet have any Addresses, +this method will create new Address(es) based on the Order's shipping & billing addresses. ### addNoteToCustomer diff --git a/docs/docs/reference/typescript-api/shipping/shipping-calculator.md b/docs/docs/reference/typescript-api/shipping/shipping-calculator.md index 0e20f9bc3e..bc37d2af30 100644 --- a/docs/docs/reference/typescript-api/shipping/shipping-calculator.md +++ b/docs/docs/reference/typescript-api/shipping/shipping-calculator.md @@ -31,7 +31,7 @@ const flatRateCalculator = new ShippingCalculator({ ui: { component: 'number-form-input', suffix: '%' }, }, }, - calculate: (order, args) => { + calculate: (ctx, order, args) => { return { price: args.rate, taxRate: args.taxRate, @@ -64,7 +64,7 @@ class ShippingCalculator extends Configurable ## ShippingCalculationResult - + The return value of the CalculateShippingFn. @@ -108,7 +108,7 @@ needed in the storefront application when listing eligible shipping methods. ## CalculateShippingFn - + A function which implements the specific shipping calculation logic. It takes an Order and an arguments object and should return the shipping price as an integer in cents. @@ -120,6 +120,6 @@ type CalculateShippingFn = ( ctx: RequestContext, order: Order, args: ConfigArgValues, - method: ShippingMethod + method: ShippingMethod, ) => CalculateShippingFnResult ``` diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 09807a6d1d..9a710fdb91 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -39,9 +39,7 @@ const config = { docs: { routeBasePath: '/', sidebarPath: require.resolve('./sidebars.js'), - // Please change this to your repo. - // Remove this to remove the "edit this page" links. - editUrl: 'https://github.com/vendure-ecommerce/vendure/tree/new-docs/docs/', + editUrl: 'https://github.com/vendure-ecommerce/vendure/blob/master/docs/', showLastUpdateTime: true, }, blog: false, diff --git a/docs/scraper/config.json b/docs/scraper/config.json index e37898d586..7ba0afdccb 100644 --- a/docs/scraper/config.json +++ b/docs/scraper/config.json @@ -1,12 +1,12 @@ { "index_name": "vendure-docs", "start_urls": [ - "https://vendure-docs-beta.netlify.app/" + "https://docs.vendure.io/" ], "sitemap_urls": [ - "https://vendure-docs-beta.netlify.app/sitemap.xml" + "https://docs.vendure.io/sitemap.xml" ], - "allowed_domains":["vendure-docs-beta.netlify.app"], + "allowed_domains":["docs.vendure.io"], "sitemap_alternate_links": true, "stop_urls": [], "selectors": { diff --git a/docs/src/components/CustomFieldProperty/index.tsx b/docs/src/components/CustomFieldProperty/index.tsx new file mode 100644 index 0000000000..6d7eaa6292 --- /dev/null +++ b/docs/src/components/CustomFieldProperty/index.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import styles from './styles.module.css'; + +export default function CustomFieldProperty(props: { required: boolean; type: string; typeLink?: string }) { + return ( +
+
+ {props.required ? ( + Required + ) : ( + Optional + )} +
+
+ {props.typeLink ? ( + + {props.type} + + ) : ( + {props.type} + )} +
+
+ ); +} diff --git a/docs/src/components/CustomFieldProperty/styles.module.css b/docs/src/components/CustomFieldProperty/styles.module.css new file mode 100644 index 0000000000..c39cb0d49f --- /dev/null +++ b/docs/src/components/CustomFieldProperty/styles.module.css @@ -0,0 +1,7 @@ +.wrapper { + display: flex; + flex-wrap: wrap; + gap: 4px 12px; + margin-top: -12px; + margin-bottom: 22px; +} diff --git a/docs/static/_redirects b/docs/static/_redirects new file mode 100644 index 0000000000..95e6949e57 --- /dev/null +++ b/docs/static/_redirects @@ -0,0 +1,22 @@ +# Add redirects for popular pages from old docs +/getting-started /guides/getting-started/installation +/developer-guide/customizing-models /guides/developer-guide/custom-fields +/developer-guide/authentication /guides/core-concepts/auth +/developer-guide/payment-integrations /reference/core-plugins/payments-plugin +/plugins /guides/developer-guide/plugins +/plugins/extending-the-admin-ui /guides/extending-the-admin-ui/getting-started +/storefront/building-a-storefront /guides/storefront/storefront-starters +/developer-guide/multi-vendor-marketplaces /guides/how-to/multi-vendor-marketplaces +/developer-guide/configuration /guides/developer-guide/configuration +/plugins/plugin-examples/extending-graphql-api /guides/developer-guide/extend-graphql-api +/storefront/order-workflow /guides/storefront/active-order +/typescript-api/custom-fields /reference/typescript-api/custom-fields +/developer-guide/migrations /guides/developer-guide/migrations +/typescript-api/core-plugins/payments-plugin/stripe-plugin /reference/core-plugins/payments-plugin/stripe-plugin +/developer-guide/channels /guides/core-concepts/channels +/graphql-api/shop /reference/graphql-api/shop/queries +/developer-guide/customizing-the-order-process /guides/core-concepts/orders +/developer-guide/importing-product-data /guides/developer-guide/importing-data +/plugins/plugin-examples /guides/developer-guide/plugins +/plugins/writing-a-vendure-plugin /guides/developer-guide/plugins +/deployment/using-docker /guides/deployment/using-docker diff --git a/lerna.json b/lerna.json index 9bd1a14a2b..5724fa51b1 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "packages": ["packages/*"], - "version": "2.1.0", + "version": "2.1.1", "npmClient": "yarn", "command": { "version": { diff --git a/packages/admin-ui-plugin/package.json b/packages/admin-ui-plugin/package.json index 5ba4b10852..a3c4abf95f 100644 --- a/packages/admin-ui-plugin/package.json +++ b/packages/admin-ui-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/admin-ui-plugin", - "version": "2.1.0", + "version": "2.1.1", "main": "lib/index.js", "types": "lib/index.d.ts", "files": [ @@ -21,8 +21,8 @@ "devDependencies": { "@types/express": "^4.17.8", "@types/fs-extra": "^9.0.1", - "@vendure/common": "^2.1.0", - "@vendure/core": "^2.1.0", + "@vendure/common": "^2.1.1", + "@vendure/core": "^2.1.1", "express": "^4.17.1", "rimraf": "^3.0.2", "typescript": "4.9.5" diff --git a/packages/admin-ui-plugin/src/constants.ts b/packages/admin-ui-plugin/src/constants.ts index 85de601824..ae226c25a2 100644 --- a/packages/admin-ui-plugin/src/constants.ts +++ b/packages/admin-ui-plugin/src/constants.ts @@ -24,4 +24,5 @@ export const defaultAvailableLanguages = [ LanguageCode.it, LanguageCode.fa, LanguageCode.ne, + LanguageCode.hr, ]; diff --git a/packages/admin-ui/i18n-coverage.json b/packages/admin-ui/i18n-coverage.json index e67e57c87e..b3884e559e 100644 --- a/packages/admin-ui/i18n-coverage.json +++ b/packages/admin-ui/i18n-coverage.json @@ -1,6 +1,7 @@ { - "generatedOn": "2023-10-02T08:18:19.015Z", - "lastCommit": "e92e8207cb0296964e779bc81f731a035ec76768", + "generatedOn": "2023-10-17T19:32:07.745Z", + "lastCommit": "97bc099006adda7449a7b63e72191c2bb7e7b4ed", + "translationStatus": { "ar": { "tokenCount": 761, @@ -47,6 +48,11 @@ "translatedCount": 760, "percentage": 100 }, + "ne": { + "tokenCount": 761, + "translatedCount": 725, + "percentage": 95 + }, "pl": { "tokenCount": 761, "translatedCount": 400, diff --git a/packages/admin-ui/package-lock.json b/packages/admin-ui/package-lock.json index c4e21caa31..28c7689beb 100644 --- a/packages/admin-ui/package-lock.json +++ b/packages/admin-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "@vendure/admin-ui", - "version": "2.1.0", + "version": "2.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/admin-ui/package.json b/packages/admin-ui/package.json index ff463be840..ea8074581c 100644 --- a/packages/admin-ui/package.json +++ b/packages/admin-ui/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/admin-ui", - "version": "2.1.0", + "version": "2.1.1", "license": "MIT", "scripts": { "ng": "ng", @@ -49,7 +49,7 @@ "@ng-select/ng-select": "^11.1.1", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", - "@vendure/common": "^2.1.0", + "@vendure/common": "^2.1.1", "@webcomponents/custom-elements": "^1.6.0", "apollo-angular": "^5.0.0", "apollo-upload-client": "^17.0.0", diff --git a/packages/admin-ui/src/lib/catalog/src/catalog.module.ts b/packages/admin-ui/src/lib/catalog/src/catalog.module.ts index eb39d6ef01..bb92ca351f 100644 --- a/packages/admin-ui/src/lib/catalog/src/catalog.module.ts +++ b/packages/admin-ui/src/lib/catalog/src/catalog.module.ts @@ -9,7 +9,6 @@ import { GetFacetDetailDocument, GetProductDetailDocument, GetProductVariantDetailDocument, - GetStockLocationDetailDocument, PageService, SharedModule, } from '@vendure/admin-ui/core'; @@ -57,12 +56,12 @@ import { import { ProductListComponent } from './components/product-list/product-list.component'; import { ProductOptionsEditorComponent } from './components/product-options-editor/product-options-editor.component'; import { ProductVariantDetailComponent } from './components/product-variant-detail/product-variant-detail.component'; -import { ProductVariantListComponent } from './components/product-variant-list/product-variant-list.component'; import { assignProductVariantsToChannelBulkAction, - removeProductVariantsFromChannelBulkAction, deleteProductVariantsBulkAction, + removeProductVariantsFromChannelBulkAction, } from './components/product-variant-list/product-variant-list-bulk-actions'; +import { ProductVariantListComponent } from './components/product-variant-list/product-variant-list.component'; import { ProductVariantQuickJumpComponent } from './components/product-variant-quick-jump/product-variant-quick-jump.component'; import { ProductVariantsEditorComponent } from './components/product-variants-editor/product-variants-editor.component'; import { ProductVariantsTableComponent } from './components/product-variants-table/product-variants-table.component'; @@ -118,10 +117,7 @@ const CATALOG_COMPONENTS = [ ], }) export class CatalogModule { - constructor( - private bulkActionRegistryService: BulkActionRegistryService, - private pageService: PageService, - ) { + constructor(bulkActionRegistryService: BulkActionRegistryService, pageService: PageService) { bulkActionRegistryService.registerBulkAction(assignFacetValuesToProductsBulkAction); bulkActionRegistryService.registerBulkAction(assignProductsToChannelBulkAction); bulkActionRegistryService.registerBulkAction(assignProductVariantsToChannelBulkAction); @@ -170,30 +166,6 @@ export class CatalogModule { route: 'variants', component: ProductVariantListComponent, }); - // pageService.registerPageTab({ - // priority: 0, - // location: 'stock-location-detail', - // tab: _('catalog.stock-location'), - // route: '', - // component: detailComponentWithResolver({ - // component: StockLocationDetailComponent, - // query: GetStockLocationDetailDocument, - // entityKey: 'stockLocation', - // getBreadcrumbs: entity => [ - // { - // label: entity ? entity.name : _('catalog.create-new-stock-location'), - // link: [entity?.id], - // }, - // ], - // }), - // }); - // pageService.registerPageTab({ - // priority: 0, - // location: 'product-list', - // tab: _('catalog.stock-locations'), - // route: 'stock-locations', - // component: StockLocationListComponent, - // }); pageService.registerPageTab({ priority: 0, location: 'product-variant-detail', diff --git a/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html b/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html index a9ecbb895b..189e242e1b 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html +++ b/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html @@ -198,6 +198,7 @@ diff --git a/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts b/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts index 07d030d4e1..87e8daa4c0 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts +++ b/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { Asset, @@ -20,7 +19,6 @@ import { PRODUCT_DETAIL_FRAGMENT, ProductDetailFragment, ProductVariantFragment, - ServerConfigService, TypedBaseDetailComponent, unicodePatternValidator, UpdateProductInput, @@ -33,17 +31,8 @@ import { normalizeString } from '@vendure/common/lib/normalize-string'; import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants'; import { unique } from '@vendure/common/lib/unique'; import { gql } from 'apollo-angular'; -import { combineLatest, concat, EMPTY, from, Observable } from 'rxjs'; -import { - distinctUntilChanged, - map, - mergeMap, - shareReplay, - skip, - switchMap, - switchMapTo, - take, -} from 'rxjs/operators'; +import { combineLatest, concat, EMPTY, from, Observable, of } from 'rxjs'; +import { distinctUntilChanged, map, mergeMap, shareReplay, switchMap, take } from 'rxjs/operators'; import { ProductDetailService } from '../../providers/product-detail/product-detail.service'; import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dialog.component'; @@ -91,9 +80,6 @@ export class ProductDetailComponent public readonly updatePermissions = [Permission.UpdateCatalog, Permission.UpdateProduct]; constructor( - route: ActivatedRoute, - router: Router, - serverConfigService: ServerConfigService, private productDetailService: ProductDetailService, private formBuilder: FormBuilder, private modalService: ModalService, @@ -106,11 +92,15 @@ export class ProductDetailComponent ngOnInit() { this.init(); - const productFacetValues$ = this.entity$.pipe(map(product => product.facetValues)); + + const productFacetValues$ = this.isNew$.pipe( + switchMap(isNew => { + return isNew ? of([]) : this.entity$.pipe(map(product => product.facetValues)); + }), + ); const productGroup = this.detailForm; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const formFacetValueIdChanges$ = productGroup.get('facetValueIds')!.valueChanges.pipe( - skip(1), distinctUntilChanged(), switchMap(ids => this.dataService.facet @@ -121,7 +111,7 @@ export class ProductDetailComponent ); this.facetValues$ = concat( productFacetValues$.pipe(take(1)), - productFacetValues$.pipe(switchMapTo(formFacetValueIdChanges$)), + productFacetValues$.pipe(switchMap(() => formFacetValueIdChanges$)), ); this.productChannels$ = this.entity$.pipe(map(p => p.channels)); } @@ -261,6 +251,7 @@ export class ProductDetailComponent facetValueIds: unique([...currentFacetValueIds, ...facetValueIds]), }); productGroup.markAsDirty(); + this.changeDetector.markForCheck(); } }); } diff --git a/packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list.component.html b/packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list.component.html index 1b11b1eb38..62466347e3 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list.component.html +++ b/packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list.component.html @@ -7,7 +7,7 @@ this.dataService.product .getPendingSearchIndexUpdates() diff --git a/packages/admin-ui/src/lib/core/src/extension/register-route-component.ts b/packages/admin-ui/src/lib/core/src/extension/register-route-component.ts index b3f3a8e8ba..df36938b75 100644 --- a/packages/admin-ui/src/lib/core/src/extension/register-route-component.ts +++ b/packages/admin-ui/src/lib/core/src/extension/register-route-component.ts @@ -86,6 +86,19 @@ export function registerRouteComponent< const breadcrumbSubject$ = new BehaviorSubject(options.breadcrumb ?? ''); const titleSubject$ = new BehaviorSubject(options.title); + if (getBreadcrumbs != null && (query == null || entityKey == null)) { + console.error( + [ + `[${ + options.path ?? 'custom' + } route] When using the "getBreadcrumbs" option, the "query" and "entityKey" options must also be provided.`, + ``, + `Alternatively, use the "breadcrumb" option instead, or use the "PageMetadataService" inside your Angular component`, + `or the "usePageMetadata" React hook to set the breadcrumb.`, + ].join('\n'), + ); + } + const resolveFn: | ResolveFn<{ entity: Observable[Field] | null>; @@ -118,7 +131,7 @@ export function registerRouteComponent< data: { breadcrumb: breadcrumbSubject$, ...(options.routeConfig?.data ?? {}), - ...(getBreadcrumbs + ...(getBreadcrumbs && query && entityKey ? { breadcrumb: data => data.detail.entity.pipe(map((entity: any) => getBreadcrumbs(entity))), diff --git a/packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts b/packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts index 69abd9ecb3..bd555a9985 100644 --- a/packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts +++ b/packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts @@ -1,7 +1,18 @@ import { Injectable } from '@angular/core'; import { notNullOrUndefined } from '@vendure/common/lib/shared-utils'; -import { BehaviorSubject, combineLatest, interval, isObservable, Observable, Subject, switchMap } from 'rxjs'; -import { map, mapTo, startWith, take } from 'rxjs/operators'; +import { + BehaviorSubject, + combineLatest, + interval, + isObservable, + Observable, + of, + Subject, + switchMap, +} from 'rxjs'; +import { filter, first, map, mapTo, startWith, take } from 'rxjs/operators'; +import { Permission } from '../../common/generated-types'; +import { DataService } from '../../data/providers/data.service'; export interface AlertConfig { id: string; @@ -10,6 +21,7 @@ export interface AlertConfig { isAlert: (value: T) => boolean; action: (data: T) => void; label: (data: T) => { text: string; translationVars?: { [key: string]: string | number } }; + requiredPermissions?: Permission[]; } export interface ActiveAlert { @@ -74,7 +86,7 @@ export class AlertsService { private alertsMap = new Map>(); private configUpdated = new Subject(); - constructor() { + constructor(private dataService: DataService) { const alerts$ = this.configUpdated.pipe( mapTo([...this.alertsMap.values()]), startWith([...this.alertsMap.values()]), @@ -91,8 +103,26 @@ export class AlertsService { } configureAlert(config: AlertConfig) { - this.alertsMap.set(config.id, new Alert(config)); - this.configUpdated.next(); + this.hasSufficientPermissions(config.requiredPermissions) + .pipe(first()) + .subscribe(hasSufficientPermissions => { + if (hasSufficientPermissions) { + this.alertsMap.set(config.id, new Alert(config)); + this.configUpdated.next(); + } + }); + } + + hasSufficientPermissions(permissions?: Permission[]) { + if (!permissions || permissions.length === 0) { + return of(true); + } + return this.dataService.client.userStatus().stream$.pipe( + filter(({ userStatus }) => userStatus.isLoggedIn), + map(({ userStatus }) => + permissions.some(permission => userStatus.permissions.includes(permission)), + ), + ); } refresh(id?: string) { diff --git a/packages/admin-ui/src/lib/core/src/public_api.ts b/packages/admin-ui/src/lib/core/src/public_api.ts index 8e6acb6291..0d33c245b8 100644 --- a/packages/admin-ui/src/lib/core/src/public_api.ts +++ b/packages/admin-ui/src/lib/core/src/public_api.ts @@ -113,6 +113,7 @@ export * from './providers/i18n/custom-message-format-compiler'; export * from './providers/i18n/i18n.service'; export * from './providers/job-queue/job-queue.service'; export * from './providers/local-storage/local-storage.service'; +export * from './providers/localization/localization.service'; export * from './providers/modal/modal.service'; export * from './providers/modal/modal.types'; export * from './providers/nav-builder/nav-builder-types'; diff --git a/packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-component.service.ts b/packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-component.service.ts index c58014e449..719efca353 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-component.service.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-custom-component.service.ts @@ -9,6 +9,7 @@ export type DataTableLocationId = | 'edit-options-list' | 'manage-product-variant-list' | 'customer-order-list' + | 'product-detail-variants-list' | string; export type DataTableColumnId = diff --git a/packages/admin-ui/src/lib/core/src/shared/components/extension-host/extension-host.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/extension-host/extension-host.component.ts index f4f7e4ed74..ffabc06563 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/extension-host/extension-host.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/extension-host/extension-host.component.ts @@ -9,6 +9,7 @@ import { } from '@angular/core'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; +import { SharedModule } from '../../shared.module'; import { ExtensionHostConfig } from './extension-host-config'; import { ExtensionHostService } from './extension-host.service'; @@ -22,6 +23,8 @@ import { ExtensionHostService } from './extension-host.service'; templateUrl: './extension-host.component.html', styleUrls: ['./extension-host.component.scss'], changeDetection: ChangeDetectionStrategy.Default, + standalone: true, + imports: [SharedModule], providers: [ExtensionHostService], }) export class ExtensionHostComponent implements OnInit, AfterViewInit, OnDestroy { diff --git a/packages/admin-ui/src/lib/core/src/shared/components/product-multi-selector-dialog/product-multi-selector-dialog.component.html b/packages/admin-ui/src/lib/core/src/shared/components/product-multi-selector-dialog/product-multi-selector-dialog.component.html index 0a0f9737cd..538c96f4e6 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/product-multi-selector-dialog/product-multi-selector-dialog.component.html +++ b/packages/admin-ui/src/lib/core/src/shared/components/product-multi-selector-dialog/product-multi-selector-dialog.component.html @@ -37,6 +37,12 @@ {{ mode === 'product' ? item.productName : item.productVariantName }} +
+ + {{ 'common.slug' | translate }}: + {{ item.slug }} + +
{{ item.sku }}
diff --git a/packages/admin-ui/src/lib/core/src/shared/components/product-multi-selector-dialog/product-multi-selector-dialog.component.scss b/packages/admin-ui/src/lib/core/src/shared/components/product-multi-selector-dialog/product-multi-selector-dialog.component.scss index 86d39a37a0..def20113c7 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/product-multi-selector-dialog/product-multi-selector-dialog.component.scss +++ b/packages/admin-ui/src/lib/core/src/shared/components/product-multi-selector-dialog/product-multi-selector-dialog.component.scss @@ -74,3 +74,4 @@ vdr-select-toggle { align-items: center; justify-content: space-between; } + diff --git a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/code-editor-form-input/base-code-editor-form-input.component.ts b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/code-editor-form-input/base-code-editor-form-input.component.ts index 0a3749abd9..b02f28a14a 100644 --- a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/code-editor-form-input/base-code-editor-form-input.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/code-editor-form-input/base-code-editor-form-input.component.ts @@ -48,7 +48,12 @@ export abstract class BaseCodeEditorFormInputComponent implements FormInputCompo editor.innerHTML = this.highlight(code, this.getErrorPos(this.errorMessage)); }; this.jar = CodeJar(this.editorElementRef.nativeElement, highlight); + let isFirstUpdate = true; this.jar.onUpdate(value => { + if (isFirstUpdate) { + isFirstUpdate = false; + return; + } this.formControl.setValue(value); this.formControl.markAsDirty(); this.isValid = this.formControl.valid; diff --git a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/customer-group-form-input/customer-group-form-input.component.ts b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/customer-group-form-input/customer-group-form-input.component.ts index df8ab3fa39..f30afa2939 100644 --- a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/customer-group-form-input/customer-group-form-input.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/customer-group-form-input/customer-group-form-input.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; -import { FormControl, UntypedFormControl } from '@angular/forms'; +import { FormControl } from '@angular/forms'; import { DefaultFormComponentConfig, DefaultFormComponentId } from '@vendure/common/lib/shared-types'; import { Observable } from 'rxjs'; import { startWith } from 'rxjs/operators'; @@ -42,13 +42,12 @@ export class CustomerGroupFormInputComponent implements FormInputComponent, OnIn } selectGroup(group: ItemOf) { - this.formControl.setValue(group ?? undefined); + this.formControl.setValue(group?.id ?? undefined); } - compareWith( - o1: ItemOf, - o2: ItemOf, - ) { - return o1.id === o2.id; + compareWith | string>(o1: T, o2: T) { + const id1 = typeof o1 === 'string' ? o1 : o1.id; + const id2 = typeof o2 === 'string' ? o2 : o2.id; + return id1 === id2; } } diff --git a/packages/admin-ui/src/lib/core/src/shared/shared.module.ts b/packages/admin-ui/src/lib/core/src/shared/shared.module.ts index 5f03fd28de..9ca91622c4 100644 --- a/packages/admin-ui/src/lib/core/src/shared/shared.module.ts +++ b/packages/admin-ui/src/lib/core/src/shared/shared.module.ts @@ -59,7 +59,6 @@ import { DropdownComponent } from './components/dropdown/dropdown.component'; import { EditNoteDialogComponent } from './components/edit-note-dialog/edit-note-dialog.component'; import { EmptyPlaceholderComponent } from './components/empty-placeholder/empty-placeholder.component'; import { EntityInfoComponent } from './components/entity-info/entity-info.component'; -import { ExtensionHostComponent } from './components/extension-host/extension-host.component'; import { FacetValueChipComponent } from './components/facet-value-chip/facet-value-chip.component'; import { FacetValueSelectorComponent } from './components/facet-value-selector/facet-value-selector.component'; import { FocalPointControlComponent } from './components/focal-point-control/focal-point-control.component'; @@ -249,7 +248,6 @@ const DECLARATIONS = [ ChannelAssignmentControlComponent, ChannelLabelPipe, IfDefaultChannelActiveDirective, - ExtensionHostComponent, CustomFieldLabelPipe, CustomFieldDescriptionPipe, FocalPointControlComponent, diff --git a/packages/admin-ui/src/lib/login/src/components/login/login.component.html b/packages/admin-ui/src/lib/login/src/components/login/login.component.html index 5651dc490c..0f8a870f17 100644 --- a/packages/admin-ui/src/lib/login/src/components/login/login.component.html +++ b/packages/admin-ui/src/lib/login/src/components/login/login.component.html @@ -1,4 +1,4 @@ -